Skip to content

Commit a9815b3

Browse files
committed
fixup! feat(ng-dev): create auth subcommand for ng-dev
1 parent 64b1fb2 commit a9815b3

File tree

3 files changed

+101
-62
lines changed

3 files changed

+101
-62
lines changed

ng-dev/auth/shared/firebase.ts

+25-15
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
linkWithCredential,
66
GithubAuthProvider,
77
} from 'firebase/auth';
8+
import {Log} from '../../utils/logging.js';
89
import {
910
deviceCodeOAuthDance,
1011
authorizationCodeOAuthDance,
@@ -15,22 +16,31 @@ import {
1516
export async function loginToFirebase() {
1617
/** The type of OAuth dance to do based on whether a session display is available. */
1718
const oAuthDance = process.env.DISPLAY ? authorizationCodeOAuthDance : deviceCodeOAuthDance;
18-
/** The id and access tokens for Google from the oauth login. */
19-
const {idToken, accessToken} = await oAuthDance(GoogleOAuthDanceConfig);
20-
/** The credential generated by the GoogleAuthProvider from the OAuth tokens. */
21-
const googleCredential = GoogleAuthProvider.credential(idToken, accessToken);
22-
/** The newly signed in user. */
23-
const {user} = await signInWithCredential(getAuth(), googleCredential);
19+
try {
20+
/** The id and access tokens for Google from the oauth login. */
21+
const {idToken, accessToken} = await oAuthDance(GoogleOAuthDanceConfig);
22+
/** The credential generated by the GoogleAuthProvider from the OAuth tokens. */
23+
const googleCredential = GoogleAuthProvider.credential(idToken, accessToken);
24+
/** The newly signed in user. */
25+
const {user} = await signInWithCredential(getAuth(), googleCredential);
2426

25-
// If the user already has a github account linked to their account, the login is complete.
26-
if (user.providerData.find((provider) => provider.providerId === 'github.com')) {
27-
return true;
28-
}
27+
// If the user already has a github account linked to their account, the login is complete.
28+
if (user.providerData.find((provider) => provider.providerId === 'github.com')) {
29+
return true;
30+
}
2931

30-
/** The access token for Github from the oauth login. */
31-
const {accessToken: githubAccessToken} = await oAuthDance(GithubOAuthDanceConfig);
32+
/** The access token for Github from the oauth login. */
33+
const {accessToken: githubAccessToken} = await oAuthDance(GithubOAuthDanceConfig);
3234

33-
// Link the Github account to the account for the currently logged in user.
34-
await linkWithCredential(user, GithubAuthProvider.credential(githubAccessToken));
35-
return true;
35+
// Link the Github account to the account for the currently logged in user.
36+
await linkWithCredential(user, GithubAuthProvider.credential(githubAccessToken));
37+
return true;
38+
} catch (e) {
39+
if (e instanceof Error) {
40+
Log.error(`${e.name}: ${e.message}`);
41+
} else {
42+
Log.error(e);
43+
}
44+
return false;
45+
}
3646
}

ng-dev/auth/shared/ng-dev-token.ts

+30-3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import {mkdir, readFile, stat, writeFile} from 'fs/promises';
44
import {homedir} from 'os';
55
import {join} from 'path';
66
import {Log} from '../../utils/logging.js';
7+
import {randomBytes, createCipheriv, createDecipheriv} from 'crypto';
8+
9+
/** Algorithm to use for encryption. */
10+
const algorithm = 'aes-256-ctr';
711

812
/** Data for an ng-dev token. */
913
interface NgDevToken {
@@ -89,7 +93,7 @@ export async function getCurrentUser() {
8993
/** Save the token to the file system as a base64 encoded string. */
9094
async function saveTokenToFileSystem(data: NgDevToken) {
9195
await mkdir(tokenDir, {recursive: true});
92-
await writeFile(tokenPath, Buffer.from(JSON.stringify(data), 'utf8').toString('base64'));
96+
await writeFile(tokenPath, encrypt(JSON.stringify(data)));
9397
}
9498

9599
/** Retrieve the token from the file system. */
@@ -98,6 +102,29 @@ async function retrieveTokenFromFileSystem(): Promise<NgDevToken | null> {
98102
return null;
99103
}
100104

101-
const rawToken = Buffer.from(await readFile(tokenPath, 'utf8'), 'base64').toString('utf8');
102-
return JSON.parse(rawToken) as NgDevToken;
105+
const rawToken = Buffer.from(await readFile(tokenPath)).toString();
106+
console.log(decrypt(rawToken));
107+
return JSON.parse(decrypt(rawToken)) as NgDevToken;
108+
}
109+
110+
/** Encrypt the provided string. */
111+
function encrypt(text: string) {
112+
const iv = randomBytes(16);
113+
const key = randomBytes(32);
114+
let cipher = createCipheriv(algorithm, key, iv);
115+
let encrypted = cipher.update(text);
116+
encrypted = Buffer.concat([encrypted, cipher.final()]);
117+
return iv.toString('hex') + ':' + encrypted.toString('hex');
118+
}
119+
120+
/** Decrypt the provided string. */
121+
function decrypt(text: string) {
122+
let textParts = text.split(':');
123+
let key = Buffer.from(textParts.shift()!, 'hex');
124+
let iv = Buffer.from(textParts.shift()!, 'hex');
125+
let encryptedText = Buffer.from(textParts.join(':'), 'hex');
126+
let decipher = createDecipheriv(algorithm, key, iv);
127+
let decrypted = decipher.update(encryptedText);
128+
decrypted = Buffer.concat([decrypted, decipher.final()]);
129+
return decrypted.toString();
103130
}

ng-dev/auth/shared/oauth.ts

+46-44
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ import {Log} from '../../utils/logging.js';
22
import fetch from 'node-fetch';
33

44
import {
5+
AuthorizationError,
6+
AuthorizationErrorJson,
57
AuthorizationNotifier,
68
AuthorizationRequest,
79
AuthorizationServiceConfiguration,
810
BaseTokenRequestHandler,
911
GRANT_TYPE_AUTHORIZATION_CODE,
1012
TokenRequest,
1113
TokenResponse,
14+
TokenResponseJson,
1215
} from '@openid/appauth';
1316
import {NodeRequestor} from '@openid/appauth/built/node_support/node_requestor.js';
1417
import {NodeBasedHandler} from '@openid/appauth/built/node_support/node_request_handler.js';
@@ -32,16 +35,16 @@ export async function authorizationCodeOAuthDance({
3235
authConfig,
3336
scope,
3437
}: OAuthDanceConfig): Promise<TokenResponse> {
35-
if (client_id === undefined) {
36-
throw Error();
37-
}
38+
/** Requestor instance for NodeJS usage. */
3839
const requestor = new NodeRequestor();
39-
40+
/** Notifier to watch for authorization completion. */
4041
const notifier = new AuthorizationNotifier();
42+
/** Handler for node based requests. */
4143
const authorizationHandler = new NodeBasedHandler();
4244

4345
authorizationHandler.setAuthorizationNotifier(notifier);
4446

47+
/** The authorization request. */
4548
let request = new AuthorizationRequest({
4649
client_id,
4750
scope,
@@ -51,6 +54,7 @@ export async function authorizationCodeOAuthDance({
5154
'access_type': 'offline',
5255
},
5356
});
57+
5458
authorizationHandler.performAuthorizationRequest(authConfig, request);
5559
await authorizationHandler.completeAuthorizationRequestIfPossible();
5660
const authorization = await authorizationHandler.authorizationPromise;
@@ -84,10 +88,6 @@ export async function deviceCodeOAuthDance({
8488
deviceAuthEndpoint,
8589
scope,
8690
}: OAuthDanceConfig): Promise<TokenResponse> {
87-
if (client_id === undefined) {
88-
throw Error();
89-
}
90-
9191
// Set up and configure the authentication url to initiate the OAuth dance.
9292
const url = new URL(deviceAuthEndpoint);
9393
url.searchParams.append('scope', scope);
@@ -101,16 +101,26 @@ export async function deviceCodeOAuthDance({
101101
headers: {'Accept': 'application/json'},
102102
}).then(
103103
(resp) =>
104-
resp.json() as unknown as {
104+
resp.json() as Promise<{
105105
verification_uri: string;
106106
verification_url: string;
107107
interval: number;
108108
user_code: string;
109109
expires_in: number;
110110
device_code: string;
111-
},
111+
}>,
112112
);
113113

114+
if (
115+
isAuthorizationError(response) &&
116+
(response.error === 'invalid_client' ||
117+
response.error === 'unsupported_grant_type' ||
118+
response.error === 'invalid_grant' ||
119+
response.error === 'invalid_request')
120+
) {
121+
throw new OAuthDanceError(new AuthorizationError(response).errorDescription || 'Unknown Error');
122+
}
123+
114124
Log.info(`Please visit: ${response.verification_uri || response.verification_url}`);
115125
Log.info(`Enter your one time ID code: ${response.user_code}`);
116126

@@ -124,59 +134,36 @@ export async function deviceCodeOAuthDance({
124134

125135
while (true) {
126136
if (Date.now() > oauthDanceTimeout) {
127-
throw {
128-
authenticated: false,
129-
message: 'Failed to completed OAuth authentication before the user code expired.',
130-
};
137+
throw new OAuthDanceError(
138+
'Failed to completed OAuth authentication before the user code expired.',
139+
);
131140
}
132141
// Wait for the requested interval before polling, this is done before the request as it is unnecessary to
133142
//immediately poll while the user has to perform the auth out of this flow.
134143
await new Promise((resolve) => setTimeout(resolve, response.interval * 1000 + pollingBackoff));
135144

136-
const result = await pollAuthServer(
145+
const result = await checkStatusOfAuthServer(
137146
authConfig.tokenEndpoint,
138147
response.device_code,
139148
client_id,
140149
client_secret,
141150
);
142-
if (!result.error) {
143-
return {
144-
...result,
145-
idToken: result.id_token,
146-
accessToken: result.access_token,
147-
};
151+
152+
if (!isAuthorizationError(result)) {
153+
return new TokenResponse(result);
148154
}
149155
if (result.error === 'access_denied') {
150-
throw {
151-
authenticated: false,
152-
message: 'Unable to authorize, as access was denied during the OAuth flow.',
153-
};
154-
}
155-
156-
if (result.error === 'authorization_pending') {
157-
// Update messaging.
156+
throw new OAuthDanceError('Unable to authorize, as access was denied during the OAuth flow.');
158157
}
159158

160159
if (result.error === 'slow_down') {
161-
// Update messaging.
160+
Log.debug('"slow_down" response from server, backing off polling interval by 5 seconds');
162161
pollingBackoff += 5000;
163162
}
164-
165-
if (
166-
result.error === 'invalid_client' ||
167-
result.error === 'unsupported_grant_type' ||
168-
result.error === 'invalid_grant' ||
169-
result.error === 'invalid_request'
170-
) {
171-
throw {
172-
authenticated: false,
173-
message: result.errorDescription,
174-
};
175-
}
176163
}
177164
}
178165

179-
async function pollAuthServer(
166+
async function checkStatusOfAuthServer(
180167
serverUrl: string,
181168
deviceCode: string,
182169
clientId: string,
@@ -193,7 +180,7 @@ async function pollAuthServer(
193180
return await fetch(url.toString(), {
194181
method: 'POST',
195182
headers: {'Accept': 'application/json'},
196-
}).then((x) => x.json() as Promise<any>);
183+
}).then((x) => x.json() as Promise<TokenResponseJson | AuthorizationErrorJson>);
197184
}
198185

199186
// NOTE: the `client_secret`s are okay to be included in this code as these values are sent
@@ -242,3 +229,18 @@ export const GithubOAuthDanceConfig: OAuthDanceConfig = {
242229
}),
243230
deviceAuthEndpoint: 'https://github.com/login/device/code',
244231
};
232+
233+
class OAuthDanceError extends Error {
234+
constructor(message: string) {
235+
super(message);
236+
}
237+
}
238+
239+
function isAuthorizationError<T>(
240+
result: T | AuthorizationErrorJson,
241+
): result is AuthorizationErrorJson {
242+
if ((result as AuthorizationErrorJson).error !== undefined) {
243+
return true;
244+
}
245+
return false;
246+
}

0 commit comments

Comments
 (0)