Skip to content

feat(ng-dev): add logging and messaging to ng-dev auth login #798

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion ng-dev/auth/shared/firebase.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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