UNPKG

angular-auth-oidc-client

Version:

An OpenID Connect Code Flow with PKCE,Implicit Flow client for Angular

1,427 lines (1,409 loc) 127 kB
import { isPlatformBrowser } from '@angular/common'; import { hextob64u, KEYUTIL, KJUR } from 'jsrsasign-reduced'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Router } from '@angular/router'; import { Subject, from, Observable, of, ReplaySubject, BehaviorSubject, throwError, timer } from 'rxjs'; import { take, catchError, switchMap, map, filter, race, shareReplay, switchMapTo, tap } from 'rxjs/operators'; import { Injectable, Inject, PLATFORM_ID, NgZone, NgModule, defineInjectable, inject } from '@angular/core'; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class AuthorizationResult { /** * @param {?} authorizationState * @param {?} validationResult */ constructor(authorizationState, validationResult) { this.authorizationState = authorizationState; this.validationResult = validationResult; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @enum {string} */ const AuthorizationState = { authorized: 'authorized', forbidden: 'forbidden', unauthorized: 'unauthorized', }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class JwtKeys { constructor() { this.keys = []; } } class JwtKey { constructor() { this.kty = ''; this.use = ''; this.kid = ''; this.x5t = ''; this.e = ''; this.n = ''; this.x5c = []; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** @enum {string} */ const ValidationResult = { NotSet: 'NotSet', StatesDoNotMatch: 'StatesDoNotMatch', SignatureFailed: 'SignatureFailed', IncorrectNonce: 'IncorrectNonce', RequiredPropertyMissing: 'RequiredPropertyMissing', MaxOffsetExpired: 'MaxOffsetExpired', IssDoesNotMatchIssuer: 'IssDoesNotMatchIssuer', NoAuthWellKnownEndPoints: 'NoAuthWellKnownEndPoints', IncorrectAud: 'IncorrectAud', TokenExpired: 'TokenExpired', IncorrectAtHash: 'IncorrectAtHash', Ok: 'Ok', LoginRequired: 'LoginRequired', SecureTokenServerError: 'SecureTokenServerError', }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class ValidateStateResult { /** * @param {?=} access_token * @param {?=} id_token * @param {?=} authResponseIsValid * @param {?=} decoded_id_token * @param {?=} state */ constructor(access_token = '', id_token = '', authResponseIsValid = false, decoded_id_token = {}, state = ValidationResult.NotSet) { this.access_token = access_token; this.id_token = id_token; this.authResponseIsValid = authResponseIsValid; this.decoded_id_token = decoded_id_token; this.state = state; } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class OidcDataService { /** * @param {?} httpClient */ constructor(httpClient) { this.httpClient = httpClient; } /** * @template T * @param {?} url * @return {?} */ getWellknownEndpoints(url) { /** @type {?} */ let headers = new HttpHeaders(); headers = headers.set('Accept', 'application/json'); return this.httpClient.get(url, { headers: headers, }); } /** * @template T * @param {?} url * @param {?} token * @return {?} */ getIdentityUserData(url, token) { /** @type {?} */ let headers = new HttpHeaders(); headers = headers.set('Accept', 'application/json'); headers = headers.set('Authorization', 'Bearer ' + decodeURIComponent(token)); return this.httpClient.get(url, { headers: headers, }); } /** * @template T * @param {?} url * @return {?} */ get(url) { /** @type {?} */ let headers = new HttpHeaders(); headers = headers.set('Accept', 'application/json'); return this.httpClient.get(url, { headers: headers, }); } } OidcDataService.decorators = [ { type: Injectable } ]; /** @nocollapse */ OidcDataService.ctorParameters = () => [ { type: HttpClient } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class PlatformProvider { /** * @param {?} platformId */ constructor(platformId) { this.platformId = platformId; } /** * @return {?} */ get isBrowser() { return isPlatformBrowser(this.platformId); } } PlatformProvider.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ PlatformProvider.ctorParameters = () => [ { type: Object, decorators: [{ type: Inject, args: [PLATFORM_ID,] }] } ]; /** @nocollapse */ PlatformProvider.ngInjectableDef = defineInjectable({ factory: function PlatformProvider_Factory() { return new PlatformProvider(inject(PLATFORM_ID)); }, token: PlatformProvider, providedIn: "root" }); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class ConfigurationProvider { /** * @param {?} platformProvider */ constructor(platformProvider) { this.platformProvider = platformProvider; this.DEFAULT_CONFIG = { stsServer: 'https://please_set', redirect_url: 'https://please_set', client_id: 'please_set', response_type: 'code', scope: 'openid email profile', hd_param: '', post_logout_redirect_uri: 'https://please_set', start_checksession: false, silent_renew: false, silent_renew_url: 'https://please_set', silent_renew_offset_in_seconds: 0, use_refresh_token: false, post_login_route: '/', forbidden_route: '/forbidden', unauthorized_route: '/unauthorized', auto_userinfo: true, auto_clean_state_after_authentication: true, trigger_authorization_result_event: false, log_console_warning_active: true, log_console_debug_active: false, iss_validation_off: false, history_cleanup_off: false, max_id_token_iat_offset_allowed_in_seconds: 3, isauthorizedrace_timeout_in_seconds: 5, disable_iat_offset_validation: false, storage: typeof Storage !== 'undefined' ? sessionStorage : null, }; this.INITIAL_AUTHWELLKNOWN = { issuer: '', jwks_uri: '', authorization_endpoint: '', token_endpoint: '', userinfo_endpoint: '', end_session_endpoint: '', check_session_iframe: '', revocation_endpoint: '', introspection_endpoint: '', }; this.mergedOpenIdConfiguration = this.DEFAULT_CONFIG; this.authWellKnownEndpoints = this.INITIAL_AUTHWELLKNOWN; this.onConfigurationChangeInternal = new Subject(); } /** * @return {?} */ get openIDConfiguration() { return this.mergedOpenIdConfiguration; } /** * @return {?} */ get wellKnownEndpoints() { return this.authWellKnownEndpoints; } /** * @return {?} */ get onConfigurationChange() { return this.onConfigurationChangeInternal.asObservable(); } /** * @param {?} passedOpenIfConfiguration * @param {?} passedAuthWellKnownEndpoints * @return {?} */ setup(passedOpenIfConfiguration, passedAuthWellKnownEndpoints) { this.mergedOpenIdConfiguration = Object.assign({}, this.mergedOpenIdConfiguration, passedOpenIfConfiguration); this.setSpecialCases(this.mergedOpenIdConfiguration); this.authWellKnownEndpoints = Object.assign({}, passedAuthWellKnownEndpoints); this.onConfigurationChangeInternal.next(Object.assign({}, this.mergedOpenIdConfiguration)); } /** * @private * @param {?} currentConfig * @return {?} */ setSpecialCases(currentConfig) { if (!this.platformProvider.isBrowser) { currentConfig.start_checksession = false; currentConfig.silent_renew = false; currentConfig.use_refresh_token = false; } } } ConfigurationProvider.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ ConfigurationProvider.ctorParameters = () => [ { type: PlatformProvider } ]; /** @nocollapse */ ConfigurationProvider.ngInjectableDef = defineInjectable({ factory: function ConfigurationProvider_Factory() { return new ConfigurationProvider(inject(PlatformProvider)); }, token: ConfigurationProvider, providedIn: "root" }); /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class LoggerService { /** * @param {?} configurationProvider */ constructor(configurationProvider) { this.configurationProvider = configurationProvider; } /** * @param {?} message * @param {...?} args * @return {?} */ logError(message, ...args) { console.error(message, ...args); } /** * @param {?} message * @return {?} */ logWarning(message) { if (this.configurationProvider.openIDConfiguration.log_console_warning_active) { console.warn(message); } } /** * @param {?} message * @return {?} */ logDebug(message) { if (this.configurationProvider.openIDConfiguration.log_console_debug_active) { console.log(message); } } } LoggerService.decorators = [ { type: Injectable } ]; /** @nocollapse */ LoggerService.ctorParameters = () => [ { type: ConfigurationProvider } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class IFrameService { /** * @param {?} loggerService */ constructor(loggerService) { this.loggerService = loggerService; } /** * @param {?} identifier * @return {?} */ getExistingIFrame(identifier) { /** @type {?} */ const iFrameOnParent = this.getIFrameFromParentWindow(identifier); if (this.isIFrameElement(iFrameOnParent)) { return iFrameOnParent; } /** @type {?} */ const iFrameOnSelf = this.getIFrameFromWindow(identifier); if (this.isIFrameElement(iFrameOnSelf)) { return iFrameOnSelf; } return null; } /** * @param {?} identifier * @return {?} */ addIFrameToWindowBody(identifier) { /** @type {?} */ const sessionIframe = window.document.createElement('iframe'); sessionIframe.id = identifier; this.loggerService.logDebug(sessionIframe); sessionIframe.style.display = 'none'; window.document.body.appendChild(sessionIframe); return sessionIframe; } /** * @private * @param {?} identifier * @return {?} */ getIFrameFromParentWindow(identifier) { try { /** @type {?} */ const iFrameElement = window.parent.document.getElementById(identifier); if (this.isIFrameElement(iFrameElement)) { return iFrameElement; } return null; } catch (e) { return null; } } /** * @private * @param {?} identifier * @return {?} */ getIFrameFromWindow(identifier) { /** @type {?} */ const iFrameElement = window.document.getElementById(identifier); if (this.isIFrameElement(iFrameElement)) { return iFrameElement; } return null; } /** * @private * @param {?} element * @return {?} */ isIFrameElement(element) { return !!element && element instanceof HTMLIFrameElement; } } IFrameService.decorators = [ { type: Injectable } ]; /** @nocollapse */ IFrameService.ctorParameters = () => [ { type: LoggerService } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class EqualityHelperService { /** * @param {?} value1 * @param {?} value2 * @return {?} */ areEqual(value1, value2) { if (!value1 || !value2) { return false; } if (this.bothValuesAreArrays(value1, value2)) { return this.arraysEqual((/** @type {?} */ (value1)), (/** @type {?} */ (value2))); } if (this.bothValuesAreStrings(value1, value2)) { return value1 === value2; } if (this.bothValuesAreObjects(value1, value2)) { return JSON.stringify(value1).toLowerCase() === JSON.stringify(value2).toLowerCase(); } if (this.oneValueIsStringAndTheOtherIsArray(value1, value2)) { if (Array.isArray(value1) && this.valueIsString(value2)) { return value1[0] === value2; } if (Array.isArray(value2) && this.valueIsString(value1)) { return value2[0] === value1; } } } /** * @private * @param {?} value1 * @param {?} value2 * @return {?} */ oneValueIsStringAndTheOtherIsArray(value1, value2) { return (Array.isArray(value1) && this.valueIsString(value2)) || (Array.isArray(value2) && this.valueIsString(value1)); } /** * @private * @param {?} value1 * @param {?} value2 * @return {?} */ bothValuesAreObjects(value1, value2) { return this.valueIsObject(value1) && this.valueIsObject(value2); } /** * @private * @param {?} value1 * @param {?} value2 * @return {?} */ bothValuesAreStrings(value1, value2) { return this.valueIsString(value1) && this.valueIsString(value2); } /** * @private * @param {?} value1 * @param {?} value2 * @return {?} */ bothValuesAreArrays(value1, value2) { return Array.isArray(value1) && Array.isArray(value2); } /** * @private * @param {?} value * @return {?} */ valueIsString(value) { return typeof value === 'string' || value instanceof String; } /** * @private * @param {?} value * @return {?} */ valueIsObject(value) { return typeof value === 'object'; } /** * @private * @param {?} arr1 * @param {?} arr2 * @return {?} */ arraysEqual(arr1, arr2) { if (arr1.length !== arr2.length) { return false; } for (let i = arr1.length; i--;) { if (arr1[i] !== arr2[i]) { return false; } } return true; } } EqualityHelperService.decorators = [ { type: Injectable } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class TokenHelperService { /** * @param {?} loggerService */ constructor(loggerService) { this.loggerService = loggerService; this.PARTS_OF_TOKEN = 3; } /** * @param {?} dataIdToken * @return {?} */ getTokenExpirationDate(dataIdToken) { if (!dataIdToken.hasOwnProperty('exp')) { return new Date(); } /** @type {?} */ const date = new Date(0); date.setUTCSeconds(dataIdToken.exp); return date; } /** * @param {?} token * @param {?} encoded * @return {?} */ getHeaderFromToken(token, encoded) { if (!this.tokenIsValid(token)) { return {}; } return this.getPartOfToken(token, 0, encoded); } /** * @param {?} token * @param {?} encoded * @return {?} */ getPayloadFromToken(token, encoded) { if (!this.tokenIsValid(token)) { return {}; } return this.getPartOfToken(token, 1, encoded); } /** * @param {?} token * @param {?} encoded * @return {?} */ getSignatureFromToken(token, encoded) { if (!this.tokenIsValid(token)) { return {}; } return this.getPartOfToken(token, 2, encoded); } /** * @private * @param {?} token * @param {?} index * @param {?} encoded * @return {?} */ getPartOfToken(token, index, encoded) { /** @type {?} */ const partOfToken = this.extractPartOfToken(token, index); if (encoded) { return partOfToken; } /** @type {?} */ const result = this.urlBase64Decode(partOfToken); return JSON.parse(result); } /** * @private * @param {?} str * @return {?} */ urlBase64Decode(str) { /** @type {?} */ let output = str.replace(/-/g, '+').replace(/_/g, '/'); switch (output.length % 4) { case 0: break; case 2: output += '=='; break; case 3: output += '='; break; default: throw Error('Illegal base64url string!'); } /** @type {?} */ const decoded = typeof window !== 'undefined' ? window.atob(output) : new Buffer(output, 'base64').toString('binary'); try { // Going backwards: from bytestream, to percent-encoding, to original string. return decodeURIComponent(decoded.split('') .map((/** * @param {?} c * @return {?} */ (c) => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))) .join('')); } catch (err) { return decoded; } } /** * @private * @param {?} token * @return {?} */ tokenIsValid(token) { if (!token) { this.loggerService.logError(`token '${token}' is not valid --> token falsy`); return false; } if (!((/** @type {?} */ (token))).includes('.')) { this.loggerService.logError(`token '${token}' is not valid --> no dots included`); return false; } /** @type {?} */ const parts = token.split('.'); if (parts.length !== this.PARTS_OF_TOKEN) { this.loggerService.logError(`token '${token}' is not valid --> token has to have exactly ${this.PARTS_OF_TOKEN} dots`); return false; } return true; } /** * @private * @param {?} token * @param {?} index * @return {?} */ extractPartOfToken(token, index) { return token.split('.')[index]; } } TokenHelperService.decorators = [ { type: Injectable } ]; /** @nocollapse */ TokenHelperService.ctorParameters = () => [ { type: LoggerService } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * Implement this class-interface to create a custom storage. * @abstract */ class OidcSecurityStorage { } OidcSecurityStorage.decorators = [ { type: Injectable } ]; class BrowserStorage { /** * @param {?} configProvider */ constructor(configProvider) { this.configProvider = configProvider; this.hasStorage = typeof Storage !== 'undefined'; } /** * @param {?} key * @return {?} */ read(key) { if (this.hasStorage) { return JSON.parse(this.configProvider.openIDConfiguration.storage.getItem(key + '_' + this.configProvider.openIDConfiguration.client_id)); } return; } /** * @param {?} key * @param {?} value * @return {?} */ write(key, value) { if (this.hasStorage) { value = value === undefined ? null : value; this.configProvider.openIDConfiguration.storage.setItem(key + '_' + this.configProvider.openIDConfiguration.client_id, JSON.stringify(value)); } } } BrowserStorage.decorators = [ { type: Injectable } ]; /** @nocollapse */ BrowserStorage.ctorParameters = () => [ { type: ConfigurationProvider } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class OidcSecurityCommon { /** * @param {?} oidcSecurityStorage */ constructor(oidcSecurityStorage) { this.oidcSecurityStorage = oidcSecurityStorage; this.storage_auth_result = 'authorizationResult'; this.storage_access_token = 'authorizationData'; this.storage_id_token = 'authorizationDataIdToken'; this.storage_is_authorized = '_isAuthorized'; this.storage_user_data = 'userData'; this.storage_auth_nonce = 'authNonce'; this.storage_code_verifier = 'code_verifier'; this.storage_auth_state_control = 'authStateControl'; this.storage_session_state = 'session_state'; this.storage_silent_renew_running = 'storage_silent_renew_running'; this.storage_custom_request_params = 'storage_custom_request_params'; } /** * @return {?} */ get authResult() { return this.retrieve(this.storage_auth_result); } /** * @param {?} value * @return {?} */ set authResult(value) { this.store(this.storage_auth_result, value); } /** * @return {?} */ get accessToken() { return this.retrieve(this.storage_access_token) || ''; } /** * @param {?} value * @return {?} */ set accessToken(value) { this.store(this.storage_access_token, value); } /** * @return {?} */ get idToken() { return this.retrieve(this.storage_id_token) || ''; } /** * @param {?} value * @return {?} */ set idToken(value) { this.store(this.storage_id_token, value); } /** * @return {?} */ get isAuthorized() { return this.retrieve(this.storage_is_authorized); } /** * @param {?} value * @return {?} */ set isAuthorized(value) { this.store(this.storage_is_authorized, value); } /** * @return {?} */ get userData() { return this.retrieve(this.storage_user_data); } /** * @param {?} value * @return {?} */ set userData(value) { this.store(this.storage_user_data, value); } /** * @return {?} */ get authNonce() { return this.retrieve(this.storage_auth_nonce) || ''; } /** * @param {?} value * @return {?} */ set authNonce(value) { this.store(this.storage_auth_nonce, value); } /** * @return {?} */ get code_verifier() { return this.retrieve(this.storage_code_verifier) || ''; } /** * @param {?} value * @return {?} */ set code_verifier(value) { this.store(this.storage_code_verifier, value); } /** * @return {?} */ get authStateControl() { return this.retrieve(this.storage_auth_state_control) || ''; } /** * @param {?} value * @return {?} */ set authStateControl(value) { this.store(this.storage_auth_state_control, value); } /** * @return {?} */ get sessionState() { return this.retrieve(this.storage_session_state); } /** * @param {?} value * @return {?} */ set sessionState(value) { this.store(this.storage_session_state, value); } /** * @return {?} */ get silentRenewRunning() { return this.retrieve(this.storage_silent_renew_running) || ''; } /** * @param {?} value * @return {?} */ set silentRenewRunning(value) { this.store(this.storage_silent_renew_running, value); } /** * @return {?} */ get customRequestParams() { return this.retrieve(this.storage_custom_request_params); } /** * @param {?} value * @return {?} */ set customRequestParams(value) { this.store(this.storage_custom_request_params, value); } /** * @private * @param {?} key * @return {?} */ retrieve(key) { return this.oidcSecurityStorage.read(key); } /** * @private * @param {?} key * @param {?} value * @return {?} */ store(key, value) { this.oidcSecurityStorage.write(key, value); } /** * @param {?} isRenewProcess * @return {?} */ resetStorageData(isRenewProcess) { if (!isRenewProcess) { this.store(this.storage_auth_result, ''); this.store(this.storage_session_state, ''); this.store(this.storage_silent_renew_running, ''); this.store(this.storage_is_authorized, false); this.store(this.storage_access_token, ''); this.store(this.storage_id_token, ''); this.store(this.storage_user_data, ''); this.store(this.storage_code_verifier, ''); } } /** * @return {?} */ getAccessToken() { return this.retrieve(this.storage_access_token); } /** * @return {?} */ getIdToken() { return this.retrieve(this.storage_id_token); } /** * @return {?} */ getRefreshToken() { return this.authResult.refresh_token; } } OidcSecurityCommon.decorators = [ { type: Injectable } ]; /** @nocollapse */ OidcSecurityCommon.ctorParameters = () => [ { type: OidcSecurityStorage } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ // http://openid.net/specs/openid-connect-implicit-1_0.html // id_token // id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) // MUST exactly match the value of the iss (issuer) Claim. // // id_token C2: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified // by the iss (issuer) Claim as an audience.The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, // or if it contains additional audiences not trusted by the Client. // // id_token C3: If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present. // // id_token C4: If an azp (authorized party) Claim is present, the Client SHOULD verify that its client_id is the Claim Value. // // id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the // alg Header Parameter of the JOSE Header.The Client MUST use the keys provided by the Issuer. // // id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the OpenID Connect Core 1.0 // [OpenID.Core] specification. // // id_token C7: The current time MUST be before the time represented by the exp Claim (possibly allowing for some small leeway to account // for clock skew). // // id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time, // limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific. // // id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one that was sent // in the Authentication Request.The Client SHOULD check the nonce value for replay attacks.The precise method for detecting replay attacks // is Client specific. // // id_token C10: If the acr Claim was requested, the Client SHOULD check that the asserted Claim Value is appropriate. // The meaning and processing of acr Claim Values is out of scope for this document. // // id_token C11: When a max_age request is made, the Client SHOULD check the auth_time Claim value and request re- authentication // if it determines too much time has elapsed since the last End- User authentication. // Access Token Validation // access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA] // for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256. // access_token C2: Take the left- most half of the hash and base64url- encode it. // access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash is present in the ID Token. class OidcSecurityValidation { /** * @param {?} arrayHelperService * @param {?} tokenHelperService * @param {?} loggerService */ constructor(arrayHelperService, tokenHelperService, loggerService) { this.arrayHelperService = arrayHelperService; this.tokenHelperService = tokenHelperService; this.loggerService = loggerService; } // id_token C7: The current time MUST be before the time represented by the exp Claim (possibly allowing for some small leeway to account for clock skew). /** * @param {?} token * @param {?=} offsetSeconds * @return {?} */ isTokenExpired(token, offsetSeconds) { /** @type {?} */ let decoded; decoded = this.tokenHelperService.getPayloadFromToken(token, false); return !this.validate_id_token_exp_not_expired(decoded, offsetSeconds); } // id_token C7: The current time MUST be before the time represented by the exp Claim (possibly allowing for some small leeway to account for clock skew). /** * @param {?} decoded_id_token * @param {?=} offsetSeconds * @return {?} */ validate_id_token_exp_not_expired(decoded_id_token, offsetSeconds) { /** @type {?} */ const tokenExpirationDate = this.tokenHelperService.getTokenExpirationDate(decoded_id_token); offsetSeconds = offsetSeconds || 0; if (!tokenExpirationDate) { return false; } /** @type {?} */ const tokenExpirationValue = tokenExpirationDate.valueOf(); /** @type {?} */ const nowWithOffset = new Date().valueOf() + offsetSeconds * 1000; /** @type {?} */ const tokenNotExpired = tokenExpirationValue > nowWithOffset; this.loggerService.logDebug(`Token not expired?: ${tokenExpirationValue} > ${nowWithOffset} (${tokenNotExpired})`); // Token not expired? return tokenNotExpired; } // iss // REQUIRED. Issuer Identifier for the Issuer of the response.The iss value is a case-sensitive URL using the https scheme that contains scheme, host, // and optionally, port number and path components and no query or fragment components. // // sub // REQUIRED. Subject Identifier.Locally unique and never reassigned identifier within the Issuer for the End- User, // which is intended to be consumed by the Client, e.g., 24400320 or AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. // It MUST NOT exceed 255 ASCII characters in length.The sub value is a case-sensitive string. // // aud // REQUIRED. Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client_id of the Relying Party as an audience value. // It MAY also contain identifiers for other audiences.In the general case, the aud value is an array of case-sensitive strings. // In the common special case when there is one audience, the aud value MAY be a single case-sensitive string. // // exp // REQUIRED. Expiration time on or after which the ID Token MUST NOT be accepted for processing. // The processing of this parameter requires that the current date/ time MUST be before the expiration date/ time listed in the value. // Implementers MAY provide for some small leeway, usually no more than a few minutes, to account for clock skew. // Its value is a JSON [RFC7159] number representing the number of seconds from 1970- 01 - 01T00: 00:00Z as measured in UTC until the date/ time. // See RFC 3339 [RFC3339] for details regarding date/ times in general and UTC in particular. // // iat // REQUIRED. Time at which the JWT was issued. Its value is a JSON number representing the number of seconds from 1970- 01 - 01T00: 00:00Z as measured // in UTC until the date/ time. /** * @param {?} dataIdToken * @return {?} */ validate_required_id_token(dataIdToken) { /** @type {?} */ let validated = true; if (!dataIdToken.hasOwnProperty('iss')) { validated = false; this.loggerService.logWarning('iss is missing, this is required in the id_token'); } if (!dataIdToken.hasOwnProperty('sub')) { validated = false; this.loggerService.logWarning('sub is missing, this is required in the id_token'); } if (!dataIdToken.hasOwnProperty('aud')) { validated = false; this.loggerService.logWarning('aud is missing, this is required in the id_token'); } if (!dataIdToken.hasOwnProperty('exp')) { validated = false; this.loggerService.logWarning('exp is missing, this is required in the id_token'); } if (!dataIdToken.hasOwnProperty('iat')) { validated = false; this.loggerService.logWarning('iat is missing, this is required in the id_token'); } return validated; } // id_token C8: The iat Claim can be used to reject tokens that were issued too far away from the current time, // limiting the amount of time that nonces need to be stored to prevent attacks.The acceptable range is Client specific. /** * @param {?} dataIdToken * @param {?} max_offset_allowed_in_seconds * @param {?} disable_iat_offset_validation * @return {?} */ validate_id_token_iat_max_offset(dataIdToken, max_offset_allowed_in_seconds, disable_iat_offset_validation) { if (disable_iat_offset_validation) { return true; } if (!dataIdToken.hasOwnProperty('iat')) { return false; } /** @type {?} */ const dateTime_iat_id_token = new Date(0); dateTime_iat_id_token.setUTCSeconds(dataIdToken.iat); max_offset_allowed_in_seconds = max_offset_allowed_in_seconds || 0; if (dateTime_iat_id_token == null) { return false; } this.loggerService.logDebug('validate_id_token_iat_max_offset: ' + (new Date().valueOf() - dateTime_iat_id_token.valueOf()) + ' < ' + max_offset_allowed_in_seconds * 1000); return new Date().valueOf() - dateTime_iat_id_token.valueOf() < max_offset_allowed_in_seconds * 1000; } // id_token C9: The value of the nonce Claim MUST be checked to verify that it is the same value as the one // that was sent in the Authentication Request.The Client SHOULD check the nonce value for replay attacks. // The precise method for detecting replay attacks is Client specific. /** * @param {?} dataIdToken * @param {?} local_nonce * @return {?} */ validate_id_token_nonce(dataIdToken, local_nonce) { /** @type {?} */ const isFromRefreshToken = dataIdToken.nonce === undefined && local_nonce === OidcSecurityValidation.RefreshTokenNoncePlaceholder; if (!isFromRefreshToken && dataIdToken.nonce !== local_nonce) { this.loggerService.logDebug('Validate_id_token_nonce failed, dataIdToken.nonce: ' + dataIdToken.nonce + ' local_nonce:' + local_nonce); return false; } return true; } // id_token C1: The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery) // MUST exactly match the value of the iss (issuer) Claim. /** * @param {?} dataIdToken * @param {?} authWellKnownEndpoints_issuer * @return {?} */ validate_id_token_iss(dataIdToken, authWellKnownEndpoints_issuer) { if (((/** @type {?} */ (dataIdToken.iss))) !== ((/** @type {?} */ (authWellKnownEndpoints_issuer)))) { this.loggerService.logDebug('Validate_id_token_iss failed, dataIdToken.iss: ' + dataIdToken.iss + ' authWellKnownEndpoints issuer:' + authWellKnownEndpoints_issuer); return false; } return true; } // id_token C2: The Client MUST validate that the aud (audience) Claim contains its client_id value registered at the Issuer identified // by the iss (issuer) Claim as an audience. // The ID Token MUST be rejected if the ID Token does not list the Client as a valid audience, or if it contains additional audiences // not trusted by the Client. /** * @param {?} dataIdToken * @param {?} aud * @return {?} */ validate_id_token_aud(dataIdToken, aud) { if (dataIdToken.aud instanceof Array) { /** @type {?} */ const result = this.arrayHelperService.areEqual(dataIdToken.aud, aud); if (!result) { this.loggerService.logDebug('Validate_id_token_aud array failed, dataIdToken.aud: ' + dataIdToken.aud + ' client_id:' + aud); return false; } return true; } else if (dataIdToken.aud !== aud) { this.loggerService.logDebug('Validate_id_token_aud failed, dataIdToken.aud: ' + dataIdToken.aud + ' client_id:' + aud); return false; } return true; } /** * @param {?} state * @param {?} local_state * @return {?} */ validateStateFromHashCallback(state, local_state) { if (((/** @type {?} */ (state))) !== ((/** @type {?} */ (local_state)))) { this.loggerService.logDebug('ValidateStateFromHashCallback failed, state: ' + state + ' local_state:' + local_state); return false; } return true; } /** * @param {?} id_token_sub * @param {?} userdata_sub * @return {?} */ validate_userdata_sub_id_token(id_token_sub, userdata_sub) { if (((/** @type {?} */ (id_token_sub))) !== ((/** @type {?} */ (userdata_sub)))) { this.loggerService.logDebug('validate_userdata_sub_id_token failed, id_token_sub: ' + id_token_sub + ' userdata_sub:' + userdata_sub); return false; } return true; } // id_token C5: The Client MUST validate the signature of the ID Token according to JWS [JWS] using the algorithm specified in the alg // Header Parameter of the JOSE Header.The Client MUST use the keys provided by the Issuer. // id_token C6: The alg value SHOULD be RS256. Validation of tokens using other signing algorithms is described in the // OpenID Connect Core 1.0 [OpenID.Core] specification. /** * @param {?} id_token * @param {?} jwtkeys * @return {?} */ validate_signature_id_token(id_token, jwtkeys) { if (!jwtkeys || !jwtkeys.keys) { return false; } /** @type {?} */ const header_data = this.tokenHelperService.getHeaderFromToken(id_token, false); if (Object.keys(header_data).length === 0 && header_data.constructor === Object) { this.loggerService.logWarning('id token has no header data'); return false; } /** @type {?} */ const kid = header_data.kid; /** @type {?} */ const alg = header_data.alg; if ('RS256' !== ((/** @type {?} */ (alg)))) { this.loggerService.logWarning('Only RS256 supported'); return false; } /** @type {?} */ let isValid = false; if (!header_data.hasOwnProperty('kid')) { // exactly 1 key in the jwtkeys and no kid in the Jose header // kty "RSA" use "sig" /** @type {?} */ let amountOfMatchingKeys = 0; for (const key of jwtkeys.keys) { if (((/** @type {?} */ (key.kty))) === 'RSA' && ((/** @type {?} */ (key.use))) === 'sig') { amountOfMatchingKeys = amountOfMatchingKeys + 1; } } if (amountOfMatchingKeys === 0) { this.loggerService.logWarning('no keys found, incorrect Signature, validation failed for id_token'); return false; } else if (amountOfMatchingKeys > 1) { this.loggerService.logWarning('no ID Token kid claim in JOSE header and multiple supplied in jwks_uri'); return false; } else { for (const key of jwtkeys.keys) { if (((/** @type {?} */ (key.kty))) === 'RSA' && ((/** @type {?} */ (key.use))) === 'sig') { /** @type {?} */ const publickey = KEYUTIL.getKey(key); isValid = KJUR.jws.JWS.verify(id_token, publickey, ['RS256']); if (!isValid) { this.loggerService.logWarning('incorrect Signature, validation failed for id_token'); } return isValid; } } } } else { // kid in the Jose header of id_token for (const key of jwtkeys.keys) { if (((/** @type {?} */ (key.kid))) === ((/** @type {?} */ (kid)))) { /** @type {?} */ const publickey = KEYUTIL.getKey(key); isValid = KJUR.jws.JWS.verify(id_token, publickey, ['RS256']); if (!isValid) { this.loggerService.logWarning('incorrect Signature, validation failed for id_token'); } return isValid; } } } return isValid; } /** * @param {?} response_type * @return {?} */ config_validate_response_type(response_type) { if (response_type === 'id_token token' || response_type === 'id_token') { return true; } if (response_type === 'code') { return true; } this.loggerService.logWarning('module configure incorrect, invalid response_type:' + response_type); return false; } // Accepts ID Token without 'kid' claim in JOSE header if only one JWK supplied in 'jwks_url' //// private validate_no_kid_in_header_only_one_allowed_in_jwtkeys(header_data: any, jwtkeys: any): boolean { //// this.oidcSecurityCommon.logDebug('amount of jwtkeys.keys: ' + jwtkeys.keys.length); //// if (!header_data.hasOwnProperty('kid')) { //// // no kid defined in Jose header //// if (jwtkeys.keys.length != 1) { //// this.oidcSecurityCommon.logDebug('jwtkeys.keys.length != 1 and no kid in header'); //// return false; //// } //// } //// return true; //// } // Access Token Validation // access_token C1: Hash the octets of the ASCII representation of the access_token with the hash algorithm specified in JWA[JWA] // for the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is RS256, the hash algorithm used is SHA-256. // access_token C2: Take the left- most half of the hash and base64url- encode it. // access_token C3: The value of at_hash in the ID Token MUST match the value produced in the previous step if at_hash // is present in the ID Token. /** * @param {?} access_token * @param {?} at_hash * @param {?} isCodeFlow * @return {?} */ validate_id_token_at_hash(access_token, at_hash, isCodeFlow) { this.loggerService.logDebug('at_hash from the server:' + at_hash); // The at_hash is optional for the code flow if (isCodeFlow) { if (!((/** @type {?} */ (at_hash)))) { this.loggerService.logDebug('Code Flow active, and no at_hash in the id_token, skipping check!'); return true; } } /** @type {?} */ const testdata = this.generate_at_hash('' + access_token); this.loggerService.logDebug('at_hash client validation not decoded:' + testdata); if (testdata === ((/** @type {?} */ (at_hash)))) { return true; // isValid; } else { /** @type {?} */ const testValue = this.generate_at_hash('' + decodeURIComponent(access_token)); this.loggerService.logDebug('-gen access--' + testValue); if (testValue === ((/** @type {?} */ (at_hash)))) { return true; // isValid } } return false; } /** * @private * @param {?} access_token * @return {?} */ generate_at_hash(access_token) { /** @type {?} */ const hash = KJUR.crypto.Util.hashString(access_token, 'sha256'); /** @type {?} */ const first128bits = hash.substr(0, hash.length / 2); /** @type {?} */ const testdata = hextob64u(first128bits); return testdata; } /** * @param {?} code_challenge * @return {?} */ generate_code_verifier(code_challenge) { /** @type {?} */ const hash = KJUR.crypto.Util.hashString(code_challenge, 'sha256'); /** @type {?} */ const testdata = hextob64u(hash); return testdata; } } OidcSecurityValidation.RefreshTokenNoncePlaceholder = '--RefreshToken--'; OidcSecurityValidation.decorators = [ { type: Injectable } ]; /** @nocollapse */ OidcSecurityValidation.ctorParameters = () => [ { type: EqualityHelperService }, { type: TokenHelperService }, { type: LoggerService } ]; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class StateValidationService { /** * @param {?} oidcSecurityCommon * @param {?} oidcSecurityValidation * @param {?} tokenHelperService * @param {?} loggerService * @param {?} configurationProvider */ constructor(oidcSecurityCommon, oidcSecurityValidation, tokenHelperService, loggerService, configurationProvider) { this.oidcSecurityCommon = oidcSecurityCommon; this.oidcSecurityValidation = oidcSecurityValidation; this.tokenHelperService = tokenHelperService; this.loggerService = loggerService; this.configurationProvider = configurationProvider; } /** * @param {?} result * @param {?} jwtKeys * @return {?} */ validateState(result, jwtKeys) { /** @type {?} */ const toReturn = new ValidateStateResult(); if (!this.oidcSecurityValidation.validateStateFromHashCallback(result.state, this.oidcSecurityCommon.authStateControl)) { this.loggerService.l