mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2025-12-16 11:16:20 +00:00
committed by
Rainer Killinger
parent
5fdef95c06
commit
15ccbe4c18
546
src/app/modules/auth/auth.service.ts
Normal file
546
src/app/modules/auth/auth.service.ts
Normal file
@@ -0,0 +1,546 @@
|
||||
/*
|
||||
* Copyright (C) 2022 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/>.
|
||||
*/
|
||||
// Temporary use of direct file until the version with bug fix is released
|
||||
// https://github.com/wi3land/ionic-appauth/blob/3716f4fc6b5491b0b75e049be0a47a5af8c4da6f/src/auth-service.ts
|
||||
// Bug: https://github.com/wi3land/ionic-appauth/issues/154
|
||||
import {
|
||||
AuthorizationError,
|
||||
AuthorizationNotifier,
|
||||
AuthorizationRequest,
|
||||
AuthorizationRequestHandler,
|
||||
AuthorizationRequestJson,
|
||||
AuthorizationResponse,
|
||||
AuthorizationServiceConfiguration,
|
||||
BaseTokenRequestHandler,
|
||||
DefaultCrypto,
|
||||
GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
GRANT_TYPE_REFRESH_TOKEN,
|
||||
JQueryRequestor,
|
||||
LocalStorageBackend,
|
||||
Requestor,
|
||||
RevokeTokenRequest,
|
||||
RevokeTokenRequestJson,
|
||||
StorageBackend,
|
||||
StringMap,
|
||||
TokenRequest,
|
||||
TokenRequestHandler,
|
||||
TokenRequestJson,
|
||||
TokenResponse,
|
||||
} from '@openid/appauth';
|
||||
import {
|
||||
ActionHistoryObserver,
|
||||
AuthActionBuilder,
|
||||
AuthActions,
|
||||
AuthObserver,
|
||||
AUTHORIZATION_RESPONSE_KEY,
|
||||
AuthSubject,
|
||||
BaseAuthObserver,
|
||||
Browser,
|
||||
DefaultBrowser,
|
||||
EndSessionHandler,
|
||||
EndSessionRequest,
|
||||
EndSessionRequestJson,
|
||||
IAuthAction,
|
||||
IAuthConfig,
|
||||
IAuthService,
|
||||
IonicAuthorizationRequestHandler,
|
||||
IonicEndSessionHandler,
|
||||
IonicUserInfoHandler,
|
||||
SessionObserver,
|
||||
UserInfoHandler,
|
||||
} from 'ionic-appauth';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||
const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds
|
||||
|
||||
export abstract class AuthService implements IAuthService {
|
||||
private _configuration?: AuthorizationServiceConfiguration;
|
||||
|
||||
private _authConfig?: IAuthConfig;
|
||||
|
||||
private _authSubject: AuthSubject = new AuthSubject();
|
||||
|
||||
private _actionHistory: ActionHistoryObserver = new ActionHistoryObserver();
|
||||
|
||||
private _session: SessionObserver = new SessionObserver();
|
||||
|
||||
private _authSubjectV2 = new BehaviorSubject<IAuthAction>(
|
||||
AuthActionBuilder.Init(),
|
||||
);
|
||||
|
||||
private _tokenSubject = new BehaviorSubject<TokenResponse | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private _userSubject = new BehaviorSubject<any>(undefined);
|
||||
|
||||
private _authenticatedSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
private _initComplete = new BehaviorSubject<boolean>(false);
|
||||
|
||||
protected tokenHandler: TokenRequestHandler;
|
||||
|
||||
protected userInfoHandler: UserInfoHandler;
|
||||
|
||||
protected requestHandler: AuthorizationRequestHandler;
|
||||
|
||||
protected endSessionHandler: EndSessionHandler;
|
||||
|
||||
constructor(
|
||||
protected browser: Browser = new DefaultBrowser(),
|
||||
protected storage: StorageBackend = new LocalStorageBackend(),
|
||||
protected requestor: Requestor = new JQueryRequestor(),
|
||||
) {
|
||||
this.tokenHandler = new BaseTokenRequestHandler(requestor);
|
||||
this.userInfoHandler = new IonicUserInfoHandler(requestor);
|
||||
this.requestHandler = new IonicAuthorizationRequestHandler(
|
||||
browser,
|
||||
storage,
|
||||
);
|
||||
this.endSessionHandler = new IonicEndSessionHandler(browser);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
get history(): IAuthAction[] {
|
||||
return [...this._actionHistory.history];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
get session() {
|
||||
return this._session.session;
|
||||
}
|
||||
|
||||
get token$(): Observable<TokenResponse | undefined> {
|
||||
return this._tokenSubject.asObservable();
|
||||
}
|
||||
|
||||
get isAuthenticated$(): Observable<boolean> {
|
||||
return this._authenticatedSubject.asObservable();
|
||||
}
|
||||
|
||||
get initComplete$(): Observable<boolean> {
|
||||
return this._initComplete.asObservable();
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get user$(): Observable<any> {
|
||||
return this._userSubject.asObservable();
|
||||
}
|
||||
|
||||
get events$(): Observable<IAuthAction> {
|
||||
return this._authSubjectV2.asObservable();
|
||||
}
|
||||
|
||||
get authConfig(): IAuthConfig {
|
||||
if (!this._authConfig) throw new Error('AuthConfig Not Defined');
|
||||
|
||||
return this._authConfig;
|
||||
}
|
||||
|
||||
set authConfig(value: IAuthConfig) {
|
||||
this._authConfig = value;
|
||||
}
|
||||
|
||||
get configuration(): Promise<AuthorizationServiceConfiguration> {
|
||||
if (!this._configuration) {
|
||||
return AuthorizationServiceConfiguration.fetchFromIssuer(
|
||||
this.authConfig.server_host,
|
||||
this.requestor,
|
||||
).catch(() => {
|
||||
throw new Error('Unable To Obtain Server Configuration');
|
||||
});
|
||||
}
|
||||
|
||||
if (this._configuration != undefined) {
|
||||
return Promise.resolve(this._configuration);
|
||||
} else {
|
||||
throw new Error('Unable To Obtain Server Configuration');
|
||||
}
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.setupAuthorizationNotifier();
|
||||
this.loadTokenFromStorage();
|
||||
this.addActionObserver(this._actionHistory);
|
||||
this.addActionObserver(this._session);
|
||||
}
|
||||
|
||||
protected notifyActionListers(action: IAuthAction) {
|
||||
/* eslint-disable unicorn/no-useless-undefined */
|
||||
switch (action.action) {
|
||||
case AuthActions.RefreshFailed:
|
||||
case AuthActions.SignInFailed:
|
||||
case AuthActions.SignOutSuccess:
|
||||
case AuthActions.SignOutFailed:
|
||||
this._tokenSubject.next(undefined);
|
||||
this._userSubject.next(undefined);
|
||||
this._authenticatedSubject.next(false);
|
||||
break;
|
||||
case AuthActions.LoadTokenFromStorageFailed:
|
||||
this._tokenSubject.next(undefined);
|
||||
this._userSubject.next(undefined);
|
||||
this._authenticatedSubject.next(false);
|
||||
this._initComplete.next(true);
|
||||
break;
|
||||
case AuthActions.SignInSuccess:
|
||||
case AuthActions.RefreshSuccess:
|
||||
this._tokenSubject.next(action.tokenResponse);
|
||||
this._authenticatedSubject.next(true);
|
||||
break;
|
||||
case AuthActions.LoadTokenFromStorageSuccess:
|
||||
this._tokenSubject.next(action.tokenResponse);
|
||||
this._authenticatedSubject.next(
|
||||
(action.tokenResponse as TokenResponse).isValid(0),
|
||||
);
|
||||
this._initComplete.next(true);
|
||||
break;
|
||||
case AuthActions.RevokeTokensSuccess:
|
||||
this._tokenSubject.next(undefined);
|
||||
break;
|
||||
case AuthActions.LoadUserInfoSuccess:
|
||||
this._userSubject.next(action.user);
|
||||
break;
|
||||
case AuthActions.LoadUserInfoFailed:
|
||||
this._userSubject.next(undefined);
|
||||
break;
|
||||
}
|
||||
|
||||
this._authSubjectV2.next(action);
|
||||
this._authSubject.notify(action);
|
||||
}
|
||||
|
||||
protected setupAuthorizationNotifier() {
|
||||
const notifier = new AuthorizationNotifier();
|
||||
this.requestHandler.setAuthorizationNotifier(notifier);
|
||||
notifier.setAuthorizationListener((request, response, error) =>
|
||||
this.onAuthorizationNotification(request, response, error),
|
||||
);
|
||||
}
|
||||
|
||||
protected onAuthorizationNotification(
|
||||
request: AuthorizationRequest,
|
||||
response: AuthorizationResponse | null,
|
||||
error: AuthorizationError | null,
|
||||
) {
|
||||
const codeVerifier: string | undefined =
|
||||
request.internal != undefined && this.authConfig.pkce
|
||||
? request.internal.code_verifier
|
||||
: undefined;
|
||||
|
||||
if (response != undefined) {
|
||||
this.requestAccessToken(response.code, codeVerifier);
|
||||
} else if (error != undefined) {
|
||||
throw new Error(error.errorDescription);
|
||||
} else {
|
||||
throw new Error('Unknown Error With Authentication');
|
||||
}
|
||||
}
|
||||
|
||||
protected async internalAuthorizationCallback(url: string) {
|
||||
this.browser.closeWindow();
|
||||
await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, url);
|
||||
return this.requestHandler.completeAuthorizationRequestIfPossible();
|
||||
}
|
||||
|
||||
protected async internalEndSessionCallback() {
|
||||
this.browser.closeWindow();
|
||||
this._actionHistory.clear();
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutSuccess());
|
||||
}
|
||||
|
||||
protected async performEndSessionRequest(state?: string): Promise<void> {
|
||||
if (this._tokenSubject.value != undefined) {
|
||||
const requestJson: EndSessionRequestJson = {
|
||||
postLogoutRedirectURI: this.authConfig.end_session_redirect_url,
|
||||
idTokenHint: this._tokenSubject.value.idToken || '',
|
||||
state: state || undefined,
|
||||
};
|
||||
|
||||
const request: EndSessionRequest = new EndSessionRequest(requestJson);
|
||||
const returnedUrl: string | undefined =
|
||||
await this.endSessionHandler.performEndSessionRequest(
|
||||
await this.configuration,
|
||||
request,
|
||||
);
|
||||
|
||||
//callback may come from showWindow or via another method
|
||||
if (returnedUrl != undefined) {
|
||||
this.endSessionCallback();
|
||||
}
|
||||
} else {
|
||||
//if user has no token they should not be logged in in the first place
|
||||
this.endSessionCallback();
|
||||
}
|
||||
}
|
||||
|
||||
protected async performAuthorizationRequest(
|
||||
authExtras?: StringMap,
|
||||
state?: string,
|
||||
): Promise<void> {
|
||||
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: state || undefined,
|
||||
};
|
||||
|
||||
const request = new AuthorizationRequest(
|
||||
requestJson,
|
||||
new DefaultCrypto(),
|
||||
this.authConfig.pkce,
|
||||
);
|
||||
|
||||
if (this.authConfig.pkce) await request.setupCodeVerifier();
|
||||
|
||||
return this.requestHandler.performAuthorizationRequest(
|
||||
await this.configuration,
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
protected async requestAccessToken(
|
||||
code: string,
|
||||
codeVerifier?: string,
|
||||
): Promise<void> {
|
||||
const requestJSON: TokenRequestJson = {
|
||||
grant_type: GRANT_TYPE_AUTHORIZATION_CODE,
|
||||
code: code,
|
||||
refresh_token: undefined,
|
||||
redirect_uri: this.authConfig.redirect_url,
|
||||
client_id: this.authConfig.client_id,
|
||||
extras: codeVerifier
|
||||
? {
|
||||
code_verifier: codeVerifier,
|
||||
client_secret: this.authConfig.client_secret as string,
|
||||
}
|
||||
: {
|
||||
client_secret: this.authConfig.client_secret as string,
|
||||
},
|
||||
};
|
||||
|
||||
const token: TokenResponse = await this.tokenHandler.performTokenRequest(
|
||||
await this.configuration,
|
||||
new TokenRequest(requestJSON),
|
||||
);
|
||||
await this.storage.setItem(
|
||||
TOKEN_RESPONSE_KEY,
|
||||
JSON.stringify(token.toJson()),
|
||||
);
|
||||
this.notifyActionListers(AuthActionBuilder.SignInSuccess(token));
|
||||
}
|
||||
|
||||
protected async requestTokenRefresh() {
|
||||
if (!this._tokenSubject.value) {
|
||||
throw new Error('No Token Defined!');
|
||||
}
|
||||
|
||||
const requestJSON: TokenRequestJson = {
|
||||
grant_type: GRANT_TYPE_REFRESH_TOKEN,
|
||||
refresh_token: this._tokenSubject.value?.refreshToken,
|
||||
redirect_uri: this.authConfig.redirect_url,
|
||||
client_id: this.authConfig.client_id,
|
||||
};
|
||||
|
||||
const token: TokenResponse = await this.tokenHandler.performTokenRequest(
|
||||
await this.configuration,
|
||||
new TokenRequest(requestJSON),
|
||||
);
|
||||
await this.storage.setItem(
|
||||
TOKEN_RESPONSE_KEY,
|
||||
JSON.stringify(token.toJson()),
|
||||
);
|
||||
this.notifyActionListers(AuthActionBuilder.RefreshSuccess(token));
|
||||
}
|
||||
|
||||
protected async internalLoadTokenFromStorage() {
|
||||
let token: TokenResponse | undefined;
|
||||
const tokenResponseString: string | null = await this.storage.getItem(
|
||||
TOKEN_RESPONSE_KEY,
|
||||
);
|
||||
|
||||
if (tokenResponseString != undefined) {
|
||||
token = new TokenResponse(JSON.parse(tokenResponseString));
|
||||
|
||||
if (token) {
|
||||
return this.notifyActionListers(
|
||||
AuthActionBuilder.LoadTokenFromStorageSuccess(token),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No Token In Storage');
|
||||
}
|
||||
|
||||
protected async requestTokenRevoke() {
|
||||
const revokeRefreshJson: RevokeTokenRequestJson = {
|
||||
token: (this._tokenSubject.value as TokenResponse).refreshToken as string,
|
||||
token_type_hint: 'refresh_token',
|
||||
client_id: this.authConfig.client_id,
|
||||
};
|
||||
|
||||
const revokeAccessJson: RevokeTokenRequestJson = {
|
||||
token: (this._tokenSubject.value as TokenResponse).accessToken,
|
||||
token_type_hint: 'access_token',
|
||||
client_id: this.authConfig.client_id,
|
||||
};
|
||||
|
||||
await this.tokenHandler.performRevokeTokenRequest(
|
||||
await this.configuration,
|
||||
new RevokeTokenRequest(revokeRefreshJson),
|
||||
);
|
||||
await this.tokenHandler.performRevokeTokenRequest(
|
||||
await this.configuration,
|
||||
new RevokeTokenRequest(revokeAccessJson),
|
||||
);
|
||||
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
this.notifyActionListers(AuthActionBuilder.RevokeTokensSuccess());
|
||||
}
|
||||
|
||||
protected async internalRequestUserInfo() {
|
||||
if (this._tokenSubject.value) {
|
||||
const userInfo = await this.userInfoHandler.performUserInfoRequest(
|
||||
await this.configuration,
|
||||
this._tokenSubject.value,
|
||||
);
|
||||
this.notifyActionListers(AuthActionBuilder.LoadUserInfoSuccess(userInfo));
|
||||
} else {
|
||||
throw new Error('No Token Available');
|
||||
}
|
||||
}
|
||||
|
||||
public async loadTokenFromStorage() {
|
||||
await this.internalLoadTokenFromStorage().catch(error => {
|
||||
this.notifyActionListers(
|
||||
AuthActionBuilder.LoadTokenFromStorageFailed(error),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async signIn(authExtras?: StringMap, state?: string) {
|
||||
await this.performAuthorizationRequest(authExtras, state).catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignInFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async signOut(state?: string, revokeTokens?: boolean) {
|
||||
if (revokeTokens) {
|
||||
await this.revokeTokens();
|
||||
}
|
||||
|
||||
await this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
|
||||
if ((await this.configuration).endSessionEndpoint) {
|
||||
await this.performEndSessionRequest(state).catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutFailed(error));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async revokeTokens() {
|
||||
await this.requestTokenRevoke().catch(error => {
|
||||
this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
this.notifyActionListers(AuthActionBuilder.RevokeTokensFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async refreshToken() {
|
||||
await this.requestTokenRefresh().catch(error => {
|
||||
this.storage.removeItem(TOKEN_RESPONSE_KEY);
|
||||
this.notifyActionListers(AuthActionBuilder.RefreshFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async loadUserInfo() {
|
||||
await this.internalRequestUserInfo().catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.LoadUserInfoFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public authorizationCallback(callbackUrl: string): void {
|
||||
this.internalAuthorizationCallback(callbackUrl).catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignInFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public endSessionCallback(): void {
|
||||
this.internalEndSessionCallback().catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async getValidToken(
|
||||
buffer: number = AUTH_EXPIRY_BUFFER,
|
||||
): Promise<TokenResponse> {
|
||||
if (this._tokenSubject.value) {
|
||||
if (!this._tokenSubject.value.isValid(buffer)) {
|
||||
await this.refreshToken();
|
||||
if (this._tokenSubject.value) {
|
||||
return this._tokenSubject.value;
|
||||
}
|
||||
} else {
|
||||
return this._tokenSubject.value;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('Unable To Obtain Valid Token');
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
public addActionListener(
|
||||
function_: (action: IAuthAction) => void,
|
||||
): AuthObserver {
|
||||
const observer: AuthObserver = AuthObserver.Create(function_);
|
||||
this.addActionObserver(observer);
|
||||
return observer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
public addActionObserver(observer: BaseAuthObserver): void {
|
||||
if (this._actionHistory.lastAction) {
|
||||
observer.update(this._actionHistory.lastAction);
|
||||
}
|
||||
|
||||
this._authSubject.attach(observer);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated independant observers have been replaced by Rxjs
|
||||
* this will be removed in a future release
|
||||
* please use $ suffixed observers in future
|
||||
*/
|
||||
public removeActionObserver(observer: BaseAuthObserver): void {
|
||||
this._authSubject.detach(observer);
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,13 @@ import {
|
||||
EndSessionHandler,
|
||||
Browser,
|
||||
DefaultBrowser,
|
||||
AuthService,
|
||||
AuthActionBuilder,
|
||||
} from 'ionic-appauth';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {SCAuthorizationProvider} from '@openstapps/core';
|
||||
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
|
||||
import {Injectable} from '@angular/core';
|
||||
import {AuthService} from './auth.service';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user