diff --git a/src/app/modules/auth/auth.service.ts b/src/app/modules/auth/auth.service.ts
new file mode 100644
index 00000000..c7bad73d
--- /dev/null
+++ b/src/app/modules/auth/auth.service.ts
@@ -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 .
+ */
+// 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(
+ AuthActionBuilder.Init(),
+ );
+
+ private _tokenSubject = new BehaviorSubject(
+ undefined,
+ );
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private _userSubject = new BehaviorSubject(undefined);
+
+ private _authenticatedSubject = new BehaviorSubject(false);
+
+ private _initComplete = new BehaviorSubject(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 {
+ return this._tokenSubject.asObservable();
+ }
+
+ get isAuthenticated$(): Observable {
+ return this._authenticatedSubject.asObservable();
+ }
+
+ get initComplete$(): Observable {
+ return this._initComplete.asObservable();
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ get user$(): Observable {
+ return this._userSubject.asObservable();
+ }
+
+ get events$(): Observable {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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 {
+ 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);
+ }
+}
diff --git a/src/app/modules/auth/default-auth.service.ts b/src/app/modules/auth/default-auth.service.ts
index 7dafaabb..f3384d79 100644
--- a/src/app/modules/auth/default-auth.service.ts
+++ b/src/app/modules/auth/default-auth.service.ts
@@ -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';