oidc-client-rx
Version:
ReactiveX enhanced OIDC and OAuth2 protocol support for browser-based JavaScript applications
223 lines (222 loc) • 11.8 kB
JavaScript
import { Injectable, inject } from "@outposts/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 (!(null == dataIdToken ? void 0 : 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 };