From e718d3b5ceee38108d316cde5486f83f7de1c285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Lottes?= Date: Mon, 26 Mar 2018 10:48:51 +0200 Subject: [PATCH] Get granted scopes --- angular-oauth2-oidc/src/oauth-service.ts | 385 ++++++++++++----------- 1 file changed, 198 insertions(+), 187 deletions(-) diff --git a/angular-oauth2-oidc/src/oauth-service.ts b/angular-oauth2-oidc/src/oauth-service.ts index 17847937..5d0beb7a 100644 --- a/angular-oauth2-oidc/src/oauth-service.ts +++ b/angular-oauth2-oidc/src/oauth-service.ts @@ -18,7 +18,7 @@ import { AuthConfig } from './auth.config'; */ @Injectable() export class OAuthService - extends AuthConfig { + extends AuthConfig { // extending AuthConfig ist just for LEGACY reasons // to not break existing code @@ -51,7 +51,7 @@ export class OAuthService * The received (passed around) state, when logging * in with implicit flow. */ - public state? = ''; + public state?= ''; private eventsSubject: Subject = new Subject(); private discoveryDocumentLoadedSubject: Subject = new Subject(); @@ -86,7 +86,7 @@ export class OAuthService this.configure(config); } - + try { if (storage) { this.setStorage(storage); @@ -94,7 +94,7 @@ export class OAuthService this.setStorage(sessionStorage); } } - catch(e) { + catch (e) { console.error('cannot access sessionStorage. Consider setting an own storage implementation using setStorage', e); } this.setupRefreshTimer(); @@ -167,11 +167,11 @@ export class OAuthService public loadDiscoveryDocumentAndLogin(options: LoginOptions = null) { return this.loadDiscoveryDocumentAndTryLogin(options).then(_ => { if (!this.hasValidIdToken() || !this.hasValidAccessToken()) { - this.initImplicitFlow(); - return false; + this.initImplicitFlow(); + return false; } else { - return true; + return true; } }); } @@ -194,7 +194,7 @@ export class OAuthService if (!issuerCheck) { errors.push('Every url in discovery document has to start with the issuer url.' - + 'Also see property strictDiscoveryDocumentValidation.'); + + 'Also see property strictDiscoveryDocumentValidation.'); } return errors; @@ -211,7 +211,7 @@ export class OAuthService if ((lcUrl.match(/^http:\/\/localhost($|[:\/])/) || lcUrl.match(/^http:\/\/localhost($|[:\/])/)) && this.requireHttps === 'remoteOnly') { - return true; + return true; } return lcUrl.startsWith('https://'); @@ -250,11 +250,11 @@ export class OAuthService let accessTokenExp = this.getAccessTokenExpiration() || Number.MAX_VALUE; let useAccessTokenExp = accessTokenExp <= idTokenExp; - if (this.hasValidAccessToken() && useAccessTokenExp ) { + if (this.hasValidAccessToken() && useAccessTokenExp) { this.setupAccessTokenTimer(); } - if (this.hasValidIdToken() && !useAccessTokenExp ) { + if (this.hasValidIdToken() && !useAccessTokenExp) { this.setupIdTokenTimer(); } } @@ -455,7 +455,7 @@ export class OAuthService return false; } - if (this.sessionChecksEnabled && !doc.check_session_iframe) { + if (this.sessionChecksEnabled && !doc.check_session_iframe) { console.warn( 'sessionChecksEnabled is activated but discovery document' + ' does not contain a check_session_iframe field'); @@ -485,8 +485,8 @@ export class OAuthService password: string, headers: HttpHeaders = new HttpHeaders()): Promise { return this - .fetchTokenUsingPasswordFlow(userName, password, headers) - .then(() => this.loadUserProfile()); + .fetchTokenUsingPasswordFlow(userName, password, headers) + .then(() => this.loadUserProfile()); } /** @@ -518,8 +518,8 @@ export class OAuthService if (!this.skipSubjectCheck) { if (this.oidc && (!existingClaims['sub'] || info.sub !== existingClaims['sub'])) { let err = 'if property oidc is true, the received user-id (sub) has to be the user-id ' - + 'of the user that has logged in with oidc.\n' - + 'if you are not using oidc but just oauth2 password flow set oidc to false'; + + 'of the user that has logged in with oidc.\n' + + 'if you are not using oidc but just oauth2 password flow set oidc to false'; reject(err); return; @@ -576,7 +576,7 @@ export class OAuthService this.http.post(this.tokenEndpoint, params, { headers }).subscribe( (tokenResponse) => { this.debug('tokenResponse', tokenResponse); - this.storeAccessTokenResponse(tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in); + this.storeAccessTokenResponse(tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, tokenResponse.scope); this.eventsSubject.next(new OAuthSuccessEvent('token_received')); resolve(tokenResponse); @@ -627,7 +627,7 @@ export class OAuthService this.http.post(this.tokenEndpoint, params, { headers }).subscribe( (tokenResponse) => { this.debug('refresh tokenResponse', tokenResponse); - this.storeAccessTokenResponse(tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in); + this.storeAccessTokenResponse(tokenResponse.access_token, tokenResponse.refresh_token, tokenResponse.expires_in, tokenResponse.scope); this.eventsSubject.next(new OAuthSuccessEvent('token_received')); this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed')); @@ -660,7 +660,7 @@ export class OAuthService expectedPrefix += this.silentRefreshMessagePrefix; } - if (!e || !e.data || typeof e.data !== 'string' ) return; + if (!e || !e.data || typeof e.data !== 'string') return; let prefixedMessage: string = e.data; @@ -677,7 +677,7 @@ export class OAuthService this.eventsSubject.next(new OAuthSuccessEvent('silently_refreshed')); } }) - .catch(err => this.debug('tryLogin during silent refresh failed', err)); + .catch(err => this.debug('tryLogin during silent refresh failed', err)); }; window.addEventListener('message', this.silentRefreshPostMessageEventListener); @@ -726,7 +726,7 @@ export class OAuthService this.createLoginUrl(null, null, redirectUri, true, params).then(url => { iframe.setAttribute('src', url); if (!this.silentRefreshShowIFrame) { - iframe.style['display'] = 'none'; + iframe.style['display'] = 'none'; } document.body.appendChild(iframe); }); @@ -734,29 +734,29 @@ export class OAuthService let errors = this.events.filter(e => e instanceof OAuthErrorEvent).first(); let success = this.events.filter(e => e.type === 'silently_refreshed').first(); let timeout = Observable.of(new OAuthErrorEvent('silent_refresh_timeout', null)) - .delay(this.silentRefreshTimeout || this.siletRefreshTimeout); + .delay(this.silentRefreshTimeout || this.siletRefreshTimeout); return Observable - .race([errors, success, timeout]) - .do(e => { - if (e.type === 'silent_refresh_timeout') { - this.eventsSubject.next(e); - } - }) - .map(e => { - if (e instanceof OAuthErrorEvent) { - throw e; - } - return e; - }) - .toPromise(); + .race([errors, success, timeout]) + .do(e => { + if (e.type === 'silent_refresh_timeout') { + this.eventsSubject.next(e); + } + }) + .map(e => { + if (e instanceof OAuthErrorEvent) { + throw e; + } + return e; + }) + .toPromise(); } private canPerformSessionCheck(): boolean { if (!this.sessionChecksEnabled) return false; if (!this.sessionCheckIFrameUrl) { console.warn('sessionChecksEnabled is activated but there ' - + 'is no sessionCheckIFrameUrl'); + + 'is no sessionCheckIFrameUrl'); return false; } let sessionState = this.getSessionState(); @@ -952,17 +952,17 @@ export class OAuthService } let url = that.loginUrl - + seperationChar - + 'response_type=' - + encodeURIComponent(that.responseType) - + '&client_id=' - + encodeURIComponent(that.clientId) - + '&state=' - + encodeURIComponent(state) - + '&redirect_uri=' - + encodeURIComponent(redirectUri) - + '&scope=' - + encodeURIComponent(scope); + + seperationChar + + 'response_type=' + + encodeURIComponent(that.responseType) + + '&client_id=' + + encodeURIComponent(that.clientId) + + '&state=' + + encodeURIComponent(state) + + '&redirect_uri=' + + encodeURIComponent(redirectUri) + + '&scope=' + + encodeURIComponent(scope); if (loginHint) { url += '&login_hint=' + encodeURIComponent(loginHint); @@ -980,7 +980,7 @@ export class OAuthService url += '&prompt=none'; } - for(let key of Object.keys(params)) { + for (let key of Object.keys(params)) { url += '&' + encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); } @@ -1019,11 +1019,11 @@ export class OAuthService this.createLoginUrl(additionalState, loginHint, null, false, addParams).then(function (url) { location.href = url; }) - .catch(error => { - console.error('Error in initImplicitFlow'); - console.error(error); - this.inImplicitFlow = false; - }); + .catch(error => { + console.error('Error in initImplicitFlow'); + console.error(error); + this.inImplicitFlow = false; + }); }; /** @@ -1058,8 +1058,9 @@ export class OAuthService } } - private storeAccessTokenResponse(accessToken: string, refreshToken: string, expiresIn: number): void { + private storeAccessTokenResponse(accessToken: string, refreshToken: string, expiresIn: number, grantedScopes: String): void { this._storage.setItem('access_token', accessToken); + this._storage.setItem('granted_scopes', JSON.stringify(grantedScopes.split('+'))); this._storage.setItem('access_token_stored_at', '' + Date.now()); if (expiresIn) { let expiresInMilliSeconds = expiresIn * 1000; @@ -1083,7 +1084,7 @@ export class OAuthService */ public tryLogin(options: LoginOptions = null): Promise { - options = options || { }; + options = options || {}; let parts: object; @@ -1108,6 +1109,7 @@ export class OAuthService let idToken = parts['id_token']; let state = decodeURIComponent(parts['state']); let sessionState = parts['session_state']; + let grantedScopes = parts['scope']; if (!this.requestAccessToken && !this.oidc) { return Promise.reject('Either requestAccessToken or oidc or both must be true.'); @@ -1127,9 +1129,9 @@ export class OAuthService let nonceInState = state; let idx = state.indexOf(';'); - if ( idx > -1) { + if (idx > -1) { nonceInState = state.substr(0, idx); - this.state = state.substr(idx+1); + this.state = state.substr(idx + 1); } /* let stateParts = state.split(';'); @@ -1149,42 +1151,42 @@ export class OAuthService } if (this.requestAccessToken) { - this.storeAccessTokenResponse(accessToken, null, parts['expires_in']); + this.storeAccessTokenResponse(accessToken, null, parts['expires_in'], grantedScopes); } if (!this.oidc) { this.eventsSubject.next(new OAuthSuccessEvent('token_received')); if (this.clearHashAfterLogin) location.hash = ''; - return Promise.resolve(); + return Promise.resolve(); } return this - .processIdToken(idToken, accessToken) - .then(result => { - if (options.validationHandler) { - return options.validationHandler({ - accessToken: accessToken, - idClaims: result.idTokenClaims, - idToken: result.idToken, - state: state - }).then(_ => result); - } - return result; - }) - .then(result => { - this.storeIdToken(result); - this.storeSessionState(sessionState); - this.eventsSubject.next(new OAuthSuccessEvent('token_received')); - this.callOnTokenReceivedIfExists(options); - this.inImplicitFlow = false; - if (this.clearHashAfterLogin) location.hash = ''; - }) - .catch(reason => { - this.eventsSubject.next(new OAuthErrorEvent('token_validation_error', reason)); - console.error('Error validating tokens'); - console.error(reason); - return Promise.reject(reason); - }); + .processIdToken(idToken, accessToken) + .then(result => { + if (options.validationHandler) { + return options.validationHandler({ + accessToken: accessToken, + idClaims: result.idTokenClaims, + idToken: result.idToken, + state: state + }).then(_ => result); + } + return result; + }) + .then(result => { + this.storeIdToken(result); + this.storeSessionState(sessionState); + this.eventsSubject.next(new OAuthSuccessEvent('token_received')); + this.callOnTokenReceivedIfExists(options); + this.inImplicitFlow = false; + if (this.clearHashAfterLogin) location.hash = ''; + }) + .catch(reason => { + this.eventsSubject.next(new OAuthErrorEvent('token_validation_error', reason)); + console.error('Error validating tokens'); + console.error(reason); + return Promise.reject(reason); + }); }; @@ -1222,127 +1224,127 @@ export class OAuthService /** * @ignore */ - public processIdToken(idToken: string, accessToken: string): Promise { - - let tokenParts = idToken.split('.'); - let headerBase64 = this.padBase64(tokenParts[0]); - let headerJson = b64DecodeUnicode(headerBase64); - let header = JSON.parse(headerJson); - let claimsBase64 = this.padBase64(tokenParts[1]); - let claimsJson = b64DecodeUnicode(claimsBase64); - let claims = JSON.parse(claimsJson); - let savedNonce = this._storage.getItem('nonce'); - - if (Array.isArray(claims.aud)) { - if (claims.aud.every(v => v !== this.clientId)) { - let err = 'Wrong audience: ' + claims.aud.join(','); - console.warn(err); - return Promise.reject(err); - } - } else { - if (claims.aud !== this.clientId) { - let err = 'Wrong audience: ' + claims.aud; - console.warn(err); - return Promise.reject(err); - } - } + public processIdToken(idToken: string, accessToken: string): Promise { + + let tokenParts = idToken.split('.'); + let headerBase64 = this.padBase64(tokenParts[0]); + let headerJson = b64DecodeUnicode(headerBase64); + let header = JSON.parse(headerJson); + let claimsBase64 = this.padBase64(tokenParts[1]); + let claimsJson = b64DecodeUnicode(claimsBase64); + let claims = JSON.parse(claimsJson); + let savedNonce = this._storage.getItem('nonce'); - /* - if (this.getKeyCount() > 1 && !header.kid) { - let err = 'There needs to be a kid property in the id_token header when multiple keys are defined via the property jwks'; + if (Array.isArray(claims.aud)) { + if (claims.aud.every(v => v !== this.clientId)) { + let err = 'Wrong audience: ' + claims.aud.join(','); console.warn(err); return Promise.reject(err); } - */ - - if (!claims.sub) { - let err = 'No sub claim in id_token'; + } else { + if (claims.aud !== this.clientId) { + let err = 'Wrong audience: ' + claims.aud; console.warn(err); return Promise.reject(err); } + } - /* For now, we only check whether the sub against - * silentRefreshSubject when sessionChecksEnabled is on - * We will reconsider in a later version to do this - * in every other case too. - */ - if (this.sessionChecksEnabled - && this.silentRefreshSubject - && this.silentRefreshSubject !== claims['sub']) { + /* + if (this.getKeyCount() > 1 && !header.kid) { + let err = 'There needs to be a kid property in the id_token header when multiple keys are defined via the property jwks'; + console.warn(err); + return Promise.reject(err); + } + */ - let err = 'After refreshing, we got an id_token for another user (sub). ' - + `Expected sub: ${this.silentRefreshSubject}, received sub: ${claims['sub']}`; + if (!claims.sub) { + let err = 'No sub claim in id_token'; + console.warn(err); + return Promise.reject(err); + } - console.warn(err); - return Promise.reject(err); - } + /* For now, we only check whether the sub against + * silentRefreshSubject when sessionChecksEnabled is on + * We will reconsider in a later version to do this + * in every other case too. + */ + if (this.sessionChecksEnabled + && this.silentRefreshSubject + && this.silentRefreshSubject !== claims['sub']) { - if (!claims.iat) { - let err = 'No iat claim in id_token'; - console.warn(err); - return Promise.reject(err); - } + let err = 'After refreshing, we got an id_token for another user (sub). ' + + `Expected sub: ${this.silentRefreshSubject}, received sub: ${claims['sub']}`; - if (claims.iss !== this.issuer) { - let err = 'Wrong issuer: ' + claims.iss; - console.warn(err); - return Promise.reject(err); - } + console.warn(err); + return Promise.reject(err); + } - if (claims.nonce !== savedNonce) { - let err = 'Wrong nonce: ' + claims.nonce; - console.warn(err); - return Promise.reject(err); - } + if (!claims.iat) { + let err = 'No iat claim in id_token'; + console.warn(err); + return Promise.reject(err); + } - if (!this.disableAtHashCheck && this.requestAccessToken && !claims['at_hash']) { - let err = 'An at_hash is needed!'; - console.warn(err); - return Promise.reject(err); - } + if (claims.iss !== this.issuer) { + let err = 'Wrong issuer: ' + claims.iss; + console.warn(err); + return Promise.reject(err); + } - let now = Date.now(); - let issuedAtMSec = claims.iat * 1000; - let expiresAtMSec = claims.exp * 1000; - let tenMinutesInMsec = 1000 * 60 * 10; - - if (issuedAtMSec - tenMinutesInMsec >= now || expiresAtMSec + tenMinutesInMsec <= now) { - let err = 'Token has been expired'; - console.error(err); - console.error({ - now: now, - issuedAtMSec: issuedAtMSec, - expiresAtMSec: expiresAtMSec - }); - return Promise.reject(err); - } + if (claims.nonce !== savedNonce) { + let err = 'Wrong nonce: ' + claims.nonce; + console.warn(err); + return Promise.reject(err); + } + + if (!this.disableAtHashCheck && this.requestAccessToken && !claims['at_hash']) { + let err = 'An at_hash is needed!'; + console.warn(err); + return Promise.reject(err); + } + + let now = Date.now(); + let issuedAtMSec = claims.iat * 1000; + let expiresAtMSec = claims.exp * 1000; + let tenMinutesInMsec = 1000 * 60 * 10; + + if (issuedAtMSec - tenMinutesInMsec >= now || expiresAtMSec + tenMinutesInMsec <= now) { + let err = 'Token has been expired'; + console.error(err); + console.error({ + now: now, + issuedAtMSec: issuedAtMSec, + expiresAtMSec: expiresAtMSec + }); + return Promise.reject(err); + } - let validationParams: ValidationParams = { - accessToken: accessToken, + let validationParams: ValidationParams = { + accessToken: accessToken, + idToken: idToken, + jwks: this.jwks, + idTokenClaims: claims, + idTokenHeader: header, + loadKeys: () => this.loadJwks() + }; + + if (!this.disableAtHashCheck && this.requestAccessToken && !this.checkAtHash(validationParams)) { + let err = 'Wrong at_hash'; + console.warn(err); + return Promise.reject(err); + } + + return this.checkSignature(validationParams).then(_ => { + let result: ParsedIdToken = { idToken: idToken, - jwks: this.jwks, idTokenClaims: claims, + idTokenClaimsJson: claimsJson, idTokenHeader: header, - loadKeys: () => this.loadJwks() + idTokenHeaderJson: headerJson, + idTokenExpiresAt: expiresAtMSec, }; - - if (!this.disableAtHashCheck && this.requestAccessToken && !this.checkAtHash(validationParams)) { - let err = 'Wrong at_hash'; - console.warn(err); - return Promise.reject(err); - } - - return this.checkSignature(validationParams).then(_ => { - let result: ParsedIdToken = { - idToken: idToken, - idTokenClaims: claims, - idTokenClaimsJson: claimsJson, - idTokenHeader: header, - idTokenHeaderJson: headerJson, - idTokenExpiresAt: expiresAtMSec, - }; - return result; - }); + return result; + }); } @@ -1355,6 +1357,15 @@ export class OAuthService return JSON.parse(claims); } + /** + * Returns the granted scopes from the server. + */ + public getGrantedScopes(): object { + let scopes = this._storage.getItem('granted_scopes'); + if (!scopes) return null; + return JSON.parse(scopes); + } + /** * Returns the current id_token. */ @@ -1472,7 +1483,7 @@ export class OAuthService this._storage.removeItem('access_token_stored_at'); this.silentRefreshSubject = null; - + this.eventsSubject.next(new OAuthInfoEvent('logout')); if (!this.logoutUrl) return;