mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-05 13:02:54 +00:00
516 lines
16 KiB
TypeScript
516 lines
16 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/>.
|
|
*/
|
|
// 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) {
|
|
throw new Error('Unable To Obtain Server Configuration');
|
|
} else {
|
|
return Promise.resolve(this._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('Unknown Error With Authentication');
|
|
} else {
|
|
throw new Error(error.errorDescription);
|
|
}
|
|
}
|
|
|
|
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) {
|
|
//if user has no token they should not be logged in in the first place
|
|
this.endSessionCallback();
|
|
} else {
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
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),
|
|
);
|
|
if (!token.accessToken) {
|
|
throw new Error('No Access Token Defined In Refresh Response');
|
|
}
|
|
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)) {
|
|
return this._tokenSubject.value;
|
|
} else {
|
|
await this.refreshToken();
|
|
if (this._tokenSubject.value) {
|
|
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);
|
|
}
|
|
}
|