angular-simple-oidc
Version:
Angular Library implementing Open Id Connect specification. Code Flow, Refresh Tokens, Session Management, Discovery Document.
1,219 lines (1,187 loc) • 67 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, APP_INITIALIZER, Optional, Injectable, Inject, NgModule } from '@angular/core';
import { isObservable, of, BehaviorSubject, throwError, combineLatest, Subject, merge, interval, fromEvent } from 'rxjs';
import { ConfigService } from 'angular-simple-oidc/config';
import { map, tap, catchError, switchMap, take, shareReplay, withLatestFrom, delay, takeUntil, filter, finalize, timeout } from 'rxjs/operators';
import * as i4 from 'angular-simple-oidc/events';
import { EventsService, SimpleOidcInfoEvent } from 'angular-simple-oidc/events';
import * as i6 from 'angular-simple-oidc/core';
import { SimpleOidcError, TokenStorageKeys, TokenValidationService, TokenHelperService, AuthorizationCallbackFormatError, TokenUrlService, RefreshTokenValidationService, AngularSimpleOidcCoreModule } from 'angular-simple-oidc/core';
import { HttpClient, HttpHeaders, HttpClientModule } from '@angular/common/http';
import { switchTap, filterInstanceOf } from 'angular-simple-oidc/operators';
import { __awaiter } from 'tslib';
const AUTH_CONFIG_REQUIRED_FIELDS = ['clientId', 'openIDProviderUrl'];
class TokenEndpointUnexpectedError extends SimpleOidcError {
constructor(context) {
super(`Token endpoint returned unexpected error`, `token-endpoint-unexpected-error`, context);
}
}
class TokenEndpointError extends SimpleOidcError {
constructor(error, context) {
super(`Token endpoint returned error: ${error}`, `token-endpoint-${error}`, context);
// TODO: Use errors from https://tools.ietf.org/html/rfc6749#section-5.2
}
}
class AuthenticationConfigurationMissingError extends SimpleOidcError {
constructor() {
super(`Expected AUTH_CONFIG to be in Injector.` +
`\nYou need to provide a configuration either with AngularSimpleOidc.forRoot() ` +
`or by adding your own (Observable<AuthConfig> | AuthConfig) ` +
`into the injector with the AUTH_CONFIG injection token.`, `auth-config-missing`, null);
}
}
class UserInfoNotSupportedError extends SimpleOidcError {
constructor(doc) {
super(`User Info is not supported, or it's URL was not in discovery document`, `user-info-not-suppo`, doc);
}
}
const WINDOW_REF = new InjectionToken('Angular Simple OIDC Window Reference');
const LOCAL_STORAGE_REF = new InjectionToken('Angular Simple OIDC LocalStorage Reference');
function localStorageFactory() {
return localStorage;
}
const LOCAL_STORAGE_PROVIDER = {
provide: LOCAL_STORAGE_REF,
useFactory: localStorageFactory
};
function windowFactory() {
return window;
}
const WINDOW_PROVIDER = {
provide: WINDOW_REF,
useFactory: windowFactory
};
// Configuration
const AUTH_CONFIG_SERVICE = new InjectionToken('AUTH_CONFIG');
const AUTH_CONFIG = new InjectionToken('AUTH_CONFIG');
const defaultConfig$2 = {
discoveryDocumentUrl: `/.well-known/openid-configuration`,
tokenCallbackRoute: 'oidc-token-callback',
tokenValidation: {
disableIdTokenIATValidation: false,
idTokenIATOffsetAllowed: 10 // seconds
},
enableAuthorizationCallbackAppInitializer: true
};
function getApplicationBaseUrl() {
const base = document.querySelector('base');
return base && base.href || '';
}
function authConfigFactory(configInput, configService, window, events) {
if (!configInput) {
throw new AuthenticationConfigurationMissingError();
}
const config$ = isObservable(configInput) ? configInput : of(configInput);
return () => config$.pipe(map(config => {
if (config && config.openIDProviderUrl) {
// do not modify the provided objects.
return Object.assign(Object.assign({}, config), { openIDProviderUrl: config.openIDProviderUrl.toLowerCase() });
}
return config;
}), tap(config => configService.configure(config, {
defaultConfig: Object.assign(Object.assign({}, defaultConfig$2), { baseUrl: getApplicationBaseUrl() }),
requiredFields: AUTH_CONFIG_REQUIRED_FIELDS
})), catchError(e => {
// make sure this errors get logged.
console.error('Callback failed in AUTH_CONFIG_INITIALIZER');
console.error(e);
events.dispatchError(e);
// Do not prevent bootstrapping in order to be able to handle errors gracefully.
return of(null);
}))
.toPromise();
}
const AUTH_CONFIG_INITIALIZER = {
multi: true,
provide: APP_INITIALIZER,
deps: [
[new Optional(), AUTH_CONFIG],
AUTH_CONFIG_SERVICE,
WINDOW_REF,
EventsService
],
useFactory: authConfigFactory
};
const AUTH_CONFIG_SERVICE_PROVIDER = {
provide: AUTH_CONFIG_SERVICE,
useClass: ConfigService,
};
// @dynamic
class TokenStorageService {
constructor(localStorage) {
this.localStorage = localStorage;
this.localStateSubject = new BehaviorSubject(this.getCurrentLocalState());
}
get currentState$() {
return this.localStateSubject.asObservable();
}
get storage() {
return this.localStorage;
}
storePreAuthorizationState(authState) {
this.storage.setItem(TokenStorageKeys.Nonce, authState.nonce);
this.storage.setItem(TokenStorageKeys.State, authState.state);
this.storage.setItem(TokenStorageKeys.CodeVerifier, authState.codeVerifier);
this.storage.setItem(TokenStorageKeys.PreRedirectUrl, authState.preRedirectUrl);
const state = this.getCurrentLocalState();
this.localStateSubject.next(state);
return of(state);
}
clearPreAuthorizationState() {
this.storage.removeItem(TokenStorageKeys.Nonce);
this.storage.removeItem(TokenStorageKeys.State);
this.storage.removeItem(TokenStorageKeys.CodeVerifier);
this.storage.removeItem(TokenStorageKeys.PreRedirectUrl);
const state = this.getCurrentLocalState();
this.localStateSubject.next(state);
return of(state);
}
storeAuthorizationCode(authorizationCode, sessionState) {
this.storage.setItem(TokenStorageKeys.AuthorizationCode, authorizationCode);
if (sessionState) {
this.storage.setItem(TokenStorageKeys.SessionState, sessionState);
}
const state = this.getCurrentLocalState();
this.localStateSubject.next(state);
return of(state);
}
storeOriginalIdToken(idToken) {
this.storage.setItem(TokenStorageKeys.OriginalIdentityToken, idToken);
const state = this.getCurrentLocalState();
this.localStateSubject.next(state);
return of(state);
}
storeTokens(tokens) {
this.storage.setItem(TokenStorageKeys.IdentityToken, tokens.idToken);
this.storeJSON(TokenStorageKeys.IdentityTokenDecoded, tokens.decodedIdToken);
this.storage.setItem(TokenStorageKeys.AccessToken, tokens.accessToken);
if (tokens.accessTokenExpiresAt) {
this.storage.setItem(TokenStorageKeys.AccessTokenExpiration, tokens.accessTokenExpiresAt.toString());
}
if (tokens.refreshToken) {
this.storage.setItem(TokenStorageKeys.RefreshToken, tokens.refreshToken);
}
const state = this.getCurrentLocalState();
this.localStateSubject.next(state);
return of(state);
}
removeAll() {
for (const k of Object.keys(TokenStorageKeys)) {
// We can't use clear since we could
// potentially delete keys which are not owned by us
this.storage.removeItem(TokenStorageKeys[k]);
}
return of(this.getCurrentLocalState());
}
getCurrentLocalState() {
const state = {
nonce: this.storage.getItem(TokenStorageKeys.Nonce),
state: this.storage.getItem(TokenStorageKeys.State),
codeVerifier: this.storage.getItem(TokenStorageKeys.CodeVerifier),
authorizationCode: this.storage.getItem(TokenStorageKeys.AuthorizationCode),
sessionState: this.storage.getItem(TokenStorageKeys.SessionState),
identityToken: this.storage.getItem(TokenStorageKeys.IdentityToken),
originalIdentityToken: this.storage.getItem(TokenStorageKeys.OriginalIdentityToken),
accessToken: this.storage.getItem(TokenStorageKeys.AccessToken),
accessTokenExpiration: parseInt(this.storage.getItem(TokenStorageKeys.AccessTokenExpiration), 10),
refreshToken: this.storage.getItem(TokenStorageKeys.RefreshToken),
preRedirectUrl: this.storage.getItem(TokenStorageKeys.PreRedirectUrl),
decodedIdentityToken: this.readJSON(TokenStorageKeys.IdentityTokenDecoded)
};
return state;
}
storeJSON(key, obj) {
this.storage.setItem(key, JSON.stringify(obj));
}
readJSON(key) {
const json = this.storage.getItem(key);
return json ? JSON.parse(json) : null;
}
}
TokenStorageService.decorators = [
{ type: Injectable }
];
TokenStorageService.ctorParameters = () => [
{ type: Storage, decorators: [{ type: Inject, args: [LOCAL_STORAGE_REF,] }] }
];
const defaultUrlRegExp = /^(\w+:\/\/[^/?]+)?(.*?)(\?.+)?$/;
const protocolRelativeUrlRegExp = /^(\/\/[^/?]+)(.*?)(\?.+)?$/;
function splitUrl(partsStr, { protocolRelative }) {
const match = (protocolRelative && partsStr.match(protocolRelativeUrlRegExp)) ||
partsStr.match(defaultUrlRegExp) ||
[];
const beforePathname = match[1] || '';
const pathname = (match[2] || '')
// Remove leading slashes
.replace(/^\/+/, '')
// Remove trailing slashes
.replace(/\/+$/, '')
// Normalize consecutive slashes to just one
.replace(/\/+/g, '/');
const afterPathname = (match[3] || '');
return { beforePathname, pathname, afterPathname };
}
function urlJoin(...parts) {
const lastArg = parts[parts.length - 1];
let options;
// If last argument is an object, then it's the options
// Note that null is an object, so we verify if is truthy
if (lastArg && typeof lastArg === 'object') {
options = lastArg;
parts = parts.slice(0, -1);
}
// Parse options
options = Object.assign({ leadingSlash: true, trailingSlash: false, protocolRelative: false }, options);
// Join parts
const partsStr = parts
.filter((part) => typeof part === 'string' || typeof part === 'number')
.join('/');
// Split the parts into beforePathname, pathname, and afterPathname
// (scheme://host)(/pathname)(?queryString)
const { beforePathname, pathname, afterPathname } = splitUrl(partsStr, options);
let url = '';
// Start with beforePathname if not empty (http://google.com)
if (beforePathname) {
url += beforePathname + (pathname ? '/' : '');
// Otherwise start with the leading slash
}
else if (options.leadingSlash) {
url += '/';
}
// Add pathname (foo/bar)
url += pathname;
// Add trailing slash
if (options.trailingSlash && !url.endsWith('/')) {
url += '/';
}
return url;
}
class ObtainDiscoveryDocumentError extends SimpleOidcError {
constructor(context) {
super('Failed to obtain discovery document', 'discovery-doc-fetch-failed', context);
}
}
class ObtainJWTKeysError extends SimpleOidcError {
constructor(context) {
super('Failed to obtain JWT Keys', 'jwt-keys-fetch-failed', context);
}
}
class DiscoveryDocumentObtainedEvent extends SimpleOidcInfoEvent {
constructor(discoveryDocument) {
super(`Discovery Document Obtained`, discoveryDocument);
}
}
class OidcDiscoveryDocClient {
constructor(config, http, events) {
this.config = config;
this.http = http;
this.events = events;
this.current$ = this.config.current$
.pipe(map(config => urlJoin(config.openIDProviderUrl, config.discoveryDocumentUrl)), tap(url => this.events.dispatch(new SimpleOidcInfoEvent('Obtaining discovery document', url))), switchMap(url => this.http.get(url)), tap(doc => this.events.dispatch(new DiscoveryDocumentObtainedEvent(doc))), catchError(e => throwError(new ObtainDiscoveryDocumentError(e))), take(1), shareReplay());
this.jwtKeys$ = this.current$
.pipe(tap(doc => this.events.dispatch(new SimpleOidcInfoEvent('Obtaining JWT Keys', doc.jwks_uri))), switchMap(doc => this.http.get(doc.jwks_uri)), tap(j => this.events.dispatch(new SimpleOidcInfoEvent('JWT Keys obtained', j))), catchError(e => throwError(new ObtainJWTKeysError(e))), take(1), shareReplay());
}
}
OidcDiscoveryDocClient.decorators = [
{ type: Injectable }
];
OidcDiscoveryDocClient.ctorParameters = () => [
{ type: ConfigService, decorators: [{ type: Inject, args: [AUTH_CONFIG_SERVICE,] }] },
{ type: HttpClient },
{ type: EventsService }
];
class TokensObtainedEvent extends SimpleOidcInfoEvent {
constructor(tokens) {
super(`Tokens obtained`, tokens);
}
}
class TokensValidatedEvent extends SimpleOidcInfoEvent {
constructor(tokens) {
super(`Tokens validated`, tokens);
}
}
class TokensReadyEvent extends SimpleOidcInfoEvent {
constructor(tokens) {
super(`Tokens are ready to be used (validated and stored)`, tokens);
}
}
class AccessTokenExpiredEvent extends SimpleOidcInfoEvent {
constructor(payload) {
super(`Access token has expired`, payload);
}
}
class AccessTokenExpiringEvent extends SimpleOidcInfoEvent {
constructor(payload) {
super(`Access token is almost expired`, payload);
}
}
class UserInfoObtainedEvent extends SimpleOidcInfoEvent {
constructor(payload) {
super(`Obtained User Profile`, payload);
}
}
class TokenEndpointClientService {
constructor(http, discoveryDocumentClient, tokenValidation, tokenHelper, events) {
this.http = http;
this.discoveryDocumentClient = discoveryDocumentClient;
this.tokenValidation = tokenValidation;
this.tokenHelper = tokenHelper;
this.events = events;
}
call(payload) {
const headers = new HttpHeaders()
.set('Content-Type', 'application/x-www-form-urlencoded');
return this.discoveryDocumentClient.current$
.pipe(take(1), switchMap(({ token_endpoint }) => {
this.events.dispatch(new SimpleOidcInfoEvent(`Executing Token Endpoint`, { url: token_endpoint, payload }));
return this.http.post(token_endpoint, payload, { headers: headers });
}), tap({
error: (e) => {
if (e instanceof SimpleOidcError) {
return;
}
if (e.status === 400) {
// https://tools.ietf.org/html/rfc6749#section-5.2
throw new TokenEndpointError(e.error.error, e);
}
else {
throw new TokenEndpointUnexpectedError(e);
}
}
}), map(response => {
let expiresAt;
if (response.expires_in) {
expiresAt = this.tokenHelper.getExpirationFromExpiresIn(response.expires_in);
}
else {
this.events.dispatch(new SimpleOidcInfoEvent(`Token Response did not contain expires_in`, response));
}
let decodedToken;
if (response.id_token) {
this.events.dispatch(new SimpleOidcInfoEvent(`Validating Identity Token format`, response.id_token));
this.tokenValidation.validateIdTokenFormat(response.id_token);
decodedToken = this.tokenHelper.getPayloadFromToken(response.id_token);
this.events.dispatch(new SimpleOidcInfoEvent(`Identity Token Payload decoded`, decodedToken));
}
else {
this.events.dispatch(new SimpleOidcInfoEvent(`Token Response did not contain id_token`, response));
}
const result = {
accessToken: response.access_token,
accessTokenExpiresIn: response.expires_in,
accessTokenExpiresAt: expiresAt ? expiresAt.getTime() : null,
error: response.error,
idToken: response.id_token,
refreshToken: response.refresh_token,
decodedIdToken: decodedToken
};
this.events.dispatch(new TokensObtainedEvent(result));
return result;
}));
}
}
TokenEndpointClientService.decorators = [
{ type: Injectable }
];
TokenEndpointClientService.ctorParameters = () => [
{ type: HttpClient },
{ type: OidcDiscoveryDocClient },
{ type: TokenValidationService },
{ type: TokenHelperService },
{ type: EventsService }
];
class DynamicIframe {
constructor(document) {
this.document = document;
this.handle = this.document.createElement('iframe');
}
appendTo(e) {
e.appendChild(this.handle);
return this;
}
appendToBody() {
this.appendTo(this.document.body);
return this;
}
setSource(url) {
this.handle.src = url;
return this;
}
hide() {
this.handle.style.display = 'none';
return this;
}
postMessage(msg, origin) {
this.handle.contentWindow.postMessage(msg, origin);
return this;
}
remove() {
// iframe may not have been appended.
if (this.handle.parentElement) {
this.handle.parentElement.removeChild(this.handle);
}
return this;
}
}
// @dynamic
class DynamicIframeService {
constructor(window) {
this.window = window;
this.pending = [];
}
create() {
const frame = new DynamicIframe(this.window.document);
this.pending.push(frame);
return frame;
}
}
DynamicIframeService.ɵprov = i0.ɵɵdefineInjectable({ factory: function DynamicIframeService_Factory() { return new DynamicIframeService(i0.ɵɵinject(WINDOW_REF)); }, token: DynamicIframeService, providedIn: "root" });
DynamicIframeService.decorators = [
{ type: Injectable, args: [{
providedIn: 'root'
},] }
];
DynamicIframeService.ctorParameters = () => [
{ type: Window, decorators: [{ type: Inject, args: [WINDOW_REF,] }] }
];
// @dynamic
class OidcCodeFlowClient {
constructor(window, config, discoveryDocumentClient, tokenStorage, tokenValidation, tokenUrl, tokenEndpointClient, events, dynamicIframe) {
this.window = window;
this.config = config;
this.discoveryDocumentClient = discoveryDocumentClient;
this.tokenStorage = tokenStorage;
this.tokenValidation = tokenValidation;
this.tokenUrl = tokenUrl;
this.tokenEndpointClient = tokenEndpointClient;
this.events = events;
this.dynamicIframe = dynamicIframe;
}
startCodeFlow(options = {}) {
const opts = Object.assign({}, options);
if (!opts.returnUrlAfterCallback) {
opts.returnUrlAfterCallback = this.window.location.href;
}
return this.config.current$
.pipe(map(config => urlJoin(config.baseUrl, config.tokenCallbackRoute)), switchMap(redirectUri => this.generateCodeFlowMetadata({ redirectUri })), tap(() => this.events.dispatch(new SimpleOidcInfoEvent(`Starting Code Flow`))), switchMap((result) => {
this.events.dispatch(new SimpleOidcInfoEvent(`Authorize URL generated`, result));
return this.tokenStorage.storePreAuthorizationState({
nonce: result.nonce,
state: result.state,
codeVerifier: result.codeVerifier,
preRedirectUrl: opts.returnUrlAfterCallback
}).pipe(tap((state) => {
this.events.dispatch(new SimpleOidcInfoEvent(`Pre-authorize state stored`, state));
this.redirectToUrl(result.url);
}));
}), take(1));
}
generateCodeFlowMetadata(params) {
return this.discoveryDocumentClient.current$
.pipe(withLatestFrom(this.config.current$), map(([discoveryDocument, config]) => this.tokenUrl.createAuthorizeUrl(discoveryDocument.authorization_endpoint, Object.assign(Object.assign({ clientId: config.clientId, scope: config.scope, responseType: 'code' }, config.acrValues && { acrValues: config.acrValues }), params))), take(1));
}
parseCodeFlowCallbackParams(href) {
try {
const result = this.tokenUrl.parseAuthorizeCallbackParamsFromUrl(href);
return Object.assign(Object.assign({}, result), { href });
}
catch (error) {
throw new AuthorizationCallbackFormatError(error);
}
}
validateCodeFlowCallback(params, localState) {
const { href, code, state, error } = params;
this.events.dispatch(new SimpleOidcInfoEvent(`Validating URL params`, { code, state, error, href }));
this.tokenValidation.validateAuthorizeCallbackFormat(code, state, error, href);
this.events.dispatch(new SimpleOidcInfoEvent(`Validating state vs local state`, { localState, state }));
this.tokenValidation.validateAuthorizeCallbackState(localState, state);
this.events.dispatch(new SimpleOidcInfoEvent(`Obtained authorization code.`, { code, state }));
}
codeFlowCallback(href, redirectUri, metadata) {
const params = this.parseCodeFlowCallbackParams(href);
this.validateCodeFlowCallback(params, metadata.state);
return this.tokenStorage.storeAuthorizationCode(params.code, params.sessionState)
.pipe(switchMap(() => this.config.current$), switchMap(authConfig => {
const payload = this.tokenUrl.createAuthorizationCodeRequestPayload({
clientId: authConfig.clientId,
clientSecret: authConfig.clientSecret,
scope: authConfig.scope,
redirectUri,
code: params.code,
codeVerifier: metadata.codeVerifier,
});
return this.requestTokenWithAuthCode(payload, metadata.nonce);
}));
}
currentWindowCodeFlowCallback() {
return this.tokenStorage.currentState$
.pipe(take(1), // we will be trigger currentState$ below
tap(() => this.events.dispatch(new SimpleOidcInfoEvent(`Starting Code Flow callback`))), withLatestFrom(this.config.current$), map(([localState, config]) => {
const redirectUri = urlJoin(config.baseUrl, config.tokenCallbackRoute);
return {
localState,
redirectUri
};
}), switchMap(({ localState, redirectUri }) => this.codeFlowCallback(this.window.location.href, redirectUri, localState).pipe(tap(() => this.historyChangeUrl(localState.preRedirectUrl)))), take(1));
}
requestTokenWithAuthCode(payload, nonce) {
// The discovery document for issuer
const discoveryDocument$ = this.discoveryDocumentClient.current$
.pipe(take(1));
// JWT Keys to validate id token signature
const jwtKeys$ = this.discoveryDocumentClient.jwtKeys$
.pipe(take(1));
return this.tokenEndpointClient.call(payload)
.pipe(tap(() => this.events.dispatch(new SimpleOidcInfoEvent(`Requesting token using authorization code`, payload))), withLatestFrom(discoveryDocument$, jwtKeys$, this.config.current$), take(1), tap(([result, discoveryDocument, jwtKeys, config]) => {
this.events.dispatch(new SimpleOidcInfoEvent('Validating identity token..', {
result, nonce, discoveryDocument, jwtKeys
}));
this.tokenValidation.validateIdToken(config.clientId, result.idToken, result.decodedIdToken, nonce, discoveryDocument, jwtKeys, config.tokenValidation);
}), tap(([result]) => {
this.events.dispatch(new SimpleOidcInfoEvent('Validating access token..', result));
this.tokenValidation.validateAccessToken(result.accessToken, result.decodedIdToken.at_hash);
}), switchTap(() => {
this.events.dispatch(new SimpleOidcInfoEvent('Clearing pre-authorize state..'));
return this.tokenStorage.clearPreAuthorizationState();
}), switchTap(([result]) => {
this.events.dispatch(new TokensValidatedEvent(result));
this.events.dispatch(new SimpleOidcInfoEvent('Storing tokens..', result));
return this.tokenStorage.storeTokens(result);
}), switchTap(([result]) => {
this.events.dispatch(new SimpleOidcInfoEvent('Storing original Identity Token..', result.idToken));
return this.tokenStorage.storeOriginalIdToken(result.idToken);
}), map(([result]) => result), tap((result) => this.events.dispatch(new TokensReadyEvent(result))));
}
redirectToUrl(url) {
this.events.dispatch(new SimpleOidcInfoEvent(`Redirecting`, url));
this.window.location.href = url;
}
historyChangeUrl(url) {
if (this.window.history) {
this.events.dispatch(new SimpleOidcInfoEvent(`Changing URL with history API`, url));
this.window.history.pushState({}, null, url);
}
else {
this.redirectToUrl(url);
}
}
}
OidcCodeFlowClient.decorators = [
{ type: Injectable }
];
OidcCodeFlowClient.ctorParameters = () => [
{ type: Window, decorators: [{ type: Inject, args: [WINDOW_REF,] }] },
{ type: ConfigService, decorators: [{ type: Inject, args: [AUTH_CONFIG_SERVICE,] }] },
{ type: OidcDiscoveryDocClient },
{ type: TokenStorageService },
{ type: TokenValidationService },
{ type: TokenUrlService },
{ type: TokenEndpointClientService },
{ type: EventsService },
{ type: DynamicIframeService }
];
// This APP_INITIALIZER makes sure the OAuthService is ready
// must be an export function for AOT to work
function simpleOidcInitializer(configService, oidcCodeFlowClient, events, window) {
return () => {
return configService.current$
.pipe(take(1), switchMap(config => {
if (config.enableAuthorizationCallbackAppInitializer
&& window.location.pathname.includes(config.tokenCallbackRoute)) {
return oidcCodeFlowClient.currentWindowCodeFlowCallback();
}
else {
return of(null);
}
}), catchError(e => {
// make sure this errors get logged.
console.error('Callback failed in APP_INITIALIZER');
console.error(e);
events.dispatchError(e);
// Do not prevent bootstrapping in order to be able to handle errors gracefully.
return of(null);
}))
.toPromise();
};
}
const SIMPLE_OIDC_APP_INITIALIZER = {
provide: APP_INITIALIZER,
useFactory: simpleOidcInitializer,
deps: [
AUTH_CONFIG_SERVICE,
OidcCodeFlowClient,
EventsService,
WINDOW_REF
],
multi: true,
};
class RefreshTokenClient {
constructor(config, tokenStorage, tokenUrl, tokenHelper, tokenEndpointClient, refreshTokenValidation, tokenValidation, events) {
this.config = config;
this.tokenStorage = tokenStorage;
this.tokenUrl = tokenUrl;
this.tokenHelper = tokenHelper;
this.tokenEndpointClient = tokenEndpointClient;
this.refreshTokenValidation = refreshTokenValidation;
this.tokenValidation = tokenValidation;
this.events = events;
}
requestTokenWithRefreshCode() {
return this.tokenStorage.currentState$.pipe(withLatestFrom(this.config.current$), take(1), switchMap(([localState, config]) => {
const payload = this.tokenUrl.createRefreshTokenRequestPayload({
clientId: config.clientId,
clientSecret: config.clientSecret,
refreshToken: localState.refreshToken
});
this.events.dispatch(new SimpleOidcInfoEvent(`Refreshing token using refresh code`, { payload, refreshToken: localState.refreshToken }));
return this.tokenEndpointClient.call(payload);
}), withLatestFrom(this.tokenStorage.currentState$), tap(([result, localState]) => {
const originalToken = this.tokenHelper.getPayloadFromToken(localState.originalIdentityToken);
this.events.dispatch(new SimpleOidcInfoEvent(`Validating new Identity Token against original`, { result, originalToken }));
this.refreshTokenValidation.validateIdToken(originalToken, result.decodedIdToken);
}), tap(([result]) => {
this.events.dispatch(new SimpleOidcInfoEvent(`Validating access token against at_hash`, { accessToken: result.accessToken, hash: result.decodedIdToken.at_hash }));
this.tokenValidation.validateAccessToken(result.accessToken, result.decodedIdToken.at_hash);
}), tap(([result]) => this.events.dispatch(new TokensValidatedEvent(result))), switchMap(([result]) => {
this.events.dispatch(new SimpleOidcInfoEvent(`Storing new tokens..`, result));
return this.tokenStorage.storeTokens(result)
.pipe(map(() => result));
}), tap((result) => this.events.dispatch(new TokensReadyEvent(result))));
}
}
RefreshTokenClient.decorators = [
{ type: Injectable }
];
RefreshTokenClient.ctorParameters = () => [
{ type: ConfigService, decorators: [{ type: Inject, args: [AUTH_CONFIG_SERVICE,] }] },
{ type: TokenStorageService },
{ type: TokenUrlService },
{ type: TokenHelperService },
{ type: TokenEndpointClientService },
{ type: RefreshTokenValidationService },
{ type: TokenValidationService },
{ type: EventsService }
];
// @dynamic
class EndSessionClientService {
constructor(window, discoveryDocumentClient, tokenUrl, tokenStorage, events) {
this.window = window;
this.discoveryDocumentClient = discoveryDocumentClient;
this.tokenUrl = tokenUrl;
this.tokenStorage = tokenStorage;
this.events = events;
}
logoutWithRedirect(postLogoutRedirectUri) {
const doc$ = this.discoveryDocumentClient.current$;
const localState$ = this.tokenStorage.currentState$;
return combineLatest(doc$, localState$)
.pipe(take(1), map(([doc, localState]) => this.tokenUrl.createEndSessionUrl(doc.end_session_endpoint, {
idTokenHint: localState.identityToken,
postLogoutRedirectUri
})), switchTap(() => {
this.events.dispatch(new SimpleOidcInfoEvent('Deleting Local Session'));
return this.tokenStorage.removeAll();
}), tap(({ url }) => {
this.events.dispatch(new SimpleOidcInfoEvent('Redirecting to End Session Endpoint', url));
this.window.location.href = url;
}));
}
}
EndSessionClientService.decorators = [
{ type: Injectable }
];
EndSessionClientService.ctorParameters = () => [
{ type: Window, decorators: [{ type: Inject, args: [WINDOW_REF,] }] },
{ type: OidcDiscoveryDocClient },
{ type: TokenUrlService },
{ type: TokenStorageService },
{ type: EventsService }
];
// @dynamic
class UserInfoClientService {
constructor(discoveryDocumentClient, tokenStorage, events, http) {
this.discoveryDocumentClient = discoveryDocumentClient;
this.tokenStorage = tokenStorage;
this.events = events;
this.http = http;
}
getUserInfo() {
const doc$ = this.discoveryDocumentClient.current$;
const localState$ = this.tokenStorage.currentState$;
return combineLatest(doc$, localState$)
.pipe(take(1), tap(() => this.events.dispatch(new SimpleOidcInfoEvent('Requesting User Info'))), tap(([doc]) => {
if (!doc.userinfo_endpoint) {
throw new UserInfoNotSupportedError(doc);
}
}), switchMap(([doc, localState]) => this.http.get(doc.userinfo_endpoint, {
headers: new HttpHeaders({ authorization: `Bearer ${localState.accessToken}` })
})), tap(profile => this.events.dispatch(new UserInfoObtainedEvent(profile))));
}
}
UserInfoClientService.decorators = [
{ type: Injectable }
];
UserInfoClientService.ctorParameters = () => [
{ type: OidcDiscoveryDocClient },
{ type: TokenStorageService },
{ type: EventsService },
{ type: HttpClient }
];
class AuthService {
constructor(oidcClient, tokenHelper, tokenStorage, refreshTokenClient, endSessionClient, config, events, userInfoClient) {
this.oidcClient = oidcClient;
this.tokenHelper = tokenHelper;
this.tokenStorage = tokenStorage;
this.refreshTokenClient = refreshTokenClient;
this.endSessionClient = endSessionClient;
this.config = config;
this.events = events;
this.userInfoClient = userInfoClient;
this.userInfo$ = this.events$.pipe(filterInstanceOf(TokensReadyEvent), switchMap(() => this.userInfoClient.getUserInfo()), shareReplay());
}
get isLoggedIn$() {
return this.tokenStorage.currentState$
.pipe(map(({ accessToken, accessTokenExpiration }) => {
if (!accessToken || this.tokenHelper.isTokenExpired(accessTokenExpiration)) {
return false;
}
return true;
}));
}
get accessToken$() {
return this.tokenStorage.currentState$
.pipe(map(s => s.accessToken));
}
get tokenExpiration$() {
return this.tokenStorage.currentState$
.pipe(map(s => new Date(s.accessTokenExpiration)));
}
get refreshToken$() {
return this.tokenStorage.currentState$
.pipe(map(s => s.refreshToken));
}
get identityToken$() {
return this.tokenStorage.currentState$
.pipe(map(s => s.identityToken));
}
get identityTokenDecoded$() {
return this.tokenStorage.currentState$
.pipe(map(s => s.decodedIdentityToken));
}
get events$() {
return this.events.events$;
}
get errors$() {
return this.events.errors$;
}
startCodeFlow(options) {
return this.oidcClient.startCodeFlow(options)
.pipe(tap({ error: e => this.events.dispatchError(e) }));
}
refreshAccessToken() {
return this.refreshTokenClient.requestTokenWithRefreshCode()
.pipe(tap({ error: e => this.events.dispatchError(e) }));
}
endSession(postLogoutRedirectUri) {
return this.config.current$
.pipe(take(1), switchMap(config => this.endSessionClient.logoutWithRedirect(postLogoutRedirectUri || config.baseUrl)), tap({ error: e => this.events.dispatchError(e) }));
}
}
AuthService.decorators = [
{ type: Injectable }
];
AuthService.ctorParameters = () => [
{ type: OidcCodeFlowClient },
{ type: TokenHelperService },
{ type: TokenStorageService },
{ type: RefreshTokenClient },
{ type: EndSessionClientService },
{ type: ConfigService, decorators: [{ type: Inject, args: [AUTH_CONFIG_SERVICE,] }] },
{ type: EventsService },
{ type: UserInfoClientService }
];
class AuthGuard {
constructor(auth, events) {
this.auth = auth;
this.events = events;
}
canActivate(next, state) {
return this.auth.isLoggedIn$
.pipe(take(1), switchMap(authenticated => {
if (!authenticated) {
this.events.dispatch(new SimpleOidcInfoEvent(`Route requires auth. No token or it's expired.`, { route: state.url }));
return this.auth.startCodeFlow({
returnUrlAfterCallback: state.url
})
// return false so that route change does not happen
.pipe(map(() => false));
}
return of(true);
}));
}
}
AuthGuard.decorators = [
{ type: Injectable }
];
AuthGuard.ctorParameters = () => [
{ type: AuthService },
{ type: EventsService }
];
class TokenExpirationDaemonService {
constructor(auth, events) {
this.auth = auth;
this.events = events;
this.destroyedSubject = new Subject();
}
startDaemon() {
this.watchTokenExpiration();
}
/**
* Sets timeouts based on the token expiration.
* Dispatches AccessTokenExpiringEvent before the access token expires (grace period)
* Dispatches AccessTokenExpiredEvent when the access token expires.
*/
watchTokenExpiration() {
this.auth.events$
.pipe(filterInstanceOf(TokensReadyEvent), switchMap(({ payload }) => {
if (payload.accessToken && payload.accessTokenExpiresAt) {
const expiration = new Date(payload.accessTokenExpiresAt);
// TODO: Expose as config?
const gracePeriod = 60;
const beforeExpiration = new Date(payload.accessTokenExpiresAt);
beforeExpiration.setSeconds(beforeExpiration.getSeconds() - gracePeriod);
const expiring$ = of(new AccessTokenExpiringEvent({
token: payload.accessToken,
expiresAt: expiration,
})).pipe(delay(beforeExpiration));
const expired$ = of(new AccessTokenExpiredEvent({
token: payload.accessToken,
expiredAt: expiration,
})).pipe(delay(expiration));
return merge(expiring$, expired$);
}
else {
return of(new SimpleOidcInfoEvent('TokenExpired event not configured due to access token or expiration empty.'));
}
}), takeUntil(this.destroyedSubject))
.subscribe((e) => {
if (e.payload) {
e.payload.now = new Date();
}
this.events.dispatch(e);
});
}
ngOnDestroy() {
this.destroyedSubject.next();
this.destroyedSubject.complete();
}
}
TokenExpirationDaemonService.decorators = [
{ type: Injectable }
];
TokenExpirationDaemonService.ctorParameters = () => [
{ type: AuthService },
{ type: EventsService }
];
class TokenFromStorageInitializerDaemonService {
constructor(events, storage, tokenHelper) {
this.events = events;
this.storage = storage;
this.tokenHelper = tokenHelper;
this.destroyedSubject = new Subject();
}
startDaemon() {
this.loadTokenFromStorage();
}
loadTokenFromStorage() {
return __awaiter(this, void 0, void 0, function* () {
const currentState = yield this.storage.currentState$
.pipe(take(1), takeUntil(this.destroyedSubject))
.toPromise();
if (!currentState.accessToken &&
!currentState.identityToken &&
!currentState.refreshToken) {
return;
}
if (!this.tokenHelper.isTokenExpired(currentState.accessTokenExpiration)) {
this.events.dispatch(new TokensReadyEvent({
accessToken: currentState.accessToken,
accessTokenExpiresAt: currentState.accessTokenExpiration,
decodedIdToken: currentState.decodedIdentityToken,
idToken: currentState.identityToken,
refreshToken: currentState.refreshToken,
}));
}
else {
this.events.dispatch(new SimpleOidcInfoEvent('Found token in storage but it\'s expired'));
}
});
}
ngOnDestroy() {
this.destroyedSubject.next();
this.destroyedSubject.complete();
}
}
TokenFromStorageInitializerDaemonService.decorators = [
{ type: Injectable }
];
TokenFromStorageInitializerDaemonService.ctorParameters = () => [
{ type: EventsService },
{ type: TokenStorageService },
{ type: TokenHelperService }
];
class TokenEventsModule {
constructor(tokenExpirationDaemonService, tokenFromStorageInitializerDaemonService) {
this.tokenExpirationDaemonService = tokenExpirationDaemonService;
this.tokenFromStorageInitializerDaemonService = tokenFromStorageInitializerDaemonService;
this.tokenExpirationDaemonService.startDaemon();
this.tokenFromStorageInitializerDaemonService.startDaemon();
}
}
TokenEventsModule.decorators = [
{ type: NgModule, args: [{
providers: [TokenExpirationDaemonService, TokenFromStorageInitializerDaemonService],
},] }
];
TokenEventsModule.ctorParameters = () => [
{ type: TokenExpirationDaemonService },
{ type: TokenFromStorageInitializerDaemonService }
];
class AngularSimpleOidcModule {
/**
* Should be called once on your Angular Root Application Module
*/
static forRoot(config) {
return {
ngModule: AngularSimpleOidcModule,
providers: [
config != null ? {
provide: AUTH_CONFIG,
useValue: config
} : [],
AUTH_CONFIG_SERVICE_PROVIDER,
AUTH_CONFIG_INITIALIZER,
SIMPLE_OIDC_APP_INITIALIZER,
]
};
}
}
AngularSimpleOidcModule.decorators = [
{ type: NgModule, args: [{
imports: [
HttpClientModule,
AngularSimpleOidcCoreModule,
TokenEventsModule
],
providers: [
WINDOW_PROVIDER,
LOCAL_STORAGE_PROVIDER,
TokenStorageService,
TokenEndpointClientService,
OidcDiscoveryDocClient,
OidcCodeFlowClient,
RefreshTokenClient,
EndSessionClientService,
UserInfoClientService,
AuthService,
AuthGuard,
],
declarations: [],
exports: [
AngularSimpleOidcCoreModule
]
},] }
];
class ExpiringTokenRefresherDaemonService {
constructor(auth, events) {
this.auth = auth;
this.events = events;
this.destroyedSubject = new Subject();
}
startDaemon() {
this.auth.events$
.pipe(filterInstanceOf(AccessTokenExpiringEvent), switchMap(() => this.auth.refreshAccessToken()), takeUntil(this.destroyedSubject))
.subscribe();
}
ngOnDestroy() {
this.destroyedSubject.next();
this.destroyedSubject.complete();
}
}
ExpiringTokenRefresherDaemonService.decorators = [
{ type: Injectable }
];
ExpiringTokenRefresherDaemonService.ctorParameters = () => [
{ type: AuthService },
{ type: EventsService }
];
class AutomaticRefreshModule {
constructor(daemon) {
this.daemon = daemon;
this.daemon.startDaemon();
}
}
AutomaticRefreshModule.decorators = [
{ type: NgModule, args: [{
providers: [ExpiringTokenRefresherDaemonService],
},] }
];
AutomaticRefreshModule.ctorParameters = () => [
{ type: ExpiringTokenRefresherDaemonService }
];
class SessionCheckNotSupportedError extends SimpleOidcError {
constructor(context) {
super(`Session check not supported by OP: check_session_iframe on discovery document and session_state are required`, `session-check-not-supported`, context);
}
}
class SessionCheckFailedError extends SimpleOidcError {
constructor(context) {
super(`OP iframe returned error. According to spec, message malformed?`, `session-check-error`, context);
}
}
class IframePostMessageTimeoutError extends SimpleOidcError {
constructor(context) {
super(`Iframe failed to postMessage back in given time.`, `iframe-post-message-timeout`, context);
}
}
class SessionManagementConfigurationMissingError extends SimpleOidcError {
constructor() {
super(`Expected SESSION_MANAGEMENT_CONFIG to be in Injector.` +
`\nYou need to provide a configuration either with SessionManagementModule.forRoot() ` +
`or by adding your own (Observable<SessionManagementConfig> | SessionManagementConfig) ` +
`into the injector with the SESSION_MANAGEMENT_CONFIG injection token.`, `session-management-config-missing`, null);
}
}
class SessionChangedEvent extends SimpleOidcInfoEvent {
constructor() {
super(`Session has changed.`);
}
}
class SessionTerminatedEvent extends SimpleOidcInfoEvent {
constructor(context) {
super(`Session has been terminated.`, context);
}
}
const SESSION_MANAGEMENT_CONFIG_REQUIRED_FIELDS = [
'iframePath'
];
const SESSION_MANAGEMENT_CONFIG_SERVICE = new InjectionToken('SESSION_MANAGEMENT_CONFIG_SERVICE');
const SESSION_MANAGEMENT_CONFIG = new InjectionToken('SESSION_MANAGEMENT_CONFIG');
const defaultConfig$1 = {
opIframePollInterval: 1 * 1000,
iframeTimeout: 10 * 1000
};
function sessionManagementConfigFactory(configInput, configService, events) {
if (!configInput) {
throw new SessionManagementConfigurationMissingError();
}
const config$ = isObservable(configInput) ? configInput : of(configInput);
return () => config$.pipe(tap(config => configService.configure(config, {
defaultConfig: defaultConfig$1,
requiredFields: SESSION_MANAGEMENT_CONFIG_REQUIRED_FIELDS
})), catchError(e => {
// make sure this errors get logged.
console.error('Callback failed in SESSION_MANAGEMENT_CONFIG_INITIALIZER');
console.error(e);
events.dispatchError(e);
// Do not prevent bootstrapping in order to be able to handle errors gracefully.
return of(null);
}))
.toPromise();
}
const SESSION_MANAGEMENT_CONFIG_INITIALIZER = {
multi: true,
provide: APP_INITIALIZER,
deps: [
[new Optional(), SESSION_MANAGEMENT_CONFIG],
SESSION_MANAGEMENT_CONFIG_SERVICE,
EventsService
],
useFactory: sessionManagementConfigFactory
};
const SESSION_MANAGEMENT_CONFIG_SERVICE_PROVIDER = {
provide: SESSION_MANAGEMENT_CONFIG_SERVICE,
useClass: ConfigService,
};
// @dynamic
class SessionCheckService {
constructor(window, discoveryClient, dynamicIframe, tokenStorage, config, sessionConfig, events) {
this.window = window;
this.discoveryClient = discoveryClient;
this.dynamicIframe = dynamicIframe;
this.tokenStorage = tokenStorage;
this.config = config;
this.sessionConfig = sessionConfig;
this.events = events;
this.destroyedSubject = new Subject();
}
ngOnDestroy() {
this.destroyedSubject.next();
this.destroyedSubject.complete();
}
startSessionCheck() {
const localState$ = this.tokenStorage.currentState$
.pipe(take(1));
const doc$ = this.discoveryClient.current$
.pipe(take(1));
const iframe = this.dynamicIframe
.create()
.hide();
return combineLatest(doc$, localState$)
.pipe(tap(() => this.events.dispatch(new SimpleOidcInfoEvent('Starting Session Check'))), map(([doc, localState]) => {
if (!doc.check_session_iframe || !localState.sessionState) {
throw new SessionCheckNotSupportedError({ doc, localState });
}
else {
iframe.setSource(doc.check_session_iframe)
.appendToBody();
return localState;
}
}), withLatestFrom(this.config.current$, this.sessionConfig.current$), take(1), switchMap(([localState, authConfig, sessionConfig]) => {
const expectedIframeOrigin = new URL(authConfig.openIDProviderUrl).origin;
const pollIframe$ = interval(sessionConfig.opIframePollInterval)
.pipe(map(() => `${authConfig.clientId.toLowerCase()} ${localState.sessionState}`), tap(msg => iframe.postMessage(msg, expectedIframeOrigin)));
const listen$ = fromEvent(this.window, 'message')
.pipe(filter((e) => e.origin === expectedIframeOrigin), map((e) => this.fireEventsFromMessage(e)), filter(msg => !!msg) // only the messages we care
);
return combineLatest(pollIframe$, listen$)
.pipe(map((arr) => arr[1]));
}), finalize(() => iframe.remove()), takeUntil(this.destroyedSubject.asObservable()));
}
/**
* The received data will either be changed or unchanged
* unless the syntax of the message sent was determined by the OP to be malformed,
* in which case the received data will be error.
* @param msg
*/
fireEventsFromMessage(msg) {
const result = msg.data;
switch (result) {
case 'changed':
this.events.dispatch(new SessionChangedEvent());
return result;
case 'unchanged':
return result;
case 'error':
throw new SessionCheckFailedError(msg);
default:
// If the message is not the one we expect, ignore it.
return null;
}
}
}
SessionCheckService.decorators = [
{ type: Injectable }
];
SessionCheckService.ctorParameters = () => [
{ type: Window, decorators: [{ type: Inject, args: [WINDOW_REF,] }] },
{ type: OidcDiscoveryDocClient },
{ type: DynamicIframeService },
{ type: TokenStorageService },
{ type: ConfigService, decorators: [{ type: Inject, args: [AUTH_CONFIG_SERVICE,] }] },
{ type: ConfigService, decorators: [{ type: Inject, args: [SESSION_MANAGEMENT_CONFIG_SERVICE,] }] },
{ type: EventsService }
];
// @dynamic
class AuthorizeEndpointSilentClientService {
constructor(window, discoveryClient, dynamicIframe, tokenStorage, authConfig, sessionConfig, events, oidcClient, tokenUrl) {