UNPKG

oidc-client-rx

Version:

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

223 lines (222 loc) 11.8 kB
import { Injectable, inject } from "injection-js"; import { base64url } from "rfc4648"; import { from, of } from "rxjs"; import { map, mergeMap, tap } from "rxjs/operators"; import { JwkExtractor } from "../extractors/jwk.extractor.js"; import { LoggerService } from "../logging/logger.service.js"; import { TokenHelperService } from "../utils/tokenHelper/token-helper.service.js"; import { JwkWindowCryptoService } from "./jwk-window-crypto.service.js"; import { JwtWindowCryptoService } from "./jwt-window-crypto.service.js"; import { alg2kty, getImportAlg, getVerifyAlg } from "./token-validation.helper.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 TokenValidationService { hasIdTokenExpired(token, configuration, offsetSeconds) { const decoded = this.tokenHelperService.getPayloadFromToken(token, false, configuration); return !this.validateIdTokenExpNotExpired(decoded, configuration, offsetSeconds); } validateIdTokenExpNotExpired(decodedIdToken, configuration, offsetSeconds) { const tokenExpirationDate = this.tokenHelperService.getTokenExpirationDate(decodedIdToken); offsetSeconds = offsetSeconds || 0; if (!tokenExpirationDate) return false; const tokenExpirationValue = tokenExpirationDate.valueOf(); const nowWithOffset = this.calculateNowWithOffset(offsetSeconds); const tokenNotExpired = tokenExpirationValue > nowWithOffset; this.loggerService.logDebug(configuration, `Has idToken expired: ${!tokenNotExpired} --> expires in ${this.millisToMinutesAndSeconds(tokenExpirationValue - nowWithOffset)} , ${new Date(tokenExpirationValue).toLocaleTimeString()} > ${new Date(nowWithOffset).toLocaleTimeString()}`); return tokenNotExpired; } validateAccessTokenNotExpired(accessTokenExpiresAt, configuration, offsetSeconds) { if (!accessTokenExpiresAt) return true; offsetSeconds = offsetSeconds || 0; const accessTokenExpirationValue = accessTokenExpiresAt.valueOf(); const nowWithOffset = this.calculateNowWithOffset(offsetSeconds); const tokenNotExpired = accessTokenExpirationValue > nowWithOffset; this.loggerService.logDebug(configuration, `Has accessToken expired: ${!tokenNotExpired} --> expires in ${this.millisToMinutesAndSeconds(accessTokenExpirationValue - nowWithOffset)} , ${new Date(accessTokenExpirationValue).toLocaleTimeString()} > ${new Date(nowWithOffset).toLocaleTimeString()}`); return tokenNotExpired; } validateRequiredIdToken(dataIdToken, configuration) { let validated = true; if (!Object.prototype.hasOwnProperty.call(dataIdToken, "iss")) { validated = false; this.loggerService.logWarning(configuration, "iss is missing, this is required in the id_token"); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, "sub")) { validated = false; this.loggerService.logWarning(configuration, "sub is missing, this is required in the id_token"); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, "aud")) { validated = false; this.loggerService.logWarning(configuration, "aud is missing, this is required in the id_token"); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, "exp")) { validated = false; this.loggerService.logWarning(configuration, "exp is missing, this is required in the id_token"); } if (!Object.prototype.hasOwnProperty.call(dataIdToken, "iat")) { validated = false; this.loggerService.logWarning(configuration, "iat is missing, this is required in the id_token"); } return validated; } validateIdTokenIatMaxOffset(dataIdToken, maxOffsetAllowedInSeconds, disableIatOffsetValidation, configuration) { if (disableIatOffsetValidation) return true; if (!Object.prototype.hasOwnProperty.call(dataIdToken, "iat")) return false; const dateTimeIatIdToken = new Date(0); dateTimeIatIdToken.setUTCSeconds(dataIdToken.iat); maxOffsetAllowedInSeconds = maxOffsetAllowedInSeconds || 0; const nowInUtc = new Date(new Date().toUTCString()); const diff = nowInUtc.valueOf() - dateTimeIatIdToken.valueOf(); const maxOffsetAllowedInMilliseconds = 1000 * maxOffsetAllowedInSeconds; this.loggerService.logDebug(configuration, `validate id token iat max offset ${diff} < ${maxOffsetAllowedInMilliseconds}`); if (diff > 0) return diff < maxOffsetAllowedInMilliseconds; return -diff < maxOffsetAllowedInMilliseconds; } validateIdTokenNonce(dataIdToken, localNonce, ignoreNonceAfterRefresh, configuration) { const isFromRefreshToken = (void 0 === dataIdToken.nonce || ignoreNonceAfterRefresh) && localNonce === TokenValidationService.refreshTokenNoncePlaceholder; if (!isFromRefreshToken && dataIdToken.nonce !== localNonce) { this.loggerService.logDebug(configuration, `Validate_id_token_nonce failed, dataIdToken.nonce: ${dataIdToken.nonce} local_nonce:${localNonce}`); return false; } return true; } validateIdTokenIss(dataIdToken, authWellKnownEndpointsIssuer, configuration) { if (dataIdToken.iss !== authWellKnownEndpointsIssuer) { this.loggerService.logDebug(configuration, `Validate_id_token_iss failed, dataIdToken.iss: ${dataIdToken.iss} authWellKnownEndpoints issuer:${authWellKnownEndpointsIssuer}`); return false; } return true; } validateIdTokenAud(dataIdToken, aud, configuration) { if (Array.isArray(dataIdToken.aud)) { const result = dataIdToken.aud.includes(aud); if (!result) { this.loggerService.logDebug(configuration, `Validate_id_token_aud array failed, dataIdToken.aud: ${dataIdToken.aud} client_id:${aud}`); return false; } return true; } if (dataIdToken.aud !== aud) { this.loggerService.logDebug(configuration, `Validate_id_token_aud failed, dataIdToken.aud: ${dataIdToken.aud} client_id:${aud}`); return false; } return true; } validateIdTokenAzpExistsIfMoreThanOneAud(dataIdToken) { if (!dataIdToken) return false; return !(Array.isArray(dataIdToken.aud) && dataIdToken.aud.length > 1 && !dataIdToken.azp); } validateIdTokenAzpValid(dataIdToken, clientId) { if (!dataIdToken?.azp) return true; return dataIdToken.azp === clientId; } validateStateFromHashCallback(state, localState, configuration) { if (`${state}` !== `${localState}`) { this.loggerService.logDebug(configuration, `ValidateStateFromHashCallback failed, state: ${state} local_state:${localState}`); return false; } return true; } validateSignatureIdToken(idToken, jwtkeys, configuration) { if (!idToken) return of(true); if (!jwtkeys || !jwtkeys.keys) return of(false); const headerData = this.tokenHelperService.getHeaderFromToken(idToken, false, configuration); if (0 === Object.keys(headerData).length && headerData.constructor === Object) { this.loggerService.logWarning(configuration, "id token has no header data"); return of(false); } const kid = headerData.kid; const alg = headerData.alg; const keys = jwtkeys.keys; let foundKeys; let key; if (!this.keyAlgorithms.includes(alg)) { this.loggerService.logWarning(configuration, "alg not supported", alg); return of(false); } const kty = alg2kty(alg); const use = "sig"; try { foundKeys = kid ? this.jwkExtractor.extractJwk(keys, { kid, kty, use }, false) : this.jwkExtractor.extractJwk(keys, { kty, use }, false); if (0 === foundKeys.length) foundKeys = kid ? this.jwkExtractor.extractJwk(keys, { kid, kty }) : this.jwkExtractor.extractJwk(keys, { kty }); key = foundKeys[0]; } catch (e) { this.loggerService.logError(configuration, e); return of(false); } const algorithm = getImportAlg(alg); const signingInput = this.tokenHelperService.getSigningInputFromToken(idToken, true, configuration); const rawSignature = this.tokenHelperService.getSignatureFromToken(idToken, true, configuration); return from(this.jwkWindowCryptoService.importVerificationKey(key, algorithm)).pipe(mergeMap((cryptoKey)=>{ const signature = base64url.parse(rawSignature, { loose: true }); const verifyAlgorithm = getVerifyAlg(alg); return from(this.jwkWindowCryptoService.verifyKey(verifyAlgorithm, cryptoKey, signature, signingInput)); }), tap((isValid)=>{ if (!isValid) this.loggerService.logWarning(configuration, "incorrect Signature, validation failed for id_token"); })); } validateIdTokenAtHash(accessToken, atHash, idTokenAlg, configuration) { this.loggerService.logDebug(configuration, `at_hash from the server:${atHash}`); let sha = "SHA-256"; if (idTokenAlg.includes("384")) sha = "SHA-384"; else if (idTokenAlg.includes("512")) sha = "SHA-512"; return this.jwtWindowCryptoService.generateAtHash(`${accessToken}`, sha).pipe(mergeMap((hash)=>{ this.loggerService.logDebug(configuration, `at_hash client validation not decoded:${hash}`); if (hash === atHash) return of(true); return this.jwtWindowCryptoService.generateAtHash(`${decodeURIComponent(accessToken)}`, sha).pipe(map((newHash)=>{ this.loggerService.logDebug(configuration, `-gen access--${hash}`); return newHash === atHash; })); })); } millisToMinutesAndSeconds(millis) { const minutes = Math.floor(millis / 60000); const seconds = (millis % 60000 / 1000).toFixed(0); return `${minutes}:${+seconds < 10 ? "0" : ""}${seconds}`; } calculateNowWithOffset(offsetSeconds) { return new Date(new Date().toUTCString()).valueOf() + 1000 * offsetSeconds; } constructor(){ this.keyAlgorithms = [ "HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "PS256", "PS384", "PS512" ]; this.tokenHelperService = inject(TokenHelperService); this.loggerService = inject(LoggerService); this.jwkExtractor = inject(JwkExtractor); this.jwkWindowCryptoService = inject(JwkWindowCryptoService); this.jwtWindowCryptoService = inject(JwtWindowCryptoService); } } TokenValidationService.refreshTokenNoncePlaceholder = "--RefreshToken--"; TokenValidationService = _ts_decorate([ Injectable() ], TokenValidationService); export { TokenValidationService };