diff --git a/ng-dev/auth/shared/firebase.ts b/ng-dev/auth/shared/firebase.ts index f38c7799c..cb7507e98 100644 --- a/ng-dev/auth/shared/firebase.ts +++ b/ng-dev/auth/shared/firebase.ts @@ -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, @@ -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. */ @@ -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); diff --git a/ng-dev/auth/shared/ng-dev-token.ts b/ng-dev/auth/shared/ng-dev-token.ts index 442b17516..35511ae0e 100644 --- a/ng-dev/auth/shared/ng-dev-token.ts +++ b/ng-dev/auth/shared/ng-dev-token.ts @@ -107,16 +107,12 @@ async function saveTokenToFileSystem(data: NgDevUserWithToken) { /** Retrieve the token from the file system. */ async function retrieveTokenFromFileSystem(): Promise { - 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. */ @@ -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) { diff --git a/ng-dev/auth/shared/oauth.ts b/ng-dev/auth/shared/oauth.ts index 2670039de..010d817b1 100644 --- a/ng-dev/auth/shared/oauth.ts +++ b/ng-dev/auth/shared/oauth.ts @@ -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; @@ -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 @@ -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(); } }