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 "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 };