From 51e3197d76e698917161e9be82aa28a480964f19 Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Mon, 19 Aug 2019 17:33:04 -0400 Subject: [PATCH 1/2] Add support for code flow silent-refresh and popup --- docs-src/silent-refresh.md | 2 +- projects/lib/src/oauth-service.ts | 84 ++++++++++++++++++------------- projects/lib/src/types.ts | 19 +++++-- 3 files changed, 66 insertions(+), 39 deletions(-) diff --git a/docs-src/silent-refresh.md b/docs-src/silent-refresh.md index e91f946e..4e5da324 100644 --- a/docs-src/silent-refresh.md +++ b/docs-src/silent-refresh.md @@ -66,7 +66,7 @@ This file is loaded into the hidden iframe after getting new tokens. Its only ta diff --git a/projects/lib/src/oauth-service.ts b/projects/lib/src/oauth-service.ts index 2b9de9dc..e7ea3179 100644 --- a/projects/lib/src/oauth-service.ts +++ b/projects/lib/src/oauth-service.ts @@ -827,14 +827,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { this.tryLogin({ customHashFragment: message, preventClearHashAfterLogin: true, - onLoginError: err => { - this.eventsSubject.next( - new OAuthErrorEvent('silent_refresh_error', err) - ); - }, - onTokenReceived: () => { - this.eventsSubject.next(new OAuthSuccessEvent('silently_refreshed')); - } + customRedirectUri: this.silentRefreshRedirectUri || this.redirectUri }).catch(err => this.debug('tryLogin during silent refresh failed', err)); }; @@ -896,7 +889,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { first() ); const success = this.events.pipe( - filter(e => e.type === 'silently_refreshed'), + filter(e => e.type === 'token_received'), first() ); const timeout = of( @@ -905,14 +898,18 @@ export class OAuthService extends AuthConfig implements OnDestroy { return race([errors, success, timeout]) .pipe( - tap(e => { - if (e.type === 'silent_refresh_timeout') { - this.eventsSubject.next(e); - } - }), map(e => { if (e instanceof OAuthErrorEvent) { + if (e.type === 'silent_refresh_timeout') { + this.eventsSubject.next(e); + } else { + e = new OAuthErrorEvent('silent_refresh_error', e); + this.eventsSubject.next(e); + } throw e; + } else if (e.type === 'token_received') { + e = new OAuthSuccessEvent('silently_refreshed'); + this.eventsSubject.next(e); } return e; }) @@ -920,7 +917,16 @@ export class OAuthService extends AuthConfig implements OnDestroy { .toPromise(); } + /** + * This method exists for backwards compatibility. + * {@link OAuthService#initLoginFlowInPopup} handles both code + * and implicit flows. + */ public initImplicitFlowInPopup(options?: { height?: number, width?: number }) { + return this.initLoginFlowInPopup(options); + } + + public initLoginFlowInPopup(options?: { height?: number, width?: number }) { options = options || {}; return this.createLoginUrl(null, null, this.silentRefreshRedirectUri, false, { display: 'popup' @@ -940,6 +946,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { this.tryLogin({ customHashFragment: message, preventClearHashAfterLogin: true, + customRedirectUri: this.silentRefreshRedirectUri }).then(() => { cleanup(); resolve(); @@ -1264,7 +1271,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { } return url; - + } initImplicitFlowInternal( @@ -1373,7 +1380,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { */ public tryLogin(options: LoginOptions = null): Promise { if (this.config.responseType === 'code') { - return this.tryLoginCodeFlow().then(_ => true); + return this.tryLoginCodeFlow(options).then(_ => true); } else { return this.tryLoginImplicitFlow(options); @@ -1395,20 +1402,27 @@ export class OAuthService extends AuthConfig implements OnDestroy { } - public tryLoginCodeFlow(): Promise { + public tryLoginCodeFlow(options: LoginOptions = null): Promise { + options = options || {}; - const parts = this.parseQueryString(window.location.search) + const querySource = options.customHashFragment ? + options.customHashFragment.substring(1) : + window.location.search; + + const parts = this.parseQueryString(querySource) const code = parts['code']; const state = parts['state']; - const href = location.href - .replace(/[&\?]code=[^&\$]*/, '') - .replace(/[&\?]scope=[^&\$]*/, '') - .replace(/[&\?]state=[^&\$]*/, '') - .replace(/[&\?]session_state=[^&\$]*/, ''); + if (!options.preventClearHashAfterLogin) { + const href = location.href + .replace(/[&\?]code=[^&\$]*/, '') + .replace(/[&\?]scope=[^&\$]*/, '') + .replace(/[&\?]state=[^&\$]*/, '') + .replace(/[&\?]session_state=[^&\$]*/, ''); - history.replaceState(null, window.name, href); + history.replaceState(null, window.name, href); + } let [nonceInState, userState] = this.parseState(state); this.state = userState; @@ -1434,7 +1448,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { if (code) { return new Promise((resolve, reject) => { - this.getTokenFromCode(code).then(result => { + this.getTokenFromCode(code, options).then(result => { resolve(); }).catch(err => { reject(err); @@ -1448,11 +1462,11 @@ export class OAuthService extends AuthConfig implements OnDestroy { /** * Get token using an intermediate code. Works for the Authorization Code flow. */ - private getTokenFromCode(code: string): Promise { + private getTokenFromCode(code: string, options: LoginOptions): Promise { let params = new HttpParams() .set('grant_type', 'authorization_code') .set('code', code) - .set('redirect_uri', this.redirectUri); + .set('redirect_uri', options.customRedirectUri || this.redirectUri); if (!this.disablePKCE) { const pkciVerifier = this._storage.getItem('PKCI_verifier'); @@ -1503,32 +1517,32 @@ export class OAuthService extends AuthConfig implements OnDestroy { (tokenResponse) => { this.debug('refresh tokenResponse', tokenResponse); this.storeAccessTokenResponse( - tokenResponse.access_token, - tokenResponse.refresh_token, + tokenResponse.access_token, + tokenResponse.refresh_token, tokenResponse.expires_in, tokenResponse.scope); if (this.oidc && tokenResponse.id_token) { - this.processIdToken(tokenResponse.id_token, tokenResponse.access_token). + this.processIdToken(tokenResponse.id_token, tokenResponse.access_token). then(result => { this.storeIdToken(result); - + this.eventsSubject.next(new OAuthSuccessEvent('token_received')); this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); - + resolve(tokenResponse); }) .catch(reason => { this.eventsSubject.next(new OAuthErrorEvent('token_validation_error', reason)); console.error('Error validating tokens'); console.error(reason); - + reject(reason); }); } else { this.eventsSubject.next(new OAuthSuccessEvent('token_received')); this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); - + resolve(tokenResponse); } }, @@ -1688,7 +1702,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { ): boolean { const savedNonce = this._storage.getItem('nonce'); if (savedNonce !== nonceInState) { - + const err = 'Validating access_token failed, wrong state/nonce.'; console.error(err, savedNonce, nonceInState); return false; diff --git a/projects/lib/src/types.ts b/projects/lib/src/types.ts index 802bd649..9950a678 100644 --- a/projects/lib/src/types.ts +++ b/projects/lib/src/types.ts @@ -1,5 +1,5 @@ /** - * Additional options that can be passt to tryLogin. + * Additional options that can be passed to tryLogin. */ export class LoginOptions { /** @@ -28,7 +28,12 @@ export class LoginOptions { /** * A custom hash fragment to be used instead of the * actual one. This is used for silent refreshes, to - * pass the iframes hash fragment to this method. + * pass the iframes hash fragment to this method, and + * is also used by popup flows in the same manner. + * This can be used with code flow, where is must be set + * to a hash symbol followed by the querystring. The + * question mark is optional, but may be present following + * the hash symbol. */ customHashFragment?: string; @@ -45,9 +50,17 @@ export class LoginOptions { /** * Normally, you want to clear your hash fragment after * the lib read the token(s) so that they are not displayed - * anymore in the url. If not, set this to true. + * anymore in the url. If not, set this to true. For code flow + * this controls removing query string values. */ preventClearHashAfterLogin? = false; + + /** + * Set this for code flow if you used a custom redirect Uri + * when retrieving the code. This is used internally for silent + * refresh and popup flows. + */ + customRedirectUri?: string; } /** From e5e1f7d02172c53109a30c0a17e2f10713d996be Mon Sep 17 00:00:00 2001 From: Kevin Cathcart Date: Thu, 12 Sep 2019 09:42:36 -0400 Subject: [PATCH 2/2] Allow automatic refresh to use iframe for code flow --- projects/lib/src/oauth-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/lib/src/oauth-service.ts b/projects/lib/src/oauth-service.ts index e7ea3179..fefdc0fc 100644 --- a/projects/lib/src/oauth-service.ts +++ b/projects/lib/src/oauth-service.ts @@ -195,7 +195,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { } protected refreshInternal(params, noPrompt) { - if (this.responseType === 'code') { + if (!this.silentRefreshRedirectUri && this.responseType === 'code') { return this.refreshToken(); } else { return this.silentRefresh(params, noPrompt);