|
2 | 2 |
|
3 | 3 | 'use strict'; |
4 | 4 |
|
5 | | -import { GoogleOAuth } from './google/google-oauth.js'; |
6 | 5 | import { Ui } from '../../browser/ui.js'; |
| 6 | +import { AuthRes, OAuth, OAuthTokensResponse } from './generic/oauth.js'; |
| 7 | +import { AuthenticationConfiguration } from '../../authentication-configuration.js'; |
| 8 | +import { Url } from '../../core/common.js'; |
| 9 | +import { Assert, AssertError } from '../../assert.js'; |
| 10 | +import { Api } from '../shared/api.js'; |
| 11 | +import { Catch } from '../../platform/catch.js'; |
| 12 | +import { InMemoryStoreKeys } from '../../core/const.js'; |
| 13 | +import { InMemoryStore } from '../../platform/store/in-memory-store.js'; |
7 | 14 | import { AcctStore } from '../../platform/store/acct-store.js'; |
8 | | -import { OAuth } from './generic/oauth.js'; |
9 | | - |
10 | 15 | export class ConfiguredIdpOAuth extends OAuth { |
11 | | - public static newAuthPopupForEnterpriseServerAuthenticationIfNeeded = async (acctEmail: string) => { |
| 16 | + public static newAuthPopupForEnterpriseServerAuthenticationIfNeeded = async (authRes: AuthRes) => { |
| 17 | + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion |
| 18 | + const acctEmail = authRes.acctEmail!; |
12 | 19 | const storage = await AcctStore.get(acctEmail, ['authentication']); |
13 | | - if (storage?.authentication?.oauth?.clientId && storage.authentication.oauth.clientId !== GoogleOAuth.OAUTH.client_id) { |
14 | | - await Ui.modal.warning( |
15 | | - `Custom IdP is configured on this domain, but it is not supported on browser extension yet. |
16 | | - Authentication with Enterprise Server will continue using Google IdP until implemented in a future update.` |
17 | | - ); |
18 | | - } else { |
19 | | - return; |
| 20 | + if (storage?.authentication?.oauth?.clientId && storage.authentication.oauth.clientId !== this.GOOGLE_OAUTH_CONFIG.client_id) { |
| 21 | + await Ui.modal.info('Google login succeeded. Now, please log in with your company credentials as well.'); |
| 22 | + return await this.newAuthPopup(acctEmail, { oauth: storage.authentication.oauth }); |
20 | 23 | } |
| 24 | + return authRes; |
21 | 25 | }; |
| 26 | + |
| 27 | + public static async newAuthPopup(acctEmail: string, authConf: AuthenticationConfiguration): Promise<AuthRes> { |
| 28 | + acctEmail = acctEmail.toLowerCase(); |
| 29 | + const authRequest = this.newAuthRequest(acctEmail, this.OAUTH_REQUEST_SCOPES); |
| 30 | + const authUrl = this.apiOAuthCodeUrl(authConf, authRequest.expectedState, acctEmail); |
| 31 | + const authRes = await this.getAuthRes({ |
| 32 | + acctEmail, |
| 33 | + expectedState: authRequest.expectedState, |
| 34 | + authUrl, |
| 35 | + authConf, |
| 36 | + }); |
| 37 | + if (authRes.result === 'Success') { |
| 38 | + if (!authRes.id_token) { |
| 39 | + return { |
| 40 | + result: 'Error', |
| 41 | + error: 'Grant was successful but missing id_token', |
| 42 | + acctEmail, |
| 43 | + id_token: undefined, // eslint-disable-line @typescript-eslint/naming-convention |
| 44 | + }; |
| 45 | + } |
| 46 | + if (!authRes.acctEmail) { |
| 47 | + return { |
| 48 | + result: 'Error', |
| 49 | + error: 'Grant was successful but missing acctEmail', |
| 50 | + acctEmail: authRes.acctEmail, |
| 51 | + id_token: undefined, // eslint-disable-line @typescript-eslint/naming-convention |
| 52 | + }; |
| 53 | + } |
| 54 | + } |
| 55 | + return authRes; |
| 56 | + } |
| 57 | + |
| 58 | + private static apiOAuthCodeUrl(authConf: AuthenticationConfiguration, state: string, acctEmail: string) { |
| 59 | + /* eslint-disable @typescript-eslint/naming-convention */ |
| 60 | + return Url.create(authConf.oauth.authCodeUrl, { |
| 61 | + client_id: authConf.oauth.clientId, |
| 62 | + response_type: 'code', |
| 63 | + access_type: 'offline', |
| 64 | + prompt: 'login', |
| 65 | + state, |
| 66 | + redirect_uri: chrome.identity.getRedirectURL('oauth'), |
| 67 | + scope: this.OAUTH_REQUEST_SCOPES.join(' '), |
| 68 | + login_hint: acctEmail, |
| 69 | + }); |
| 70 | + /* eslint-enable @typescript-eslint/naming-convention */ |
| 71 | + } |
| 72 | + |
| 73 | + private static async getAuthRes({ |
| 74 | + acctEmail, |
| 75 | + expectedState, |
| 76 | + authUrl, |
| 77 | + authConf, |
| 78 | + }: { |
| 79 | + acctEmail: string; |
| 80 | + expectedState: string; |
| 81 | + authUrl: string; |
| 82 | + authConf: AuthenticationConfiguration; |
| 83 | + }): Promise<AuthRes> { |
| 84 | + /* eslint-disable @typescript-eslint/naming-convention */ |
| 85 | + try { |
| 86 | + const redirectUri = await chrome.identity.launchWebAuthFlow({ url: authUrl, interactive: true }); |
| 87 | + if (chrome.runtime.lastError || !redirectUri || redirectUri?.includes('access_denied')) { |
| 88 | + return { acctEmail, result: 'Denied', error: `Failed to launch web auth flow`, id_token: undefined }; |
| 89 | + } |
| 90 | + |
| 91 | + if (!redirectUri) { |
| 92 | + return { acctEmail, result: 'Denied', error: 'Invalid response url', id_token: undefined }; |
| 93 | + } |
| 94 | + const uncheckedUrlParams = Url.parse(['scope', 'code', 'state'], redirectUri); |
| 95 | + const code = Assert.urlParamRequire.optionalString(uncheckedUrlParams, 'code'); |
| 96 | + const receivedState = Assert.urlParamRequire.string(uncheckedUrlParams, 'state'); |
| 97 | + if (!code) { |
| 98 | + return { |
| 99 | + acctEmail, |
| 100 | + result: 'Denied', |
| 101 | + error: "OAuth result was 'Success' but no auth code", |
| 102 | + id_token: undefined, |
| 103 | + }; |
| 104 | + } |
| 105 | + if (receivedState !== expectedState) { |
| 106 | + return { acctEmail, result: 'Error', error: `Wrong oauth CSRF token. Please try again.`, id_token: undefined }; |
| 107 | + } |
| 108 | + const { id_token } = await this.authGetTokens(code, authConf); |
| 109 | + const { email } = this.parseIdToken(id_token); |
| 110 | + if (!email) { |
| 111 | + throw new Error('Missing email address in id_token'); |
| 112 | + } |
| 113 | + if (acctEmail !== email) { |
| 114 | + return { |
| 115 | + acctEmail, |
| 116 | + result: 'Error', |
| 117 | + error: `Google account email and custom IDP email do not match. Please use the same email address..`, |
| 118 | + id_token: undefined, |
| 119 | + }; |
| 120 | + } |
| 121 | + await InMemoryStore.set(acctEmail, InMemoryStoreKeys.CUSTOM_IDP_ID_TOKEN, id_token); |
| 122 | + return { acctEmail: email, result: 'Success', id_token }; |
| 123 | + } catch (err) { |
| 124 | + return { acctEmail, result: 'Error', error: err instanceof AssertError ? 'Could not parse URL returned from OAuth' : String(err), id_token: undefined }; |
| 125 | + } |
| 126 | + /* eslint-enable @typescript-eslint/naming-convention */ |
| 127 | + } |
| 128 | + |
| 129 | + private static async authGetTokens(code: string, authConf: AuthenticationConfiguration): Promise<OAuthTokensResponse> { |
| 130 | + return await Api.ajax( |
| 131 | + { |
| 132 | + /* eslint-disable @typescript-eslint/naming-convention */ |
| 133 | + url: authConf.oauth.tokensUrl, |
| 134 | + method: 'POST', |
| 135 | + data: { |
| 136 | + grant_type: 'authorization_code', |
| 137 | + code, |
| 138 | + client_id: authConf.oauth.clientId, |
| 139 | + redirect_uri: chrome.identity.getRedirectURL('oauth'), |
| 140 | + }, |
| 141 | + dataType: 'JSON', |
| 142 | + /* eslint-enable @typescript-eslint/naming-convention */ |
| 143 | + stack: Catch.stackTrace(), |
| 144 | + }, |
| 145 | + 'json' |
| 146 | + ); |
| 147 | + } |
22 | 148 | } |
0 commit comments