Skip to content

Commit 0ead411

Browse files
author
Ioan Moldovan
authored
#5444 ConfiguredIDPOAuth shows authentication popup and save JWT to storage (#5795)
* WIP: add custom idp implementation * WIP: added ui test * fix: ui test * fix: refactor * fix: typo * fix: test client id * fix: pr reviews * fix: pr reviews * feat: use chrome.identity.launchWebAuthFlow and updated UI test
1 parent 8a22205 commit 0ead411

File tree

16 files changed

+328
-133
lines changed

16 files changed

+328
-133
lines changed

extension/js/common/api/authentication/configured-idp-oauth.ts

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,147 @@
22

33
'use strict';
44

5-
import { GoogleOAuth } from './google/google-oauth.js';
65
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';
714
import { AcctStore } from '../../platform/store/acct-store.js';
8-
import { OAuth } from './generic/oauth.js';
9-
1015
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!;
1219
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 });
2023
}
24+
return authRes;
2125
};
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+
}
22148
}

extension/js/common/api/authentication/generic/oauth.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,59 @@
22

33
'use strict';
44

5+
import { GoogleAuthWindowResult$result } from '../../../browser/browser-msg.js';
56
import { Buf } from '../../../core/buf.js';
67
import { Str } from '../../../core/common.js';
8+
import { GOOGLE_OAUTH_SCREEN_HOST, OAUTH_GOOGLE_API_HOST } from '../../../core/const.js';
79
import { GmailRes } from '../../email-provider/gmail/gmail-parser.js';
10+
import { Api } from '../../shared/api.js';
11+
12+
export type AuthReq = { acctEmail?: string; scopes: string[]; messageId?: string; expectedState: string };
13+
// eslint-disable-next-line @typescript-eslint/naming-convention
14+
type AuthResultSuccess = { result: 'Success'; acctEmail: string; id_token: string; error?: undefined };
15+
type AuthResultError = {
16+
result: GoogleAuthWindowResult$result;
17+
acctEmail?: string;
18+
error?: string;
19+
// eslint-disable-next-line @typescript-eslint/naming-convention
20+
id_token: undefined;
21+
};
22+
export type AuthRes = AuthResultSuccess | AuthResultError;
23+
24+
/* eslint-disable @typescript-eslint/naming-convention */
25+
export type OAuthTokensResponse = {
26+
access_token: string;
27+
expires_in: number;
28+
refresh_token?: string;
29+
id_token: string;
30+
token_type: 'Bearer';
31+
};
32+
/* eslint-enable @typescript-eslint/naming-convention */
833

934
export class OAuth {
35+
/* eslint-disable @typescript-eslint/naming-convention */
36+
public static GOOGLE_OAUTH_CONFIG = {
37+
client_id: '717284730244-5oejn54f10gnrektjdc4fv4rbic1bj1p.apps.googleusercontent.com',
38+
client_secret: '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',
39+
redirect_uri: 'https://www.google.com/robots.txt',
40+
url_code: `${GOOGLE_OAUTH_SCREEN_HOST}/o/oauth2/auth`,
41+
url_tokens: `${OAUTH_GOOGLE_API_HOST}/token`,
42+
state_header: 'CRYPTUP_STATE_',
43+
scopes: {
44+
email: 'email',
45+
openid: 'openid',
46+
profile: 'https://www.googleapis.com/auth/userinfo.profile', // needed so that `name` is present in `id_token`, which is required for key-server auth when in use
47+
compose: 'https://www.googleapis.com/auth/gmail.compose',
48+
modify: 'https://www.googleapis.com/auth/gmail.modify',
49+
readContacts: 'https://www.googleapis.com/auth/contacts.readonly',
50+
readOtherContacts: 'https://www.googleapis.com/auth/contacts.other.readonly',
51+
},
52+
legacy_scopes: {
53+
gmail: 'https://mail.google.com/', // causes a freakish oauth warn: "can permannently delete all your email" ...
54+
},
55+
};
56+
public static OAUTH_REQUEST_SCOPES = ['offline_access', 'openid', 'profile', 'email'];
57+
/* eslint-enable @typescript-eslint/naming-convention */
1058
/**
1159
* Happens on enterprise builds
1260
*/
@@ -32,4 +80,16 @@ export class OAuth {
3280
}
3381
return claims;
3482
};
83+
84+
public static newAuthRequest(acctEmail: string | undefined, scopes: string[]): AuthReq {
85+
const authReq = {
86+
acctEmail,
87+
scopes,
88+
csrfToken: `csrf-${Api.randomFortyHexChars()}`,
89+
};
90+
return {
91+
...authReq,
92+
expectedState: `CRYPTUP_STATE_${JSON.stringify(authReq)}`,
93+
};
94+
}
3595
}

0 commit comments

Comments
 (0)