Skip to content

Commit

Permalink
feat(ng-dev): add logging and messaging to ng-dev auth login (#798)
Browse files Browse the repository at this point in the history
Add additional logging and information about the log in process for ng-dev auth login.

PR Close #798
  • Loading branch information
josephperrott committed Aug 26, 2022
1 parent 62546cf commit 88c198a
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 36 deletions.
28 changes: 27 additions & 1 deletion ng-dev/auth/shared/firebase.ts
Expand Up @@ -5,7 +5,9 @@ import {
linkWithCredential,
GithubAuthProvider,
} from 'firebase/auth';
import {Log} from '../../utils/logging.js';
import {bold, Log} from '../../utils/logging.js';
import {Prompt} from '../../utils/prompt.js';
import {hasTokenStoreFile} from './ng-dev-token.js';
import {
deviceCodeOAuthDance,
authorizationCodeOAuthDance,
Expand All @@ -17,6 +19,24 @@ export async function loginToFirebase() {
/** The type of OAuth dance to do based on whether a session display is available. */
const oAuthDance = process.env.DISPLAY ? authorizationCodeOAuthDance : deviceCodeOAuthDance;
try {
// Only present intial information about usage of login when it appears that the user has
// not logged into the service in the past.
if (!(await hasTokenStoreFile())) {
Log.warn(Array(80).fill('#').join(''));
Log.warn('The ng-dev auth service uses Google OAuth credentials to log in and create a');
Log.warn('short lived credential used for authenticating with the ng-dev service.');
Log.warn('');
Log.warn('In addition to logging in using Google credentials, upon first login you will be');
Log.warn('prompted to associate your Github account to your login, allowing the service to');
Log.warn('perform requests on your your behalf.');
Log.warn(Array(80).fill('#').join(''));
if (!(await Prompt.confirm('Continue to login?', true))) {
return false;
}
}

Log.log(`Please log in using the instructions below with your google.com credentials:`);

/** The id and access tokens for Google from the oauth login. */
const {idToken, accessToken} = await oAuthDance(GoogleOAuthDanceConfig);
/** The credential generated by the GoogleAuthProvider from the OAuth tokens. */
Expand All @@ -26,9 +46,15 @@ export async function loginToFirebase() {

// If the user already has a github account linked to their account, the login is complete.
if (user.providerData.find((provider) => provider.providerId === 'github.com')) {
Log.debug('Skipping Github linking as the users account is already linked.');
return true;
}

Log.log('');
Log.log(`There is no Github account currently linked to ${bold(user.email)} in the service,`);
Log.log('please login using the instructions below to link your Github account.');
Log.log('');

/** The access token for Github from the oauth login. */
const {accessToken: githubAccessToken} = await oAuthDance(GithubOAuthDanceConfig);

Expand Down
21 changes: 13 additions & 8 deletions ng-dev/auth/shared/ng-dev-token.ts
Expand Up @@ -107,16 +107,12 @@ async function saveTokenToFileSystem(data: NgDevUserWithToken) {

/** Retrieve the token from the file system. */
async function retrieveTokenFromFileSystem(): Promise<NgDevUserWithToken | null> {
try {
if (!(await stat(tokenPath))) {
return null;
}
} catch {
return null;
if (await hasTokenStoreFile()) {
const rawToken = Buffer.from(await readFile(tokenPath)).toString();
return JSON.parse(decrypt(rawToken)) as NgDevUserWithToken;
}

const rawToken = Buffer.from(await readFile(tokenPath)).toString();
return JSON.parse(decrypt(rawToken)) as NgDevUserWithToken;
return null;
}

/** Encrypt the provided string. */
Expand Down Expand Up @@ -183,6 +179,15 @@ export function configureAuthorizedGitClientWithTemporaryToken() {
});
}

/** Whether there is already a file at the location used for login credentials. */
export async function hasTokenStoreFile() {
try {
return !!(await stat(tokenPath));
} catch {
return false;
}
}

/** Assert the provied token is non-null. */
function assertLoggedIn(token: NgDevUserWithToken | null): asserts token is NgDevUserWithToken {
if (token == null) {
Expand Down
67 changes: 40 additions & 27 deletions ng-dev/auth/shared/oauth.ts
Expand Up @@ -15,6 +15,7 @@ import {
} from '@openid/appauth';
import {NodeRequestor} from '@openid/appauth/built/node_support/node_requestor.js';
import {NodeBasedHandler} from '@openid/appauth/built/node_support/node_request_handler.js';
import {Spinner} from '../../utils/spinner.js';

interface OAuthDanceConfig {
authConfig: AuthorizationServiceConfiguration;
Expand Down Expand Up @@ -121,8 +122,11 @@ export async function deviceCodeOAuthDance({
throw new OAuthDanceError(new AuthorizationError(response).errorDescription || 'Unknown Error');
}

Log.info(`Please visit: ${response.verification_uri || response.verification_url}`);
Log.info(`Enter your one time ID code: ${response.user_code}`);
Log.info(` Please visit: ${response.verification_uri || response.verification_url}`);
Log.info(` Enter your one time ID code: ${response.user_code}`);
Log.info('');

const pollingSpinner = new Spinner('Polling auth server for login confirmation');

/**
* The number of milliseconds to add to the requested internal from Google, utilized if Google requests
Expand All @@ -132,34 +136,43 @@ export async function deviceCodeOAuthDance({

const oauthDanceTimeout = Date.now() + response.expires_in * 1000;

while (true) {
if (Date.now() > oauthDanceTimeout) {
throw new OAuthDanceError(
'Failed to completed OAuth authentication before the user code expired.',
try {
while (true) {
if (Date.now() > oauthDanceTimeout) {
throw new OAuthDanceError(
'Failed to completed OAuth authentication before the user code expired.',
);
}
// Wait for the requested interval before polling, this is done before the request as it is unnecessary to
//immediately poll while the user has to perform the auth out of this flow.
await new Promise((resolve) =>
setTimeout(resolve, response.interval * 1000 + pollingBackoff),
);
}
// Wait for the requested interval before polling, this is done before the request as it is unnecessary to
//immediately poll while the user has to perform the auth out of this flow.
await new Promise((resolve) => setTimeout(resolve, response.interval * 1000 + pollingBackoff));

const result = await checkStatusOfAuthServer(
authConfig.tokenEndpoint,
response.device_code,
client_id,
client_secret,
);

if (!isAuthorizationError(result)) {
return new TokenResponse(result);
}
if (result.error === 'access_denied') {
throw new OAuthDanceError('Unable to authorize, as access was denied during the OAuth flow.');
}

if (result.error === 'slow_down') {
Log.debug('"slow_down" response from server, backing off polling interval by 5 seconds');
pollingBackoff += 5000;
const result = await checkStatusOfAuthServer(
authConfig.tokenEndpoint,
response.device_code,
client_id,
client_secret,
);

if (!isAuthorizationError(result)) {
return new TokenResponse(result);
}
if (result.error === 'access_denied') {
throw new OAuthDanceError(
'Unable to authorize, as access was denied during the OAuth flow.',
);
}

if (result.error === 'slow_down') {
Log.debug('"slow_down" response from server, backing off polling interval by 5 seconds');
pollingBackoff += 5000;
}
}
} finally {
pollingSpinner.update('');
pollingSpinner.complete();
}
}

Expand Down

0 comments on commit 88c198a

Please sign in to comment.