-
Notifications
You must be signed in to change notification settings - Fork 22
/
auth.ts
132 lines (125 loc) · 3.86 KB
/
auth.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import { stringify } from "https://deno.land/std@0.147.0/encoding/yaml.ts";
import * as path from "https://deno.land/std@0.147.0/path/mod.ts";
import { ensureDir } from "https://deno.land/std@0.147.0/fs/mod.ts";
import { wait as spinner } from "https://deno.land/x/wait@0.1.12/mod.ts";
import wait from "../../core/runtime/async/wait.ts";
import { getDefaultGhConfigPath } from "./index.ts";
// https://github.com/cli/cli/blob/trunk/internal/authflow/flow.go#L18-L23
const oauthClientId = "178c6fc778ccc68e1d6a";
const oauthClientSecret = "34ddeff2b558a23d38fba8a6de74f086ede1cc0b";
const scopes = ["repo", "read:org", "gist"];
const grantType = "urn:ietf:params:oauth:grant-type:device_code";
function getDeviceInitUrl(host: string) {
return `https://${host}/login/device/code`;
}
function getTokenUrl(host: string) {
return `https://${host}/login/oauth/access_token`;
}
export interface RequestCodeResult {
deviceCode: string;
expiresIn: number;
interval: number;
userCode: string;
verificationUri: string;
}
export async function requestCode(): Promise<RequestCodeResult> {
const res = await fetch(getDeviceInitUrl("github.com"), {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: oauthClientId,
scope: scopes.join(" "),
}),
});
const resText = await res.text();
const parsedRes = new URLSearchParams(resText);
return {
deviceCode: parsedRes.get("device_code") ?? "",
expiresIn: Number(parsedRes.get("expires_in")),
interval: Number(parsedRes.get("interval")),
userCode: parsedRes.get("user_code") ?? "",
verificationUri: parsedRes.get("verification_uri") ?? "",
};
}
export async function validateToken(token: string): Promise<void> {
const res = await fetch("https://api.github.com/", {
headers: {
Authorization: `token ${token}`,
},
});
if (!res.ok) {
if (res.status === 401) throw new PollapoUnauthorizedError();
throw new Error(
`Unexpected HTTP request failure with response ${res.status}`,
);
}
}
export class PollapoUnauthorizedError extends Error {
constructor() {
super("Unauthorized Github API token.");
}
}
interface PollTokenResponse {
accessToken: string;
tokenType: string;
scope: string;
}
export async function pollToken(
code: RequestCodeResult,
): Promise<PollTokenResponse> {
const { interval } = code;
const startDate = new Date();
const expireDate = new Date(startDate);
expireDate.setSeconds(
startDate.getSeconds() + code.expiresIn,
);
const loading = spinner("Wait for authorization complete...").start();
while (true) {
await wait(interval * 1000);
try {
const res = await fetch(getTokenUrl("github.com"), {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
client_id: oauthClientId,
device_code: code.deviceCode,
grant_type: grantType,
}),
});
const resText = await res.text();
const parsedRes = new URLSearchParams(resText);
const resError = parsedRes.get("error");
if (resError) {
throw new Error(resError);
}
loading.stop();
return {
accessToken: parsedRes.get("access_token") ?? "",
tokenType: parsedRes.get("token_type") ?? "",
scope: parsedRes.get("scope") ?? "",
};
} catch (err) {
if (err.message !== "authorization_pending") {
loading.stop();
throw err;
}
}
}
}
export async function writeGhHosts(
token: string,
hostsFilePath = getDefaultGhConfigPath("hosts.yml"),
) {
const hostsData = {
"github.com": {
"oauth_token": token,
"git_protocol": "ssh",
},
};
await ensureDir(path.dirname(hostsFilePath));
await Deno.writeTextFile(hostsFilePath, stringify(hostsData));
}