UNPKG

oidc-client-rx

Version:

ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications

213 lines (212 loc) 14.5 kB
import { Injectable, inject } from "injection-js"; import { of } from "rxjs"; import { map, mergeMap } from "rxjs/operators"; import { LoggerService } from "../logging/logger.service.js"; import { StoragePersistenceService } from "../storage/storage-persistence.service.js"; import { EqualityService } from "../utils/equality/equality.service.js"; import { FlowHelper } from "../utils/flowHelper/flow-helper.service.js"; import { TokenHelperService } from "../utils/tokenHelper/token-helper.service.js"; import { StateValidationResult } from "./state-validation-result.js"; import { TokenValidationService } from "./token-validation.service.js"; import { ValidationResult } from "./validation-result.js"; function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : null === desc ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if ("object" == typeof Reflect && "function" == typeof Reflect.decorate) r = Reflect.decorate(decorators, target, key, desc); else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } class StateValidationService { getValidatedStateResult(callbackContext, configuration) { const hasError = Boolean(callbackContext.authResult?.error); const hasCallbackContext = Boolean(callbackContext); if (!hasCallbackContext || hasError) return of(new StateValidationResult("", "", false, {})); return this.validateState(callbackContext, configuration); } validateState(callbackContext, configuration) { const toReturn = new StateValidationResult(); const authStateControl = this.storagePersistenceService.read("authStateControl", configuration); if (!this.tokenValidationService.validateStateFromHashCallback(callbackContext.authResult?.state, authStateControl, configuration)) { this.loggerService.logWarning(configuration, "authCallback incorrect state"); toReturn.state = ValidationResult.StatesDoNotMatch; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } const isCurrentFlowImplicitFlowWithAccessToken = this.flowHelper.isCurrentFlowImplicitFlowWithAccessToken(configuration); const isCurrentFlowCodeFlow = this.flowHelper.isCurrentFlowCodeFlow(configuration); if (isCurrentFlowImplicitFlowWithAccessToken || isCurrentFlowCodeFlow) toReturn.accessToken = callbackContext.authResult?.access_token ?? ""; const disableIdTokenValidation = configuration.disableIdTokenValidation; if (disableIdTokenValidation) { toReturn.state = ValidationResult.Ok; toReturn.authResponseIsValid = true; return of(toReturn); } const isInRefreshTokenFlow = callbackContext.isRenewProcess && !!callbackContext.refreshToken; const hasIdToken = Boolean(callbackContext.authResult?.id_token); if (isInRefreshTokenFlow && !hasIdToken) { toReturn.state = ValidationResult.Ok; toReturn.authResponseIsValid = true; return of(toReturn); } if (hasIdToken) { const { clientId, issValidationOff, maxIdTokenIatOffsetAllowedInSeconds, disableIatOffsetValidation, ignoreNonceAfterRefresh, renewTimeBeforeTokenExpiresInSeconds } = configuration; toReturn.idToken = callbackContext.authResult?.id_token ?? ""; toReturn.decodedIdToken = this.tokenHelperService.getPayloadFromToken(toReturn.idToken, false, configuration); return this.tokenValidationService.validateSignatureIdToken(toReturn.idToken, callbackContext.jwtKeys, configuration).pipe(mergeMap((isSignatureIdTokenValid)=>{ if (!isSignatureIdTokenValid) { this.loggerService.logDebug(configuration, "authCallback Signature validation failed id_token"); toReturn.state = ValidationResult.SignatureFailed; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } const authNonce = this.storagePersistenceService.read("authNonce", configuration); if (!this.tokenValidationService.validateIdTokenNonce(toReturn.decodedIdToken, authNonce, Boolean(ignoreNonceAfterRefresh), configuration)) { this.loggerService.logWarning(configuration, "authCallback incorrect nonce, did you call the checkAuth() method multiple times?"); toReturn.state = ValidationResult.IncorrectNonce; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (!this.tokenValidationService.validateRequiredIdToken(toReturn.decodedIdToken, configuration)) { this.loggerService.logDebug(configuration, "authCallback Validation, one of the REQUIRED properties missing from id_token"); toReturn.state = ValidationResult.RequiredPropertyMissing; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (!isInRefreshTokenFlow && !this.tokenValidationService.validateIdTokenIatMaxOffset(toReturn.decodedIdToken, maxIdTokenIatOffsetAllowedInSeconds ?? 120, Boolean(disableIatOffsetValidation), configuration)) { this.loggerService.logWarning(configuration, "authCallback Validation, iat rejected id_token was issued too far away from the current time"); toReturn.state = ValidationResult.MaxOffsetExpired; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } const authWellKnownEndPoints = this.storagePersistenceService.read("authWellKnownEndPoints", configuration); if (authWellKnownEndPoints) { if (issValidationOff) this.loggerService.logDebug(configuration, "iss validation is turned off, this is not recommended!"); else if (!issValidationOff && !this.tokenValidationService.validateIdTokenIss(toReturn.decodedIdToken, authWellKnownEndPoints.issuer, configuration)) { this.loggerService.logWarning(configuration, "authCallback incorrect iss does not match authWellKnownEndpoints issuer"); toReturn.state = ValidationResult.IssDoesNotMatchIssuer; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } } else { this.loggerService.logWarning(configuration, "authWellKnownEndpoints is undefined"); toReturn.state = ValidationResult.NoAuthWellKnownEndPoints; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (!this.tokenValidationService.validateIdTokenAud(toReturn.decodedIdToken, clientId, configuration)) { this.loggerService.logWarning(configuration, "authCallback incorrect aud"); toReturn.state = ValidationResult.IncorrectAud; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (!this.tokenValidationService.validateIdTokenAzpExistsIfMoreThanOneAud(toReturn.decodedIdToken)) { this.loggerService.logWarning(configuration, "authCallback missing azp"); toReturn.state = ValidationResult.IncorrectAzp; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (!this.tokenValidationService.validateIdTokenAzpValid(toReturn.decodedIdToken, clientId)) { this.loggerService.logWarning(configuration, "authCallback incorrect azp"); toReturn.state = ValidationResult.IncorrectAzp; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (!this.isIdTokenAfterRefreshTokenRequestValid(callbackContext, toReturn.decodedIdToken, configuration)) { this.loggerService.logWarning(configuration, "authCallback pre, post id_token claims do not match in refresh"); toReturn.state = ValidationResult.IncorrectIdTokenClaimsAfterRefresh; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (!isInRefreshTokenFlow && !this.tokenValidationService.validateIdTokenExpNotExpired(toReturn.decodedIdToken, configuration, renewTimeBeforeTokenExpiresInSeconds)) { this.loggerService.logWarning(configuration, "authCallback id token expired"); toReturn.state = ValidationResult.TokenExpired; this.handleUnsuccessfulValidation(configuration); return of(toReturn); } return this.validateDefault(isCurrentFlowImplicitFlowWithAccessToken, isCurrentFlowCodeFlow, toReturn, configuration, callbackContext); })); } this.loggerService.logDebug(configuration, "No id_token found, skipping id_token validation"); return this.validateDefault(isCurrentFlowImplicitFlowWithAccessToken, isCurrentFlowCodeFlow, toReturn, configuration, callbackContext); } validateDefault(isCurrentFlowImplicitFlowWithAccessToken, isCurrentFlowCodeFlow, toReturn, configuration, callbackContext) { if (!isCurrentFlowImplicitFlowWithAccessToken && !isCurrentFlowCodeFlow) { toReturn.authResponseIsValid = true; toReturn.state = ValidationResult.Ok; this.handleSuccessfulValidation(configuration); this.handleUnsuccessfulValidation(configuration); return of(toReturn); } if (callbackContext.authResult?.id_token) { const idTokenHeader = this.tokenHelperService.getHeaderFromToken(toReturn.idToken, false, configuration); if (!isCurrentFlowCodeFlow || !!toReturn.decodedIdToken.at_hash) return this.tokenValidationService.validateIdTokenAtHash(toReturn.accessToken, toReturn.decodedIdToken.at_hash, idTokenHeader.alg, configuration).pipe(map((valid)=>{ if (!valid || !toReturn.accessToken) { this.loggerService.logWarning(configuration, "authCallback incorrect at_hash"); toReturn.state = ValidationResult.IncorrectAtHash; this.handleUnsuccessfulValidation(configuration); return toReturn; } toReturn.authResponseIsValid = true; toReturn.state = ValidationResult.Ok; this.handleSuccessfulValidation(configuration); return toReturn; })); this.loggerService.logDebug(configuration, "Code Flow active, and no at_hash in the id_token, skipping check!"); } toReturn.authResponseIsValid = true; toReturn.state = ValidationResult.Ok; this.handleSuccessfulValidation(configuration); return of(toReturn); } isIdTokenAfterRefreshTokenRequestValid(callbackContext, newIdToken, configuration) { const { useRefreshToken, disableRefreshIdTokenAuthTimeValidation } = configuration; if (!useRefreshToken) return true; if (!callbackContext.existingIdToken) return true; const decodedIdToken = this.tokenHelperService.getPayloadFromToken(callbackContext.existingIdToken, false, configuration); if (decodedIdToken.iss !== newIdToken.iss) { this.loggerService.logDebug(configuration, `iss do not match: ${decodedIdToken.iss} ${newIdToken.iss}`); return false; } if (decodedIdToken.azp !== newIdToken.azp) { this.loggerService.logDebug(configuration, `azp do not match: ${decodedIdToken.azp} ${newIdToken.azp}`); return false; } if (decodedIdToken.sub !== newIdToken.sub) { this.loggerService.logDebug(configuration, `sub do not match: ${decodedIdToken.sub} ${newIdToken.sub}`); return false; } if (!this.equalityService.isStringEqualOrNonOrderedArrayEqual(decodedIdToken?.aud, newIdToken?.aud)) { this.loggerService.logDebug(configuration, `aud in new id_token is not valid: '${decodedIdToken?.aud}' '${newIdToken.aud}'`); return false; } if (disableRefreshIdTokenAuthTimeValidation) return true; if (decodedIdToken.auth_time !== newIdToken.auth_time) { this.loggerService.logDebug(configuration, `auth_time do not match: ${decodedIdToken.auth_time} ${newIdToken.auth_time}`); return false; } return true; } handleSuccessfulValidation(configuration) { const { autoCleanStateAfterAuthentication } = configuration; this.storagePersistenceService.write("authNonce", null, configuration); if (autoCleanStateAfterAuthentication) this.storagePersistenceService.write("authStateControl", "", configuration); this.loggerService.logDebug(configuration, "authCallback token(s) validated, continue"); } handleUnsuccessfulValidation(configuration) { const { autoCleanStateAfterAuthentication } = configuration; this.storagePersistenceService.write("authNonce", null, configuration); if (autoCleanStateAfterAuthentication) this.storagePersistenceService.write("authStateControl", "", configuration); this.loggerService.logDebug(configuration, "authCallback token(s) invalid"); } constructor(){ this.storagePersistenceService = inject(StoragePersistenceService); this.tokenValidationService = inject(TokenValidationService); this.tokenHelperService = inject(TokenHelperService); this.loggerService = inject(LoggerService); this.equalityService = inject(EqualityService); this.flowHelper = inject(FlowHelper); } } StateValidationService = _ts_decorate([ Injectable() ], StateValidationService); export { StateValidationService };