33'use strict' ;
44
55import { Ui } from '../../browser/ui.js' ;
6- import { AuthRes , OAuth , OAuthTokensResponse } from './generic/oauth.js' ;
6+ import { AuthorizationHeader , AuthRes , OAuth , OAuthTokensResponse } from './generic/oauth.js' ;
77import { AuthenticationConfiguration } from '../../authentication-configuration.js' ;
88import { Url } from '../../core/common.js' ;
99import { Assert , AssertError } from '../../assert.js' ;
1010import { Api } from '../shared/api.js' ;
1111import { Catch } from '../../platform/catch.js' ;
1212import { InMemoryStoreKeys } from '../../core/const.js' ;
1313import { InMemoryStore } from '../../platform/store/in-memory-store.js' ;
14- import { AcctStore } from '../../platform/store/acct-store.js' ;
15- import { BackendAuthErr } from '../shared/api-error.js' ;
14+ import { AcctStore , AcctStoreDict } from '../../platform/store/acct-store.js' ;
15+ import { EnterpriseServerAuthErr } from '../shared/api-error.js' ;
1616export class ConfiguredIdpOAuth extends OAuth {
1717 public static newAuthPopupForEnterpriseServerAuthenticationIfNeeded = async ( authRes : AuthRes ) => {
1818 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1919 const acctEmail = authRes . acctEmail ! ;
2020 const storage = await AcctStore . get ( acctEmail , [ 'authentication' ] ) ;
2121 if ( storage ?. authentication ?. oauth ?. clientId && storage . authentication . oauth . clientId !== this . GOOGLE_OAUTH_CONFIG . client_id ) {
2222 await Ui . modal . info ( 'Google login succeeded. Now, please log in with your company credentials as well.' ) ;
23- return await this . newAuthPopup ( acctEmail , { oauth : storage . authentication . oauth } ) ;
23+ return await this . newAuthPopup ( acctEmail ) ;
2424 }
2525 return authRes ;
2626 } ;
2727
28- public static authHdr = async ( acctEmail : string , shouldThrowErrorForEmptyIdToken = true ) : Promise < { authorization : string } | undefined > => {
29- let idToken = await InMemoryStore . getUntilAvailable ( acctEmail , InMemoryStoreKeys . ID_TOKEN ) ;
30- if ( idToken ) {
31- const customIDPIdToken = await InMemoryStore . get ( acctEmail , InMemoryStoreKeys . CUSTOM_IDP_ID_TOKEN ) ;
32- // if special JWT is stored in local store, it should be used for Enterprise Server authentication instead of Google JWT
33- // https://github.com/FlowCrypt/flowcrypt-browser/issues/5799
34- if ( customIDPIdToken ) {
35- idToken = customIDPIdToken ;
28+ public static authHdr = async ( acctEmail : string , shouldThrowErrorForEmptyIdToken = true , forceRefresh = false ) : Promise < AuthorizationHeader | undefined > => {
29+ const { custom_idp_token_refresh } = await AcctStore . get ( acctEmail , [ 'custom_idp_token_refresh' ] ) ; // eslint-disable-line @typescript-eslint/naming-convention
30+ if ( ! forceRefresh ) {
31+ const authHdr = await this . getAuthHeaderDependsOnType ( acctEmail ) ;
32+ if ( authHdr ) {
33+ return authHdr ;
34+ }
35+ }
36+ if ( ! custom_idp_token_refresh ) {
37+ if ( shouldThrowErrorForEmptyIdToken ) {
38+ throw new EnterpriseServerAuthErr ( `Account ${ acctEmail } not connected to FlowCrypt Browser Extension` ) ;
39+ }
40+ return undefined ;
41+ }
42+ // refresh token
43+ const refreshTokenRes = await this . authRefreshToken ( custom_idp_token_refresh , acctEmail ) ;
44+ if ( refreshTokenRes . access_token ) {
45+ await this . authSaveTokens ( acctEmail , refreshTokenRes ) ;
46+ const authHdr = await this . getAuthHeaderDependsOnType ( acctEmail ) ;
47+ if ( authHdr ) {
48+ return authHdr ;
3649 }
37- return { authorization : `Bearer ${ idToken } ` } ;
3850 }
3951 if ( shouldThrowErrorForEmptyIdToken ) {
4052 // user will not actually see this message, they'll see a generic login prompt
41- throw new BackendAuthErr ( 'Missing id token, please re-authenticate' ) ;
53+ throw new EnterpriseServerAuthErr (
54+ `Could not refresh custom idp auth token - did not become valid (access:${ refreshTokenRes . id_token } ,expires_in:${
55+ refreshTokenRes . expires_in
56+ } ,now:${ Date . now ( ) } )`
57+ ) ;
4258 }
4359 return undefined ;
4460 } ;
4561
46- public static async newAuthPopup ( acctEmail : string , authConf : AuthenticationConfiguration ) : Promise < AuthRes > {
62+ public static async newAuthPopup ( acctEmail : string ) : Promise < AuthRes > {
4763 acctEmail = acctEmail . toLowerCase ( ) ;
4864 const authRequest = this . newAuthRequest ( acctEmail , this . OAUTH_REQUEST_SCOPES ) ;
49- const authUrl = this . apiOAuthCodeUrl ( authConf , authRequest . expectedState , acctEmail ) ;
65+ const authUrl = await this . apiOAuthCodeUrl ( authRequest . expectedState , acctEmail ) ;
5066 const authRes = await this . getAuthRes ( {
5167 acctEmail,
5268 expectedState : authRequest . expectedState ,
5369 authUrl,
54- authConf,
5570 } ) ;
5671 if ( authRes . result === 'Success' ) {
5772 if ( ! authRes . id_token ) {
@@ -74,7 +89,29 @@ export class ConfiguredIdpOAuth extends OAuth {
7489 return authRes ;
7590 }
7691
77- private static apiOAuthCodeUrl ( authConf : AuthenticationConfiguration , state : string , acctEmail : string ) {
92+ private static async authRefreshToken ( refreshToken : string , acctEmail : string ) : Promise < OAuthTokensResponse > {
93+ const authConf = await this . getAuthenticationConfiguration ( acctEmail ) ;
94+ return await Api . ajax (
95+ {
96+ /* eslint-disable @typescript-eslint/naming-convention */
97+ url : authConf . oauth . tokensUrl ,
98+ method : 'POST' ,
99+ data : {
100+ grant_type : 'refresh_token' ,
101+ refreshToken,
102+ client_id : authConf . oauth . clientId ,
103+ redirect_uri : chrome . identity . getRedirectURL ( 'oauth' ) ,
104+ } ,
105+ dataType : 'JSON' ,
106+ /* eslint-enable @typescript-eslint/naming-convention */
107+ stack : Catch . stackTrace ( ) ,
108+ } ,
109+ 'json'
110+ ) ;
111+ }
112+
113+ private static async apiOAuthCodeUrl ( state : string , acctEmail : string ) {
114+ const authConf = await this . getAuthenticationConfiguration ( acctEmail ) ;
78115 /* eslint-disable @typescript-eslint/naming-convention */
79116 return Url . create ( authConf . oauth . authCodeUrl , {
80117 client_id : authConf . oauth . clientId ,
@@ -89,17 +126,7 @@ export class ConfiguredIdpOAuth extends OAuth {
89126 /* eslint-enable @typescript-eslint/naming-convention */
90127 }
91128
92- private static async getAuthRes ( {
93- acctEmail,
94- expectedState,
95- authUrl,
96- authConf,
97- } : {
98- acctEmail : string ;
99- expectedState : string ;
100- authUrl : string ;
101- authConf : AuthenticationConfiguration ;
102- } ) : Promise < AuthRes > {
129+ private static async getAuthRes ( { acctEmail, expectedState, authUrl } : { acctEmail : string ; expectedState : string ; authUrl : string } ) : Promise < AuthRes > {
103130 /* eslint-disable @typescript-eslint/naming-convention */
104131 try {
105132 const redirectUri = await chrome . identity . launchWebAuthFlow ( { url : authUrl , interactive : true } ) ;
@@ -124,7 +151,7 @@ export class ConfiguredIdpOAuth extends OAuth {
124151 if ( receivedState !== expectedState ) {
125152 return { acctEmail, result : 'Error' , error : `Wrong oauth CSRF token. Please try again.` , id_token : undefined } ;
126153 }
127- const { id_token } = await this . authGetTokens ( code , authConf ) ;
154+ const { id_token } = await this . retrieveAndSaveAuthToken ( acctEmail , code ) ;
128155 const { email } = this . parseIdToken ( id_token ) ;
129156 if ( ! email ) {
130157 throw new Error ( 'Missing email address in id_token' ) ;
@@ -137,15 +164,36 @@ export class ConfiguredIdpOAuth extends OAuth {
137164 id_token : undefined ,
138165 } ;
139166 }
140- await InMemoryStore . set ( acctEmail , InMemoryStoreKeys . CUSTOM_IDP_ID_TOKEN , id_token ) ;
141167 return { acctEmail : email , result : 'Success' , id_token } ;
142168 } catch ( err ) {
143169 return { acctEmail, result : 'Error' , error : err instanceof AssertError ? 'Could not parse URL returned from OAuth' : String ( err ) , id_token : undefined } ;
144170 }
145171 /* eslint-enable @typescript-eslint/naming-convention */
146172 }
147173
148- private static async authGetTokens ( code : string , authConf : AuthenticationConfiguration ) : Promise < OAuthTokensResponse > {
174+ // eslint-disable-next-line @typescript-eslint/naming-convention
175+ private static async retrieveAndSaveAuthToken ( acctEmail : string , authCode : string ) : Promise < { id_token : string } > {
176+ const tokensObj = await this . authGetTokens ( acctEmail , authCode ) ;
177+ const claims = this . parseIdToken ( tokensObj . id_token ) ;
178+ if ( ! claims . email ) {
179+ throw new Error ( 'Missing email address in id_token' ) ;
180+ }
181+ await this . authSaveTokens ( claims . email , tokensObj ) ;
182+ return { id_token : tokensObj . id_token } ; // eslint-disable-line @typescript-eslint/naming-convention
183+ }
184+
185+ private static async authSaveTokens ( acctEmail : string , tokensObj : OAuthTokensResponse ) {
186+ const tokenExpires = new Date ( ) . getTime ( ) + ( tokensObj . expires_in - 120 ) * 1000 ; // let our copy expire 2 minutes beforehand
187+ const toSave : AcctStoreDict = { } ;
188+ if ( typeof tokensObj . refresh_token !== 'undefined' ) {
189+ toSave . custom_idp_token_refresh = tokensObj . refresh_token ;
190+ }
191+ await AcctStore . set ( acctEmail , toSave ) ;
192+ await InMemoryStore . set ( acctEmail , InMemoryStoreKeys . CUSTOM_IDP_ID_TOKEN , tokensObj . id_token , tokenExpires ) ;
193+ }
194+
195+ private static async authGetTokens ( acctEmail : string , code : string ) : Promise < OAuthTokensResponse > {
196+ const authConf = await this . getAuthenticationConfiguration ( acctEmail ) ;
149197 return await Api . ajax (
150198 {
151199 /* eslint-disable @typescript-eslint/naming-convention */
@@ -164,4 +212,25 @@ export class ConfiguredIdpOAuth extends OAuth {
164212 'json'
165213 ) ;
166214 }
215+
216+ private static async getAuthenticationConfiguration ( acctEmail : string ) : Promise < AuthenticationConfiguration > {
217+ const storage = await AcctStore . get ( acctEmail , [ 'authentication' ] ) ;
218+ if ( ! storage . authentication ) {
219+ throw new EnterpriseServerAuthErr ( 'Could not get authentication configuration' ) ;
220+ }
221+ return storage . authentication ;
222+ }
223+
224+ private static async getAuthHeaderDependsOnType ( acctEmail : string ) : Promise < AuthorizationHeader | undefined > {
225+ let idToken = await InMemoryStore . getUntilAvailable ( acctEmail , InMemoryStoreKeys . ID_TOKEN ) ;
226+ const storage = await AcctStore . get ( acctEmail , [ 'authentication' ] ) ;
227+ if ( storage . authentication ?. oauth ) {
228+ // If custom authentication (IDP) is used, return the custom IDP ID token if available.
229+ // If the custom IDP ID token is not found, throw an EnterpriseServerAuthErr.
230+ // The custom IDP ID token should be used for Enterprise Server authentication instead of the Google JWT.
231+ // https://github.com/FlowCrypt/flowcrypt-browser/issues/5799
232+ idToken = await InMemoryStore . get ( acctEmail , InMemoryStoreKeys . CUSTOM_IDP_ID_TOKEN ) ;
233+ }
234+ return idToken ? { authorization : `Bearer ${ idToken } ` } : undefined ;
235+ }
167236}
0 commit comments