diff --git a/docs-src/implicit-flow.md b/docs-src/implicit-flow.md index 405be16b..53b540be 100644 --- a/docs-src/implicit-flow.md +++ b/docs-src/implicit-flow.md @@ -154,7 +154,7 @@ This file is loaded into the hidden iframe after getting new tokens. Its only ta @@ -190,4 +190,4 @@ To automatically refresh a token when/ some time before it expires, just call th this.oauthService.setupAutomaticSilentRefresh(); ``` -By default, this event is fired after 75% of the token's life time is over. You can adjust this factor by setting the property ``timeoutFactor`` to a value between 0 and 1. For instance, 0.5 means, that the event is fired after half of the life time is over and 0.33 triggers the event after a third. \ No newline at end of file +By default, this event is fired after 75% of the token's life time is over. You can adjust this factor by setting the property ``timeoutFactor`` to a value between 0 and 1. For instance, 0.5 means, that the event is fired after half of the life time is over and 0.33 triggers the event after a third. diff --git a/package-lock.json b/package-lock.json index c11dd751..d5179308 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6462,7 +6462,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -6483,12 +6484,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6503,17 +6506,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -6630,7 +6636,8 @@ "inherits": { "version": "2.0.4", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -6642,6 +6649,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6656,6 +6664,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6663,12 +6672,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.9.0", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -6687,6 +6698,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -6776,7 +6788,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -6788,6 +6801,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -6873,7 +6887,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -6909,6 +6924,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6928,6 +6944,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6971,12 +6988,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, diff --git a/projects/lib/src/auth.config.ts b/projects/lib/src/auth.config.ts index f30b0f09..20dbe388 100644 --- a/projects/lib/src/auth.config.ts +++ b/projects/lib/src/auth.config.ts @@ -64,6 +64,11 @@ export class AuthConfig { */ public tokenEndpoint?: string = null; + /** + * Names of known parameters sent out in the TokenResponse. https://tools.ietf.org/html/rfc6749#section-5.1 + */ + public customTokenParameters?: string[] = []; + /** * Url of the userinfo endpoint as defined by OpenId Connect. */ diff --git a/projects/lib/src/oauth-service.ts b/projects/lib/src/oauth-service.ts index 7f0a204d..b985c2d9 100644 --- a/projects/lib/src/oauth-service.ts +++ b/projects/lib/src/oauth-service.ts @@ -1,7 +1,8 @@ -import { Injectable, NgZone, Optional, OnDestroy } from '@angular/core'; +import { Injectable, NgZone, Optional, OnDestroy, Inject } from '@angular/core'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Observable, Subject, Subscription, of, race, from } from 'rxjs'; import { filter, delay, first, tap, map, switchMap, debounceTime } from 'rxjs/operators'; +import { DOCUMENT } from '@angular/common'; import { ValidationHandler, @@ -91,6 +92,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { protected urlHelper: UrlHelperService, protected logger: OAuthLogger, @Optional() protected crypto: HashHandler, + @Inject(DOCUMENT) private document: Document, ) { super(); @@ -733,7 +735,8 @@ export class OAuthService extends AuthConfig implements OnDestroy { tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, - tokenResponse.scope + tokenResponse.scope, + this.extractRecognizedCustomParameters(tokenResponse) ); this.eventsSubject.next(new OAuthSuccessEvent('token_received')); @@ -810,7 +813,8 @@ export class OAuthService extends AuthConfig implements OnDestroy { tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, - tokenResponse.scope + tokenResponse.scope, + this.extractRecognizedCustomParameters(tokenResponse) ); this.eventsSubject.next(new OAuthSuccessEvent('token_received')); @@ -1400,7 +1404,8 @@ export class OAuthService extends AuthConfig implements OnDestroy { accessToken: string, refreshToken: string, expiresIn: number, - grantedScopes: String + grantedScopes: String, + customParameters?: Map ): void { this._storage.setItem('access_token', accessToken); if (grantedScopes) { @@ -1417,6 +1422,11 @@ export class OAuthService extends AuthConfig implements OnDestroy { if (refreshToken) { this._storage.setItem('refresh_token', refreshToken); } + if (customParameters) { + customParameters.forEach((value : string, key: string) => { + this._storage.setItem(key, value); + }); + } } /** @@ -1455,7 +1465,7 @@ export class OAuthService extends AuthConfig implements OnDestroy { options.customHashFragment.substring(1) : window.location.search; - const parts = this.getCodePartsFromUrl(window.location.search); + const parts = this.getCodePartsFromUrl(querySource); const code = parts['code']; const state = parts['state']; @@ -1580,7 +1590,8 @@ export class OAuthService extends AuthConfig implements OnDestroy { tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, - tokenResponse.scope); + tokenResponse.scope, + this.extractRecognizedCustomParameters(tokenResponse)); if (this.oidc && tokenResponse.id_token) { this.processIdToken(tokenResponse.id_token, tokenResponse.access_token). @@ -2084,6 +2095,16 @@ export class OAuthService extends AuthConfig implements OnDestroy { return false; } + /** + * Retrieve a saved custom property of the TokenReponse object. Only if predefined in authconfig. + */ + public getCustomTokenResponseProperty(requestedProperty: string): any { + return this._storage && this.config.customTokenParameters + && (this.config.customTokenParameters.indexOf(requestedProperty) >= 0) + && this._storage.getItem(requestedProperty) !== null + ? JSON.parse(this._storage.getItem(requestedProperty)) : null; + } + /** * Returns the auth-header that can be used * to transmit the access_token to a service @@ -2095,10 +2116,11 @@ export class OAuthService extends AuthConfig implements OnDestroy { /** * Removes all tokens and logs the user out. * If a logout url is configured, the user is - * redirected to it. + * redirected to it with optional state parameter. * @param noRedirectToLogoutUrl + * @param state */ - public logOut(noRedirectToLogoutUrl = false): void { + public logOut(noRedirectToLogoutUrl = false, state = ''): void { const id_token = this.getIdToken(); this._storage.removeItem('access_token'); this._storage.removeItem('id_token'); @@ -2111,7 +2133,9 @@ export class OAuthService extends AuthConfig implements OnDestroy { this._storage.removeItem('access_token_stored_at'); this._storage.removeItem('granted_scopes'); this._storage.removeItem('session_state'); - + if (this.config.customTokenParameters) { + this.config.customTokenParameters.forEach(customParam => this._storage.removeItem(customParam)); + } this.silentRefreshSubject = null; this.eventsSubject.next(new OAuthInfoEvent('logout')); @@ -2151,6 +2175,10 @@ export class OAuthService extends AuthConfig implements OnDestroy { const postLogoutUrl = this.postLogoutRedirectUri || this.redirectUri; if (postLogoutUrl) { params = params.set('post_logout_redirect_uri', postLogoutUrl); + + if (state) { + params = params.set('state', state); + } } logoutUrl = @@ -2180,14 +2208,14 @@ export class OAuthService extends AuthConfig implements OnDestroy { this.clearIdTokenTimer(); this.removeSilentRefreshEventListener(); - const silentRefreshFrame = document.getElementById(this.silentRefreshIFrameName); + const silentRefreshFrame = this.document.getElementById(this.silentRefreshIFrameName); if (silentRefreshFrame) { silentRefreshFrame.remove(); } this.stopSessionCheckTimer(); this.removeSessionCheckEventListener(); - const sessionCheckFrame = document.getElementById(this.sessionCheckIFrameName); + const sessionCheckFrame = this.document.getElementById(this.sessionCheckIFrameName); if (sessionCheckFrame) { sessionCheckFrame.remove(); } @@ -2305,8 +2333,21 @@ export class OAuthService extends AuthConfig implements OnDestroy { const verifier = await this.createNonce(); const challengeRaw = await this.crypto.calcHash(verifier, 'sha-256'); - const challange = base64UrlEncode(challengeRaw); - - return [challange, verifier]; + const challenge = base64UrlEncode(challengeRaw); + + return [challenge, verifier]; + } + + private extractRecognizedCustomParameters(tokenResponse: TokenResponse): Map { + let foundParameters: Map = new Map(); + if (!this.config.customTokenParameters) { + return foundParameters; + } + this.config.customTokenParameters.forEach((recognizedParameter: string) => { + if (tokenResponse[recognizedParameter]) { + foundParameters.set(recognizedParameter, JSON.stringify(tokenResponse[recognizedParameter])); + } + }); + return foundParameters; } } diff --git a/projects/sample/src/app/auth-code-flow.config.ts b/projects/sample/src/app/auth-code-flow.config.ts index 902839a9..fa049b5e 100644 --- a/projects/sample/src/app/auth-code-flow.config.ts +++ b/projects/sample/src/app/auth-code-flow.config.ts @@ -9,6 +9,8 @@ export const authCodeFlowConfig: AuthConfig = { ? '/#/index.html' : '/index.html'), + silentRefreshRedirectUri: `${window.location.origin}/silent-refresh.html`, + // The SPA's id. The SPA is registerd with this id at the auth-server // clientId: 'server.code', clientId: 'spa', diff --git a/projects/sample/src/app/home/home.component.html b/projects/sample/src/app/home/home.component.html index 2b115404..df1690fb 100644 --- a/projects/sample/src/app/home/home.component.html +++ b/projects/sample/src/app/home/home.component.html @@ -28,6 +28,17 @@

Login with Implicit Flow

+
+
+

Login with Implicit Flow in popup

+

+ + +

+ Username/Password: max/geheim +
+
+

Login with Code Flow

@@ -39,6 +50,17 @@

Login with Code Flow

+
+
+

Login with Code Flow in popup

+

+ + +

+ Username/Password: alice/alice +
+
+

diff --git a/projects/sample/src/app/home/home.component.ts b/projects/sample/src/app/home/home.component.ts index bcc19e6d..6cfceb36 100644 --- a/projects/sample/src/app/home/home.component.ts +++ b/projects/sample/src/app/home/home.component.ts @@ -34,6 +34,19 @@ export class HomeComponent implements OnInit { // the parameter here is optional. It's passed around and can be used after logging in } + async loginImplicitInPopup() { + + // Tweak config for implicit flow + this.oauthService.configure(authConfig); + await this.oauthService.loadDiscoveryDocument(); + sessionStorage.setItem('flow', 'implicit'); + + this.oauthService.initLoginFlowInPopup().then(() => { + this.loadUserProfile(); + }); + // the parameter here is optional. It's passed around and can be used after logging in + } + async loginCode() { // Tweak config for code flow this.oauthService.configure(authCodeFlowConfig); @@ -44,6 +57,17 @@ export class HomeComponent implements OnInit { // the parameter here is optional. It's passed around and can be used after logging in } + async loginCodeInPopup() { + // Tweak config for code flow + this.oauthService.configure(authCodeFlowConfig); + await this.oauthService.loadDiscoveryDocument(); + sessionStorage.setItem('flow', 'code'); + + this.oauthService.initLoginFlowInPopup().then(() => { + this.loadUserProfile(); + }); + } + logout() { this.oauthService.logOut(); } diff --git a/projects/sample/src/silent-refresh.html b/projects/sample/src/silent-refresh.html index 7e3423c4..6d5fc966 100644 --- a/projects/sample/src/silent-refresh.html +++ b/projects/sample/src/silent-refresh.html @@ -1,7 +1,7 @@ - \ No newline at end of file +