Files
openstapps/frontend/app/src/app/modules/auth/auth.provider.ts
T
Jovan Krunić ba728d551b feat: use only one identity provider
Additionally remove ionic-appauth dependency,
refactor the relevant code and fix some issues.

Closes #245
2026-10-18 16:10:13 +02:00

1046 lines
27 KiB
TypeScript

/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Injectable, inject} from '@angular/core';
import {Browser} from '@capacitor/browser';
import {Platform} from '@ionic/angular/standalone';
import {
AuthorizationError,
AuthorizationNotifier,
AuthorizationRequest,
AuthorizationRequestJson,
AuthorizationResponse,
AuthorizationServiceConfiguration,
AppAuthError,
BaseTokenRequestHandler,
DefaultCrypto,
ErrorType,
GRANT_TYPE_AUTHORIZATION_CODE,
GRANT_TYPE_REFRESH_TOKEN,
Requestor,
RevokeTokenRequest,
RevokeTokenRequestJson,
StorageBackend,
StringMap,
TokenRequest,
TokenRequestHandler,
TokenRequestJson,
TokenResponse,
} from '@openid/appauth';
import {SCAuthorizationProvider, SCAuthorizationProviderEndpoints} from '@openstapps/core';
import {BehaviorSubject, Observable} from 'rxjs';
import {ConfigProvider} from '../config/config.provider';
import {CapacitorAuthorizationRequestHandler} from './capacitor-authorization-request-handler';
import {getClientConfig, getServiceConfiguration} from './auth.provider.methods';
import {requestorFactory} from './factories/requestor.factory';
import {storageFactory} from './factories/storage.factory';
const TOKEN_RESPONSE_KEY = 'token_response';
export const INVALID_GRANT: ErrorType = 'invalid_grant';
/**
* Refresh at most this many seconds before the access token expires.
*/
const MAX_EARLY_REFRESH_SECONDS = 60;
/**
* Retry interval after a failed background session check (e.g. offline).
*/
const SESSION_CHECK_RETRY_MS = 60 * 1000;
/**
* Logout never waits longer than this for the IdP to revoke the tokens.
*/
const REVOKE_TIMEOUT_MS = 5000;
/**
* Largest delay setTimeout accepts (about 24.8 days).
*/
const MAX_TIMEOUT_MS = 2_147_483_647;
/**
* Returns the AppAuth isValid() buffer (negative seconds) for a token:
* refresh shortly before expiry, but never earlier than after three quarters
* of the token lifetime. A fixed buffer would make short-lived tokens
* (e.g. Shibboleth's default of 10 minutes) count as expired right away.
*/
export function earlyRefreshBuffer(token: TokenResponse): number {
const lifetime = token.expiresIn ?? 0;
if (lifetime <= 0) {
return 0;
}
return -Math.min(MAX_EARLY_REFRESH_SECONDS, Math.floor(lifetime / 4));
}
export interface AuthConfig {
server_host: string;
client_id: string;
redirect_url: string;
end_session_redirect_url?: string;
scopes: string;
pkce: boolean;
}
export enum AuthActions {
Init = 'Init',
SignInSuccess = 'SignInSuccess',
SignInFailed = 'SignInFailed',
SignOutSuccess = 'SignOutSuccess',
SignOutFailed = 'SignOutFailed',
RefreshSuccess = 'RefreshSuccess',
RefreshFailed = 'RefreshFailed',
LoadTokenFromStorageSuccess = 'LoadTokenFromStorageSuccess',
LoadTokenFromStorageFailed = 'LoadTokenFromStorageFailed',
RevokeTokensSuccess = 'RevokeTokensSuccess',
RevokeTokensFailed = 'RevokeTokensFailed',
LoadUserInfoSuccess = 'LoadUserInfoSuccess',
LoadUserInfoFailed = 'LoadUserInfoFailed',
/**
* The session can no longer be renewed: the access token expired without a
* refresh token (browser), or the IdP rejected the refresh token with
* invalid_grant (e.g. refresh token chain lifetime reached).
* The user is logged out locally.
*/
SessionExpired = 'SessionExpired',
}
export interface IAuthAction {
action: AuthActions;
tokenResponse?: TokenResponse;
user?: unknown;
error?: unknown;
}
/**
* Returns true if the token endpoint rejected the grant (RFC 6749, 5.2).
*
* AppAuth's BaseTokenRequestHandler only produces
* AppAuthError('invalid_grant') when the requestor resolves with the error
* body. Our requestors reject non-2xx responses themselves, so the OAuth error
* may also be found in the error's extras (the parsed response body).
*/
export function isInvalidGrant(error?: unknown): boolean {
if (!(error instanceof AppAuthError)) {
return false;
}
if (error.message === INVALID_GRANT) {
return true;
}
const extras = error.extras as {error?: unknown} | undefined;
return extras?.error === INVALID_GRANT;
}
@Injectable({
providedIn: 'root',
})
export class AuthProvider {
private readonly platform = inject(Platform);
private readonly configProvider = inject(ConfigProvider);
private authConfigValue?: AuthConfig;
private localConfiguration?: AuthorizationServiceConfiguration;
/**
* Endpoints from the backend configuration. AppAuth's
* AuthorizationServiceConfiguration has no userinfo / end-session.
*/
private endpoints?: SCAuthorizationProviderEndpoints;
private readonly authSubject = new BehaviorSubject<IAuthAction>({
action: AuthActions.Init,
});
private readonly tokenSubject = new BehaviorSubject<TokenResponse | undefined>(undefined);
private readonly userSubject = new BehaviorSubject<unknown>(undefined);
private readonly authenticatedSubject = new BehaviorSubject(false);
private readonly loggedInSubject = new BehaviorSubject(false);
private readonly initCompleteSubject = new BehaviorSubject(false);
private readonly storage: StorageBackend;
private readonly requestor: Requestor;
private readonly requestHandler: CapacitorAuthorizationRequestHandler;
private readonly tokenHandler: TokenRequestHandler;
private notifierInitialized = false;
/**
* Refresh request currently in progress. Parallel callers share it, so the
* same refresh token is never sent twice (with refresh token rotation the
* second request would fail with invalid_grant and log the user out).
*/
private refreshInFlight?: Promise<void>;
/**
* Fires when the current access token expires (or is due for refresh),
* so the UI does not keep showing a session that has already ended.
*/
private expiryTimer?: ReturnType<typeof setTimeout>;
private visibilityListenerInitialized = false;
constructor() {
this.storage = storageFactory(this.platform);
this.requestor = requestorFactory(this.platform);
/*
* Same handler on all platforms. On the web, the login is a page
* redirect and there is no in-app browser to close.
*/
this.requestHandler = this.platform.is('capacitor')
? new CapacitorAuthorizationRequestHandler(this.storage)
: new CapacitorAuthorizationRequestHandler(
this.storage,
async url => {
window.location.assign(url);
},
async () => {
// Nothing to close on the web.
},
);
this.tokenHandler = new BaseTokenRequestHandler(this.requestor);
/*
* Every new (or removed) token reschedules the expiry check.
*/
this.tokenSubject.subscribe(token => {
this.scheduleExpiryCheck(token);
});
}
get token$(): Observable<TokenResponse | undefined> {
return this.tokenSubject.asObservable();
}
get isAuthenticated$(): Observable<boolean> {
return this.authenticatedSubject.asObservable();
}
get isLoggedIn$(): Observable<boolean> {
return this.loggedInSubject.asObservable();
}
get initComplete$(): Observable<boolean> {
return this.initCompleteSubject.asObservable();
}
get user$(): Observable<unknown> {
return this.userSubject.asObservable();
}
get events$(): Observable<IAuthAction> {
return this.authSubject.asObservable();
}
get authConfig(): AuthConfig {
if (!this.authConfigValue) {
throw new Error('AuthConfig Not Defined');
}
return this.authConfigValue;
}
get configuration(): Promise<AuthorizationServiceConfiguration> {
if (!this.localConfiguration) {
throw new Error('Local Configuration Not Defined');
}
return Promise.resolve(this.localConfiguration);
}
public async init(): Promise<void> {
this.setupConfiguration();
this.setupAuthorizationNotifier();
this.setupVisibilityListener();
await this.loadTokenFromStorage();
}
private setupConfiguration(): void {
const config = this.configProvider.getAnyValue('auth') as {
default: SCAuthorizationProvider;
};
this.authConfigValue = getClientConfig(config);
this.localConfiguration = getServiceConfiguration(config);
this.endpoints = config.default.endpoints;
}
public async signIn(authExtras?: StringMap, state?: string): Promise<void> {
try {
const requestJson: AuthorizationRequestJson = {
response_type: AuthorizationRequest.RESPONSE_TYPE_CODE,
client_id: this.authConfig.client_id,
redirect_uri: this.authConfig.redirect_url,
scope: this.authConfig.scopes,
extras: authExtras,
state,
};
const request = new AuthorizationRequest(requestJson, new DefaultCrypto(), this.authConfig.pkce);
await this.requestHandler.performAuthorizationRequest(await this.configuration, request);
} catch (error) {
this.notify({
action: AuthActions.SignInFailed,
error,
});
}
}
public authorizationCallback(callbackUrl: string): void {
void this.handleAuthorizationCallback(callbackUrl).catch(error => {
this.notify({
action: AuthActions.SignInFailed,
error,
});
});
}
public async refreshToken(): Promise<void> {
try {
await this.refreshSession();
} catch {
/*
* Already handled by refreshSession(): the session is expired on
* invalid_grant and kept on temporary errors.
*/
}
}
public async loadUserInfo(): Promise<void> {
try {
const token = await this.getValidToken(0);
const endpoint = this.endpoints?.userinfo;
if (!endpoint) {
throw new Error('OIDC provider does not expose a userinfo endpoint.');
}
const user = await this.requestor.xhr<Record<string, unknown>>({
url: endpoint,
method: 'GET',
dataType: 'json',
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
});
this.notify({
action: AuthActions.LoadUserInfoSuccess,
user,
});
} catch (error) {
this.notify({
action: AuthActions.LoadUserInfoFailed,
error,
});
}
}
public async getEndSessionEndpoint(): Promise<string | undefined> {
return this.endpoints?.endSession;
}
/**
* Logs the user out of the app.
*
* With revokeTokens (default), the refresh and access tokens are also
* revoked at the IdP (RFC 7009), so a copied token cannot be used anymore.
* This is best effort: the local logout always happens first and never
* waits longer than REVOKE_TIMEOUT_MS for the IdP.
* @param _state Unused.
* @param revokeTokens Revoke the tokens at the IdP.
*/
public async signOut(_state?: string, revokeTokens = true): Promise<void> {
const token = this.tokenSubject.value;
try {
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
/*
* Local logout only.
*
* The optional logout from the identity provider is
* deliberately handled by AuthHelperService after the
* confirmation dialog.
*/
this.notify({
action: AuthActions.SignOutSuccess,
});
} catch (error) {
this.notify({
action: AuthActions.SignOutFailed,
error,
});
}
if (revokeTokens && token) {
try {
await this.withTimeout(this.revokeTokenResponse(token), REVOKE_TIMEOUT_MS);
} catch (error) {
/*
* Offline, IdP unreachable or revocation not allowed: the user is
* logged out locally anyway.
*/
this.notify({
action: AuthActions.RevokeTokensFailed,
error,
});
}
}
}
public async revokeTokens(): Promise<void> {
try {
await this.requestTokenRevoke();
this.notify({
action: AuthActions.RevokeTokensSuccess,
});
} catch (error) {
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
this.notify({
action: AuthActions.RevokeTokensFailed,
error,
});
}
}
public endSessionCallback(): void {
void this.internalEndSessionCallback().catch(error => {
this.notify({
action: AuthActions.SignOutFailed,
error,
});
});
}
public async loadTokenFromStorage(): Promise<void> {
try {
const stored = await this.storage.getItem(TOKEN_RESPONSE_KEY);
if (!stored) {
throw new Error('No Token In Storage');
}
const token = new TokenResponse(JSON.parse(stored));
/*
* Expired and not renewable (browser, no refresh token):
* this session is over, do not show the user as logged in.
*/
if (!token.isValid(0) && !token.refreshToken) {
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
throw new Error('Stored Token Expired');
}
this.notify({
action: AuthActions.LoadTokenFromStorageSuccess,
tokenResponse: token,
});
} catch (error) {
this.notify({
action: AuthActions.LoadTokenFromStorageFailed,
error,
});
}
}
/**
* Returns a usable access token, refreshing it first if it is expired
* or about to expire.
* @param buffer AppAuth isValid() buffer in (negative) seconds.
* Defaults to earlyRefreshBuffer() of the current token.
*/
public async getValidToken(buffer?: number): Promise<TokenResponse> {
const currentToken = this.tokenSubject.value;
/*
* Access token still valid for longer than the buffer.
*/
if (currentToken?.isValid(buffer ?? earlyRefreshBuffer(currentToken))) {
return currentToken;
}
if (!currentToken?.refreshToken) {
/*
* Without a refresh token (browser) nothing can be renewed early,
* so the buffer is pointless: use the token until it really expires.
*/
if (currentToken?.isValid(0)) {
return currentToken;
}
/*
* Expired and not renewable: the session is over.
*/
if (currentToken) {
await this.expireSession(new Error('Access Token Expired Without Refresh Token'));
}
throw new Error('Unable To Obtain Valid Token: No Refresh Token Available');
}
/*
* Access token is expired or will expire soon: refresh.
*
* refreshSession() stores the newly returned token and updates
* tokenSubject via RefreshSuccess. On invalid_grant it has already
* logged the user out.
*/
await this.refreshSession();
const refreshedToken = this.tokenSubject.value;
if (!refreshedToken) {
throw new Error('Unable To Obtain Valid Token: Refresh Returned No Token');
}
/*
* A freshly issued token is usable even if it is short-lived.
* Only reject one that is already expired.
*/
if (!refreshedToken.isValid(0)) {
throw new Error('Unable To Obtain Valid Token: Refreshed Token Is Already Expired');
}
return refreshedToken;
}
/**
* Refreshes the token once, shared by all parallel callers.
* Rejects with the original error after it has been handled.
*/
private refreshSession(): Promise<void> {
this.refreshInFlight ??= (async () => {
try {
await this.requestTokenRefresh();
} catch (error) {
await this.handleRefreshFailure(error);
throw error;
}
})().finally(() => {
this.refreshInFlight = undefined;
});
return this.refreshInFlight;
}
private async handleRefreshFailure(error: unknown): Promise<void> {
this.notify({
action: AuthActions.RefreshFailed,
error,
});
/*
* invalid_grant means that the refresh token is no longer usable,
* for example because it expired, was revoked or the refresh-token
* chain lifetime has been reached. Log the user out.
*
* Network and other temporary errors must not log the user out.
*/
if (isInvalidGrant(error)) {
await this.expireSession(error);
}
}
/**
* Ends the local session because it cannot be renewed anymore.
*/
private async expireSession(error: unknown): Promise<void> {
/*
* Already logged out, e.g. the expiry timer and a request noticed the
* expiry at the same time. Report the expiry only once.
*/
if (!this.tokenSubject.value) {
return;
}
/*
* Log out in memory first (synchronously), so parallel callers see
* the session as ended.
*/
this.notify({
action: AuthActions.SessionExpired,
error,
});
try {
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
} catch {
/*
* Still logged out in memory if removing the stored token fails.
*/
}
}
/**
* Schedules checkSession() for the moment the token expires
* (or is due for refresh when a refresh token is available).
*/
private scheduleExpiryCheck(token: TokenResponse | undefined): void {
if (this.expiryTimer) {
clearTimeout(this.expiryTimer);
this.expiryTimer = undefined;
}
if (!token?.expiresIn) {
return;
}
const buffer = token.refreshToken ? earlyRefreshBuffer(token) : 0;
const dueAtMs = (token.issuedAt + token.expiresIn + buffer) * 1000;
/*
* One extra second so that isValid() is definitely false when
* the timer fires.
*/
const delayMs = Math.min(Math.max(0, dueAtMs - Date.now()) + 1000, MAX_TIMEOUT_MS);
this.expiryTimer = setTimeout(() => {
this.expiryTimer = undefined;
void this.checkSession();
}, delayMs);
}
/**
* Refreshes the token or ends the session if it can no longer be used.
*
* Called by the expiry timer and when the app becomes visible again
* (browsers and mobile OSes pause timers in the background).
*/
private async checkSession(): Promise<void> {
if (!this.tokenSubject.value) {
return;
}
try {
/*
* Returns immediately if the token is still valid. Otherwise it
* refreshes, or logs out via SessionExpired.
*/
await this.getValidToken();
} catch {
/*
* Still logged in after a failure means a temporary error
* (e.g. offline): try again later.
*/
if (this.tokenSubject.value && !this.expiryTimer) {
this.expiryTimer = setTimeout(() => {
this.expiryTimer = undefined;
void this.checkSession();
}, SESSION_CHECK_RETRY_MS);
}
}
}
private setupVisibilityListener(): void {
if (this.visibilityListenerInitialized || typeof document === 'undefined') {
return;
}
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
void this.checkSession();
}
});
this.visibilityListenerInitialized = true;
}
private setupAuthorizationNotifier(): void {
if (this.notifierInitialized) {
return;
}
const notifier = new AuthorizationNotifier();
this.requestHandler.setAuthorizationNotifier(notifier);
notifier.setAuthorizationListener((request, response, error) => {
void this.onAuthorizationNotification(request, response, error).catch(notificationError => {
this.notify({
action: AuthActions.SignInFailed,
error: notificationError,
});
});
});
this.notifierInitialized = true;
}
private async onAuthorizationNotification(
request: AuthorizationRequest,
response: AuthorizationResponse | null,
error: AuthorizationError | null,
): Promise<void> {
if (error) {
throw new Error(error.errorDescription || error.error || 'Authorization failed.');
}
if (!response) {
throw new Error('Authorization response is missing.');
}
const codeVerifier = this.authConfig.pkce ? request.internal?.['code_verifier'] : undefined;
await this.requestAccessToken(response.code, codeVerifier);
}
private async requestAccessToken(code: string, codeVerifier?: string): Promise<void> {
const requestJson: TokenRequestJson = {
grant_type: GRANT_TYPE_AUTHORIZATION_CODE,
code,
refresh_token: undefined,
redirect_uri: this.authConfig.redirect_url,
client_id: this.authConfig.client_id,
extras: codeVerifier
? {
code_verifier: codeVerifier,
}
: undefined,
};
const token = await this.tokenHandler.performTokenRequest(
await this.configuration,
new TokenRequest(requestJson),
);
await this.storeToken(token);
this.notify({
action: AuthActions.SignInSuccess,
tokenResponse: token,
});
}
private async requestTokenRefresh(): Promise<void> {
const currentToken = this.tokenSubject.value;
if (!currentToken) {
throw new Error('No Token Defined!');
}
if (!currentToken.refreshToken) {
throw new Error('No Refresh Token Defined!');
}
const requestJson: TokenRequestJson = {
grant_type: GRANT_TYPE_REFRESH_TOKEN,
code: undefined,
refresh_token: currentToken.refreshToken,
redirect_uri: this.authConfig.redirect_url,
client_id: this.authConfig.client_id,
};
const response = await this.tokenHandler.performTokenRequest(
await this.configuration,
new TokenRequest(requestJson),
);
if (!response.accessToken) {
throw new Error('No Access Token Defined In Refresh Response');
}
const token = new TokenResponse({
...response.toJson(),
refresh_token: response.refreshToken ?? currentToken.refreshToken,
id_token: response.idToken ?? currentToken.idToken,
});
await this.storeToken(token);
this.notify({
action: AuthActions.RefreshSuccess,
tokenResponse: token,
});
}
private async requestTokenRevoke(): Promise<void> {
const token = this.tokenSubject.value;
if (!token) {
throw new Error('No Token Defined!');
}
await this.revokeTokenResponse(token);
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
}
/**
* Revokes the refresh token (if any) and the access token at the IdP.
*
* The refresh token goes first: it is the long-lived one. Both are tried
* even if the first request fails; the first error is rethrown.
* Does nothing if the backend configures no revocation endpoint.
*/
private async revokeTokenResponse(token: TokenResponse): Promise<void> {
const configuration = await this.configuration;
if (!configuration.revocationEndpoint) {
return;
}
const requests: RevokeTokenRequestJson[] = [];
if (token.refreshToken) {
requests.push({
token: token.refreshToken,
token_type_hint: 'refresh_token',
client_id: this.authConfig.client_id,
});
}
requests.push({
token: token.accessToken,
token_type_hint: 'access_token',
client_id: this.authConfig.client_id,
});
let firstError: unknown;
for (const request of requests) {
try {
await this.tokenHandler.performRevokeTokenRequest(
configuration,
new RevokeTokenRequest(request),
);
} catch (error) {
firstError ??= error;
}
}
if (firstError !== undefined) {
throw firstError;
}
}
/**
* Rejects if the promise does not settle within the given time.
*/
private withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<never>((_resolve, reject) => {
timer = setTimeout(() => {
reject(new Error('Token Revocation Timed Out'));
}, timeoutMs);
});
return Promise.race([promise, timeout]).finally(() => {
clearTimeout(timer);
});
}
private async handleAuthorizationCallback(callbackUrl: string): Promise<void> {
await this.requestHandler.completeAuthorizationRequestFromUrl(callbackUrl);
}
private async internalEndSessionCallback(): Promise<void> {
await this.closeBrowser();
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
this.notify({
action: AuthActions.SignOutSuccess,
});
}
private async closeBrowser(): Promise<void> {
if (!this.platform.is('capacitor')) {
return;
}
try {
await Browser.close();
} catch {
// Browser may already be closed.
}
}
private async storeToken(token: TokenResponse): Promise<void> {
await this.storage.setItem(
TOKEN_RESPONSE_KEY,
JSON.stringify(token.toJson()),
);
}
private notify(action: IAuthAction): void {
switch (action.action) {
case AuthActions.SignOutSuccess:
case AuthActions.SignOutFailed:
case AuthActions.SessionExpired: {
// eslint-disable-next-line unicorn/no-useless-undefined -- BehaviorSubject.next() requires an argument
this.tokenSubject.next(undefined);
// eslint-disable-next-line unicorn/no-useless-undefined -- BehaviorSubject.next() requires an argument
this.userSubject.next(undefined);
this.authenticatedSubject.next(false);
this.loggedInSubject.next(false);
break;
}
case AuthActions.LoadTokenFromStorageFailed: {
// eslint-disable-next-line unicorn/no-useless-undefined -- BehaviorSubject.next() requires an argument
this.tokenSubject.next(undefined);
// eslint-disable-next-line unicorn/no-useless-undefined -- BehaviorSubject.next() requires an argument
this.userSubject.next(undefined);
this.authenticatedSubject.next(false);
this.loggedInSubject.next(false);
this.initCompleteSubject.next(true);
break;
}
case AuthActions.SignInSuccess:
case AuthActions.RefreshSuccess: {
if (action.tokenResponse) {
this.tokenSubject.next(action.tokenResponse);
this.authenticatedSubject.next(true);
this.loggedInSubject.next(true);
}
break;
}
case AuthActions.LoadTokenFromStorageSuccess: {
if (action.tokenResponse) {
this.tokenSubject.next(action.tokenResponse);
this.authenticatedSubject.next(action.tokenResponse.isValid(0));
this.loggedInSubject.next(true);
}
this.initCompleteSubject.next(true);
break;
}
case AuthActions.RevokeTokensSuccess: {
// eslint-disable-next-line unicorn/no-useless-undefined -- BehaviorSubject.next() requires an argument
this.tokenSubject.next(undefined);
// eslint-disable-next-line unicorn/no-useless-undefined -- BehaviorSubject.next() requires an argument
this.userSubject.next(undefined);
this.authenticatedSubject.next(false);
this.loggedInSubject.next(false);
break;
}
case AuthActions.LoadUserInfoSuccess: {
this.userSubject.next(action.user);
break;
}
default: {
break;
}
}
this.authSubject.next(action);
}
}