mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-21 00:52:55 +00:00
feat: add auth support (default and paia)
This commit is contained in:
committed by
Jovan Krunić
parent
046a95ba1d
commit
b5f239ea4e
@@ -0,0 +1,3 @@
|
||||
<div class="centeredMessageContainer">
|
||||
<p>{{ 'auth.messages.default.authorizing' | translate }}</p>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import {Component, OnInit, OnDestroy} from '@angular/core';
|
||||
import {NavController} from '@ionic/angular';
|
||||
import {Router} from '@angular/router';
|
||||
import {IAuthAction} from 'ionic-appauth';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {DefaultAuthService} from '../../default-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-callback',
|
||||
templateUrl: './auth-callback-page.component.html',
|
||||
styleUrls: ['./auth-callback-page.component.scss'],
|
||||
})
|
||||
export class AuthCallbackPageComponent implements OnInit, OnDestroy {
|
||||
sub: Subscription;
|
||||
|
||||
constructor(
|
||||
private auth: DefaultAuthService,
|
||||
private navCtrl: NavController,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.sub = this.auth.events$.subscribe(action => this.postCallback(action));
|
||||
this.auth.authorizationCallback(window.location.origin + this.router.url);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
async postCallback(_action: IAuthAction) {
|
||||
await this.navCtrl.navigateRoot('profile');
|
||||
}
|
||||
}
|
||||
42
src/app/modules/auth/auth-guard.service.ts
Normal file
42
src/app/modules/auth/auth-guard.service.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {CanActivate, Router, RouterStateSnapshot} from '@angular/router';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
import {PAIAAuthService} from './paia/paia-auth.service';
|
||||
import {IAuthService} from 'ionic-appauth';
|
||||
import {ActivatedAuthRouteSnapshot} from './auth-routes';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuardService implements CanActivate {
|
||||
authService: IAuthService | PAIAAuthService;
|
||||
|
||||
constructor(
|
||||
private defaultAuth: DefaultAuthService,
|
||||
private paiaAuth: PAIAAuthService,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
public async canActivate(
|
||||
route: ActivatedAuthRouteSnapshot,
|
||||
_state: RouterStateSnapshot,
|
||||
) {
|
||||
switch (route.data.authProvider) {
|
||||
case 'paia':
|
||||
this.authService = this.paiaAuth;
|
||||
break;
|
||||
default:
|
||||
this.authService = this.defaultAuth;
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.authService.getValidToken();
|
||||
} catch {
|
||||
this.router.navigate(['profile']);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
50
src/app/modules/auth/auth-helper.service.ts
Normal file
50
src/app/modules/auth/auth-helper.service.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {
|
||||
SCAuthorizationProviderType,
|
||||
SCUserConfiguration,
|
||||
userMapping,
|
||||
} from '../profile/user';
|
||||
import {IPAIAAuthAction} from './paia/paia-auth-action';
|
||||
import {AuthActions, IAuthAction} from 'ionic-appauth';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {JSONFile} from '@angular/cli/utilities/json-file';
|
||||
import {JSONPath} from 'jsonpath-plus';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthHelperService {
|
||||
constructor(private translateService: TranslateService) {}
|
||||
|
||||
public getAuthMessage(
|
||||
provider: SCAuthorizationProviderType,
|
||||
action: IAuthAction | IPAIAAuthAction,
|
||||
) {
|
||||
let message: string | undefined;
|
||||
switch (action.action) {
|
||||
case AuthActions.SignInSuccess:
|
||||
message = this.translateService.instant(
|
||||
`auth.messages.${provider}.logged_in_success`,
|
||||
);
|
||||
break;
|
||||
case AuthActions.SignOutSuccess:
|
||||
message = this.translateService.instant(
|
||||
`auth.messages.${provider}.logged_out_success`,
|
||||
);
|
||||
break;
|
||||
}
|
||||
return message;
|
||||
}
|
||||
|
||||
getUserFromUserInfo(userInfo: JSONFile) {
|
||||
const user: SCUserConfiguration = {id: '', name: '', role: 'student'};
|
||||
for (const key in userMapping) {
|
||||
user[key as keyof SCUserConfiguration] = JSONPath({
|
||||
path: userMapping[key as keyof SCUserConfiguration] as string,
|
||||
json: userInfo,
|
||||
})[0];
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
31
src/app/modules/auth/auth-routes.ts
Normal file
31
src/app/modules/auth/auth-routes.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import {ActivatedRouteSnapshot, Data, Route} from '@angular/router';
|
||||
import {SCAuthorizationProviderType} from '../profile/user';
|
||||
|
||||
export interface AuthRoute extends Route {
|
||||
data: {
|
||||
authProvider: SCAuthorizationProviderType;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: any;
|
||||
};
|
||||
}
|
||||
|
||||
export class ActivatedAuthRouteSnapshot extends ActivatedRouteSnapshot {
|
||||
data: Data & {authProvider: AuthRoute['data']['authProvider']};
|
||||
}
|
||||
|
||||
export type AuthRoutes = AuthRoute[];
|
||||
33
src/app/modules/auth/auth-routing.module.ts
Normal file
33
src/app/modules/auth/auth-routing.module.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {RouterModule, Routes} from '@angular/router';
|
||||
import {NgModule} from '@angular/core';
|
||||
import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component';
|
||||
import {PAIAAuthCallbackPageComponent} from './paia/auth-callback/page/auth-callback-page.component';
|
||||
|
||||
const authRoutes: Routes = [
|
||||
{path: 'auth/callback', component: AuthCallbackPageComponent},
|
||||
{path: 'auth/paia/callback', component: PAIAAuthCallbackPageComponent},
|
||||
];
|
||||
|
||||
/**
|
||||
* Module defining routes for auth module
|
||||
*/
|
||||
@NgModule({
|
||||
exports: [RouterModule],
|
||||
imports: [RouterModule.forChild(authRoutes)],
|
||||
})
|
||||
export class AuthRoutingModule {}
|
||||
47
src/app/modules/auth/auth.module.ts
Normal file
47
src/app/modules/auth/auth.module.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {Platform} from '@ionic/angular';
|
||||
import {Requestor, StorageBackend} from '@openid/appauth';
|
||||
import {authFactory, paiaAuthFactory, storageFactory} from './factories';
|
||||
import {DefaultAuthService} from './default-auth.service';
|
||||
import {Browser} from 'ionic-appauth';
|
||||
import {CapacitorBrowser} from 'ionic-appauth/lib/capacitor';
|
||||
import {httpFactory} from './factories/http.factory';
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {PAIAAuthService} from './paia/paia-auth.service';
|
||||
import {AuthRoutingModule} from './auth-routing.module';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {AuthCallbackPageComponent} from './auth-callback/page/auth-callback-page.component';
|
||||
import {PAIAAuthCallbackPageComponent} from './paia/auth-callback/page/auth-callback-page.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AuthCallbackPageComponent, PAIAAuthCallbackPageComponent],
|
||||
imports: [CommonModule, AuthRoutingModule, TranslateModule],
|
||||
providers: [
|
||||
{
|
||||
provide: StorageBackend,
|
||||
useFactory: storageFactory,
|
||||
deps: [Platform],
|
||||
},
|
||||
{
|
||||
provide: Requestor,
|
||||
useFactory: httpFactory,
|
||||
deps: [Platform, HttpClient],
|
||||
},
|
||||
{
|
||||
provide: Browser,
|
||||
useClass: CapacitorBrowser,
|
||||
},
|
||||
{
|
||||
provide: DefaultAuthService,
|
||||
useFactory: authFactory,
|
||||
deps: [Requestor, Browser, StorageBackend],
|
||||
},
|
||||
{
|
||||
provide: PAIAAuthService,
|
||||
useFactory: paiaAuthFactory,
|
||||
deps: [Requestor, Browser, StorageBackend],
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
53
src/app/modules/auth/capacitor-requestor.ts
Normal file
53
src/app/modules/auth/capacitor-requestor.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {Requestor} from '@openid/appauth';
|
||||
import {Http, HttpHeaders, HttpResponse} from '@capacitor-community/http';
|
||||
import {XhrSettings} from 'ionic-appauth/lib/cordova';
|
||||
import qs from 'qs';
|
||||
|
||||
// REQUIRES CAPACITOR PLUGIN
|
||||
// @capacitor-community/http
|
||||
export class CapacitorRequestor extends Requestor {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public async xhr<T>(settings: XhrSettings): Promise<T> {
|
||||
if (!settings.method) settings.method = 'GET';
|
||||
|
||||
switch (settings.method) {
|
||||
case 'GET':
|
||||
return this.get(settings.url, settings.headers);
|
||||
case 'POST':
|
||||
return this.post(settings.url, settings.data, settings.headers);
|
||||
case 'PUT':
|
||||
return this.put(settings.url, settings.data, settings.headers);
|
||||
case 'DELETE':
|
||||
return this.delete(settings.url, settings.headers);
|
||||
}
|
||||
}
|
||||
|
||||
private async get<T>(url: string, headers: HttpHeaders) {
|
||||
return Http.get({url, headers}).then(
|
||||
(response: HttpResponse) => response.data as T,
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async post<T>(url: string, data: any, headers: HttpHeaders) {
|
||||
return Http.post({url, data: qs.parse(data), headers}).then(
|
||||
(response: HttpResponse) => response.data as T,
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private async put<T>(url: string, data: any, headers: HttpHeaders) {
|
||||
return Http.put({url, data, headers}).then(
|
||||
(response: HttpResponse) => response.data as T,
|
||||
);
|
||||
}
|
||||
|
||||
private async delete<T>(url: string, headers: HttpHeaders) {
|
||||
return Http.del({url, headers}).then(
|
||||
(response: HttpResponse) => response.data as T,
|
||||
);
|
||||
}
|
||||
}
|
||||
53
src/app/modules/auth/default-auth.service.ts
Normal file
53
src/app/modules/auth/default-auth.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {AuthorizationRequestHandler} from '@openid/appauth';
|
||||
import {
|
||||
StorageBackend,
|
||||
Requestor,
|
||||
AuthorizationServiceConfiguration,
|
||||
LocalStorageBackend,
|
||||
JQueryRequestor,
|
||||
TokenRequestHandler,
|
||||
} from '@openid/appauth';
|
||||
import {
|
||||
UserInfoHandler,
|
||||
EndSessionHandler,
|
||||
Browser,
|
||||
DefaultBrowser,
|
||||
AuthService,
|
||||
AuthActionBuilder,
|
||||
} from 'ionic-appauth';
|
||||
|
||||
const TOKEN_RESPONSE_KEY = 'token_response';
|
||||
|
||||
export class DefaultAuthService extends AuthService {
|
||||
public localConfiguration: AuthorizationServiceConfiguration;
|
||||
|
||||
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(),
|
||||
) {
|
||||
super(browser, storage, requestor);
|
||||
}
|
||||
|
||||
get configuration(): Promise<AuthorizationServiceConfiguration> {
|
||||
if (!this.localConfiguration)
|
||||
throw new Error('Local Configuration Not Defined');
|
||||
|
||||
return Promise.resolve(this.localConfiguration);
|
||||
}
|
||||
|
||||
public async signOut() {
|
||||
await this.storage.removeItem(TOKEN_RESPONSE_KEY).catch(error => {
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutFailed(error));
|
||||
});
|
||||
this.notifyActionListers(AuthActionBuilder.SignOutSuccess());
|
||||
}
|
||||
}
|
||||
26
src/app/modules/auth/end-session/end-session.module.ts
Normal file
26
src/app/modules/auth/end-session/end-session.module.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
import {Routes, RouterModule} from '@angular/router';
|
||||
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
|
||||
import {EndSessionPageComponent} from './page/end-session-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'logout',
|
||||
component: EndSessionPageComponent,
|
||||
},
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
IonicModule,
|
||||
RouterModule.forChild(routes),
|
||||
],
|
||||
declarations: [EndSessionPageComponent],
|
||||
})
|
||||
export class EndSessionPageModule {}
|
||||
@@ -0,0 +1 @@
|
||||
<p>Signing out...</p>
|
||||
@@ -0,0 +1,20 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {NavController} from '@ionic/angular';
|
||||
import {DefaultAuthService} from '../../default-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'end-session',
|
||||
templateUrl: './end-session-page.component.html',
|
||||
styleUrls: ['./end-session-page.component.scss'],
|
||||
})
|
||||
export class EndSessionPageComponent implements OnInit {
|
||||
constructor(
|
||||
private auth: DefaultAuthService,
|
||||
private navCtrl: NavController,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
this.auth.endSessionCallback();
|
||||
await this.navCtrl.navigateRoot('profile');
|
||||
}
|
||||
}
|
||||
52
src/app/modules/auth/factories/auth.factory.ts
Normal file
52
src/app/modules/auth/factories/auth.factory.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {
|
||||
StorageBackend,
|
||||
Requestor,
|
||||
AuthorizationServiceConfiguration,
|
||||
} from '@openid/appauth';
|
||||
import {Browser} from 'ionic-appauth';
|
||||
import {environment} from 'src/environments/environment';
|
||||
import {DefaultAuthService} from '../default-auth.service';
|
||||
import {PAIAAuthService} from '../paia/paia-auth.service';
|
||||
|
||||
export const authFactory = (
|
||||
requestor: Requestor,
|
||||
browser: Browser,
|
||||
storage: StorageBackend,
|
||||
) => {
|
||||
const authService = new DefaultAuthService(browser, storage, requestor);
|
||||
authService.authConfig = environment.oauth2.client.his;
|
||||
authService.localConfiguration = new AuthorizationServiceConfiguration(
|
||||
environment.oauth2.service.his,
|
||||
);
|
||||
|
||||
return authService;
|
||||
};
|
||||
|
||||
export const paiaAuthFactory = (
|
||||
requestor: Requestor,
|
||||
browser: Browser,
|
||||
storage: StorageBackend,
|
||||
) => {
|
||||
const authService = new PAIAAuthService(browser, storage, requestor);
|
||||
authService.authConfig = environment.oauth2.client.paia;
|
||||
authService.localConfiguration = new AuthorizationServiceConfiguration(
|
||||
environment.oauth2.service.paia,
|
||||
);
|
||||
|
||||
return authService;
|
||||
};
|
||||
9
src/app/modules/auth/factories/browser.factory.ts
Normal file
9
src/app/modules/auth/factories/browser.factory.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {Platform} from '@ionic/angular';
|
||||
import {DefaultBrowser} from 'ionic-appauth';
|
||||
import {CapacitorBrowser} from 'ionic-appauth/lib/capacitor';
|
||||
|
||||
export const browserFactory = (platform: Platform) => {
|
||||
return platform.is('capacitor')
|
||||
? new CapacitorBrowser()
|
||||
: new DefaultBrowser();
|
||||
};
|
||||
10
src/app/modules/auth/factories/http.factory.ts
Normal file
10
src/app/modules/auth/factories/http.factory.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import {HttpClient} from '@angular/common/http';
|
||||
import {Platform} from '@ionic/angular';
|
||||
import {CapacitorRequestor} from '../capacitor-requestor';
|
||||
import {NgHttpService} from '../ng-http.service';
|
||||
|
||||
export const httpFactory = (platform: Platform, httpClient: HttpClient) => {
|
||||
return platform.is('capacitor')
|
||||
? new CapacitorRequestor()
|
||||
: new NgHttpService(httpClient);
|
||||
};
|
||||
3
src/app/modules/auth/factories/index.ts
Normal file
3
src/app/modules/auth/factories/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './auth.factory';
|
||||
export * from './browser.factory';
|
||||
export * from './storage.factory';
|
||||
9
src/app/modules/auth/factories/storage.factory.ts
Normal file
9
src/app/modules/auth/factories/storage.factory.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import {Platform} from '@ionic/angular';
|
||||
import {CapacitorSecureStorage} from 'ionic-appauth/lib/capacitor';
|
||||
import {IonicStorage} from 'ionic-appauth/lib';
|
||||
|
||||
export const storageFactory = (platform: Platform) => {
|
||||
return platform.is('capacitor')
|
||||
? new CapacitorSecureStorage()
|
||||
: new IonicStorage();
|
||||
};
|
||||
53
src/app/modules/auth/ng-http.service.ts
Normal file
53
src/app/modules/auth/ng-http.service.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {Requestor} from '@openid/appauth';
|
||||
import {HttpClient, HttpHeaders} from '@angular/common/http';
|
||||
import {XhrSettings} from 'ionic-appauth/lib/cordova';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class NgHttpService implements Requestor {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
public async xhr<T>(settings: XhrSettings): Promise<T> {
|
||||
if (!settings.method) {
|
||||
settings.method = 'GET';
|
||||
}
|
||||
|
||||
switch (settings.method) {
|
||||
case 'GET':
|
||||
return this.http
|
||||
.get<T>(settings.url, {headers: this.getHeaders(settings.headers)})
|
||||
.toPromise();
|
||||
case 'POST':
|
||||
return this.http
|
||||
.post<T>(settings.url, settings.data, {
|
||||
headers: this.getHeaders(settings.headers),
|
||||
})
|
||||
.toPromise();
|
||||
case 'PUT':
|
||||
return this.http
|
||||
.put<T>(settings.url, settings.data, {
|
||||
headers: this.getHeaders(settings.headers),
|
||||
})
|
||||
.toPromise();
|
||||
case 'DELETE':
|
||||
return this.http
|
||||
.delete<T>(settings.url, {headers: this.getHeaders(settings.headers)})
|
||||
.toPromise();
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private getHeaders(headers: any): HttpHeaders {
|
||||
let httpHeaders: HttpHeaders = new HttpHeaders();
|
||||
|
||||
if (headers !== undefined) {
|
||||
for (const key of Object.keys(headers)) {
|
||||
httpHeaders = httpHeaders.append(key, headers[key]);
|
||||
}
|
||||
}
|
||||
|
||||
return httpHeaders;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<div class="centeredMessageContainer">
|
||||
<p>{{ 'auth.messages.paia.authorizing' | translate }}</p>
|
||||
</div>
|
||||
@@ -0,0 +1,34 @@
|
||||
import {Component, OnInit, OnDestroy} from '@angular/core';
|
||||
import {NavController} from '@ionic/angular';
|
||||
import {Router} from '@angular/router';
|
||||
import {IAuthAction} from 'ionic-appauth';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {PAIAAuthService} from '../../paia-auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'auth-callback',
|
||||
templateUrl: './auth-callback-page.component.html',
|
||||
styleUrls: ['./auth-callback-page.component.scss'],
|
||||
})
|
||||
export class PAIAAuthCallbackPageComponent implements OnInit, OnDestroy {
|
||||
sub: Subscription;
|
||||
|
||||
constructor(
|
||||
private auth: PAIAAuthService,
|
||||
private navCtrl: NavController,
|
||||
private router: Router,
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.sub = this.auth.events$.subscribe(action => this.postCallback(action));
|
||||
this.auth.authorizationCallback(window.location.origin + this.router.url);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
|
||||
async postCallback(_action: IAuthAction) {
|
||||
this.navCtrl.navigateRoot('profile');
|
||||
}
|
||||
}
|
||||
217
src/app/modules/auth/paia/authorization-request-handler.ts
Normal file
217
src/app/modules/auth/paia/authorization-request-handler.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
import {
|
||||
StorageBackend,
|
||||
BasicQueryStringUtils,
|
||||
DefaultCrypto,
|
||||
AuthorizationServiceConfiguration,
|
||||
AuthorizationRequest,
|
||||
StringMap,
|
||||
AuthorizationError,
|
||||
AuthorizationErrorJson,
|
||||
} from '@openid/appauth';
|
||||
import {Browser} from 'ionic-appauth';
|
||||
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
||||
import {PAIAAuthorizationRequestResponse} from './authorization-request-response';
|
||||
import {
|
||||
PAIAAuthorizationResponse,
|
||||
PAIAAuthorizationResponseJson,
|
||||
} from './paia-authorization-response';
|
||||
|
||||
/** key for authorization request. */
|
||||
const authorizationRequestKey = (handle: string) => {
|
||||
return `${handle}_appauth_authorization_request`;
|
||||
};
|
||||
|
||||
/** key in local storage which represents the current authorization request. */
|
||||
const AUTHORIZATION_REQUEST_HANDLE_KEY =
|
||||
'appauth_current_authorization_request';
|
||||
export const AUTHORIZATION_RESPONSE_KEY = 'auth_response';
|
||||
|
||||
// TODO: PAIA specific ...!!! use whatever you can from the parent class !
|
||||
|
||||
export class PAIAAuthorizationRequestHandler {
|
||||
notifier: PAIAAuthorizationNotifier;
|
||||
|
||||
constructor(
|
||||
private browser: Browser,
|
||||
private storage: StorageBackend,
|
||||
public utils = new BasicQueryStringUtils(),
|
||||
protected crypto = new Crypto(),
|
||||
private generateRandom = new DefaultCrypto(),
|
||||
) {}
|
||||
|
||||
public async performAuthorizationRequest(
|
||||
configuration: AuthorizationServiceConfiguration,
|
||||
request: AuthorizationRequest,
|
||||
): Promise<void> {
|
||||
const handle = this.generateRandom.generateRandom(10);
|
||||
await this.storage.setItem(AUTHORIZATION_REQUEST_HANDLE_KEY, handle);
|
||||
await this.storage.setItem(
|
||||
authorizationRequestKey(handle),
|
||||
JSON.stringify(await request.toJson()),
|
||||
);
|
||||
const url = this.buildRequestUrl(configuration, request);
|
||||
const returnedUrl: string | undefined = await this.browser.showWindow(
|
||||
url,
|
||||
request.redirectUri,
|
||||
);
|
||||
|
||||
// callback may come from showWindow or via another method
|
||||
if (typeof returnedUrl !== 'undefined') {
|
||||
await this.storage.setItem(AUTHORIZATION_RESPONSE_KEY, returnedUrl);
|
||||
await this.completeAuthorizationRequestIfPossible();
|
||||
}
|
||||
}
|
||||
|
||||
protected async completeAuthorizationRequest(): Promise<PAIAAuthorizationRequestResponse> {
|
||||
const handle = await this.storage.getItem(AUTHORIZATION_REQUEST_HANDLE_KEY);
|
||||
|
||||
if (!handle) {
|
||||
throw new Error('Handle Not Available');
|
||||
}
|
||||
|
||||
const request: AuthorizationRequest = this.getAuthorizationRequest(
|
||||
await this.storage.getItem(authorizationRequestKey(handle)),
|
||||
);
|
||||
const queryParameters = this.getQueryParams(
|
||||
await this.storage.getItem(AUTHORIZATION_RESPONSE_KEY),
|
||||
);
|
||||
void this.removeItemsFromStorage(handle);
|
||||
|
||||
// const state: string | undefined = queryParams['state'];
|
||||
const error: string | undefined = queryParameters['error'];
|
||||
|
||||
// TODO: we need state from PAIA (we don't get state at the moment)
|
||||
// if (state !== request.state) {
|
||||
// throw new Error("State Does Not Match");
|
||||
// }
|
||||
|
||||
return <PAIAAuthorizationRequestResponse>{
|
||||
request: request, // request
|
||||
response: !error
|
||||
? this.getAuthorizationResponse(queryParameters)
|
||||
: undefined,
|
||||
error: error ? this.getAuthorizationError(queryParameters) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
private getAuthorizationRequest(
|
||||
authRequest: string | null,
|
||||
): AuthorizationRequest {
|
||||
if (authRequest == undefined) {
|
||||
throw new Error('No Auth Request Available');
|
||||
}
|
||||
|
||||
return new AuthorizationRequest(JSON.parse(authRequest));
|
||||
}
|
||||
|
||||
private getAuthorizationError(
|
||||
queryParameters: StringMap,
|
||||
): AuthorizationError {
|
||||
const authorizationErrorJSON: AuthorizationErrorJson = {
|
||||
error: queryParameters['error'],
|
||||
error_description: queryParameters['error_description'],
|
||||
error_uri: undefined,
|
||||
state: queryParameters['state'],
|
||||
};
|
||||
return new AuthorizationError(authorizationErrorJSON);
|
||||
}
|
||||
|
||||
private getAuthorizationResponse(
|
||||
queryParameters: StringMap,
|
||||
): PAIAAuthorizationResponse {
|
||||
const authorizationResponseJSON: PAIAAuthorizationResponseJson = {
|
||||
code: queryParameters['code'],
|
||||
patron: queryParameters['patron'],
|
||||
// TODO: currently PAIA is not providing state
|
||||
state: queryParameters['state'] ?? '',
|
||||
};
|
||||
return new PAIAAuthorizationResponse(authorizationResponseJSON);
|
||||
}
|
||||
|
||||
private async removeItemsFromStorage(handle: string): Promise<void> {
|
||||
await this.storage.removeItem(AUTHORIZATION_REQUEST_HANDLE_KEY);
|
||||
await this.storage.removeItem(authorizationRequestKey(handle));
|
||||
await this.storage.removeItem(AUTHORIZATION_RESPONSE_KEY);
|
||||
}
|
||||
|
||||
private getQueryParams(authResponse: string | null): StringMap {
|
||||
if (authResponse != undefined) {
|
||||
const querySide: string = authResponse.split('#')[0];
|
||||
const parts: string[] = querySide.split('?');
|
||||
if (parts.length !== 2) throw new Error('Invalid auth response string');
|
||||
const hash = parts[1];
|
||||
return this.utils.parseQueryString(hash);
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
setAuthorizationNotifier(
|
||||
notifier: PAIAAuthorizationNotifier,
|
||||
): PAIAAuthorizationRequestHandler {
|
||||
this.notifier = notifier;
|
||||
return this;
|
||||
}
|
||||
|
||||
completeAuthorizationRequestIfPossible(): Promise<void> {
|
||||
// call complete authorization if possible to see there might
|
||||
// be a response that needs to be delivered.
|
||||
console.log(
|
||||
`Checking to see if there is an authorization response to be delivered.`,
|
||||
);
|
||||
if (!this.notifier) {
|
||||
console.log(`Notifier is not present on AuthorizationRequest handler.
|
||||
No delivery of result will be possible`);
|
||||
}
|
||||
return this.completeAuthorizationRequest().then(result => {
|
||||
if (!result) {
|
||||
console.log(`No result is available yet.`);
|
||||
}
|
||||
if (result && this.notifier) {
|
||||
this.notifier.onAuthorizationComplete(
|
||||
result.request,
|
||||
result.response,
|
||||
result.error,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility method to be able to build the authorization request URL.
|
||||
*/
|
||||
protected buildRequestUrl(
|
||||
configuration: AuthorizationServiceConfiguration,
|
||||
request: AuthorizationRequest,
|
||||
) {
|
||||
// build the query string
|
||||
// coerce to any type for convenience
|
||||
const requestMap: StringMap = {
|
||||
redirect_uri: request.redirectUri,
|
||||
client_id: request.clientId,
|
||||
response_type: request.responseType,
|
||||
state: request.state,
|
||||
scope: request.scope,
|
||||
};
|
||||
|
||||
const query = this.utils.stringify(requestMap);
|
||||
const baseUrl = configuration.authorizationEndpoint;
|
||||
|
||||
return `${baseUrl}?${query}&grant_type=client_credentials`;
|
||||
}
|
||||
}
|
||||
26
src/app/modules/auth/paia/authorization-request-response.ts
Normal file
26
src/app/modules/auth/paia/authorization-request-response.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {AuthorizationError, AuthorizationRequest} from '@openid/appauth';
|
||||
import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
||||
|
||||
/**
|
||||
* Represents a structural type holding both authorization request and response.
|
||||
*/
|
||||
export interface PAIAAuthorizationRequestResponse {
|
||||
request: AuthorizationRequest;
|
||||
response: PAIAAuthorizationResponse | null;
|
||||
error: AuthorizationError | null;
|
||||
}
|
||||
85
src/app/modules/auth/paia/paia-auth-action.ts
Normal file
85
src/app/modules/auth/paia/paia-auth-action.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {PAIATokenResponse} from './paia-token-response';
|
||||
import {AuthActionBuilder, IAuthAction} from 'ionic-appauth';
|
||||
|
||||
export interface IPAIAAuthAction extends IAuthAction {
|
||||
tokenResponse?: PAIATokenResponse;
|
||||
}
|
||||
|
||||
export class PAIAAuthActionBuilder extends AuthActionBuilder {
|
||||
public static Init(): IPAIAAuthAction {
|
||||
return AuthActionBuilder.Init() as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static SignOutSuccess(): IPAIAAuthAction {
|
||||
return AuthActionBuilder.SignOutSuccess() as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static SignOutFailed(error: Error): IPAIAAuthAction {
|
||||
return AuthActionBuilder.SignOutFailed(error) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static RefreshSuccess(
|
||||
tokenResponse: PAIATokenResponse,
|
||||
): IPAIAAuthAction {
|
||||
return AuthActionBuilder.RefreshSuccess(tokenResponse) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static RefreshFailed(error: Error): IPAIAAuthAction {
|
||||
return AuthActionBuilder.RefreshFailed(error) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static SignInSuccess(
|
||||
tokenResponse: PAIATokenResponse,
|
||||
): IPAIAAuthAction {
|
||||
return AuthActionBuilder.SignInSuccess(tokenResponse) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static SignInFailed(error: Error): IPAIAAuthAction {
|
||||
return AuthActionBuilder.SignInFailed(error) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static LoadTokenFromStorageSuccess(
|
||||
tokenResponse: PAIATokenResponse,
|
||||
): IPAIAAuthAction {
|
||||
return AuthActionBuilder.LoadTokenFromStorageSuccess(
|
||||
tokenResponse,
|
||||
) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static LoadTokenFromStorageFailed(error: Error): IPAIAAuthAction {
|
||||
return AuthActionBuilder.LoadTokenFromStorageFailed(
|
||||
error,
|
||||
) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static RevokeTokensSuccess(): IPAIAAuthAction {
|
||||
return AuthActionBuilder.RevokeTokensSuccess() as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static RevokeTokensFailed(error: Error): IPAIAAuthAction {
|
||||
return AuthActionBuilder.RevokeTokensFailed(error) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static LoadUserInfoSuccess(user: Error): IPAIAAuthAction {
|
||||
return AuthActionBuilder.LoadUserInfoSuccess(user) as IPAIAAuthAction;
|
||||
}
|
||||
|
||||
public static LoadUserInfoFailed(error: Error): IPAIAAuthAction {
|
||||
return AuthActionBuilder.LoadUserInfoFailed(error) as IPAIAAuthAction;
|
||||
}
|
||||
}
|
||||
342
src/app/modules/auth/paia/paia-auth.service.ts
Normal file
342
src/app/modules/auth/paia/paia-auth.service.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
import {
|
||||
AuthorizationError,
|
||||
AuthorizationRequest,
|
||||
AuthorizationRequestJson,
|
||||
AuthorizationServiceConfiguration,
|
||||
BasicQueryStringUtils,
|
||||
DefaultCrypto,
|
||||
JQueryRequestor,
|
||||
LocalStorageBackend,
|
||||
Requestor,
|
||||
StorageBackend,
|
||||
StringMap,
|
||||
} from '@openid/appauth';
|
||||
import {
|
||||
AuthActions,
|
||||
AUTHORIZATION_RESPONSE_KEY,
|
||||
AuthSubject,
|
||||
Browser,
|
||||
DefaultBrowser,
|
||||
EndSessionHandler,
|
||||
IAuthConfig,
|
||||
IonicEndSessionHandler,
|
||||
IonicUserInfoHandler,
|
||||
UserInfoHandler,
|
||||
} from 'ionic-appauth';
|
||||
import {BehaviorSubject, Observable} from 'rxjs';
|
||||
import {PAIATokenRequestHandler} from './token-request-handler';
|
||||
import {PAIAAuthorizationRequestHandler} from './authorization-request-handler';
|
||||
import {PAIATokenRequest, PAIATokenRequestJson} from './paia-token-request';
|
||||
import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
||||
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
|
||||
import {PAIATokenResponse} from './paia-token-response';
|
||||
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
|
||||
|
||||
const TOKEN_KEY = 'auth_paia_token';
|
||||
const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds
|
||||
|
||||
export interface IAuthService {
|
||||
signIn(authExtras?: StringMap, state?: string): void;
|
||||
signOut(state?: string, revokeTokens?: boolean): void;
|
||||
loadUserInfo(): void;
|
||||
authorizationCallback(callbackUrl: string): void;
|
||||
loadTokenFromStorage(): void;
|
||||
getValidToken(buffer?: number): Promise<PAIATokenResponse>;
|
||||
}
|
||||
|
||||
export class PAIAAuthService implements IAuthService {
|
||||
private _authConfig?: IAuthConfig;
|
||||
|
||||
private _authSubject: AuthSubject = new AuthSubject();
|
||||
|
||||
private _authSubjectV2 = new BehaviorSubject<IPAIAAuthAction>(
|
||||
PAIAAuthActionBuilder.Init(),
|
||||
);
|
||||
|
||||
private _tokenSubject = new BehaviorSubject<PAIATokenResponse | 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: PAIATokenRequestHandler;
|
||||
|
||||
protected userInfoHandler: UserInfoHandler;
|
||||
|
||||
protected requestHandler: PAIAAuthorizationRequestHandler;
|
||||
|
||||
protected endSessionHandler: EndSessionHandler;
|
||||
|
||||
public localConfiguration: AuthorizationServiceConfiguration;
|
||||
|
||||
constructor(
|
||||
protected browser: Browser = new DefaultBrowser(),
|
||||
protected storage: StorageBackend = new LocalStorageBackend(),
|
||||
protected requestor: Requestor = new JQueryRequestor(),
|
||||
utils = new BasicQueryStringUtils(),
|
||||
) {
|
||||
this.tokenHandler = new PAIATokenRequestHandler(requestor);
|
||||
this.userInfoHandler = new IonicUserInfoHandler(requestor);
|
||||
this.requestHandler = new PAIAAuthorizationRequestHandler(
|
||||
browser,
|
||||
storage,
|
||||
utils,
|
||||
crypto,
|
||||
);
|
||||
this.endSessionHandler = new IonicEndSessionHandler(browser);
|
||||
}
|
||||
|
||||
get token$(): Observable<PAIATokenResponse | 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<IPAIAAuthAction> {
|
||||
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.localConfiguration)
|
||||
throw new Error('Local Configuration Not Defined');
|
||||
|
||||
return Promise.resolve(this.localConfiguration);
|
||||
}
|
||||
|
||||
public async init() {
|
||||
this.setupAuthorizationNotifier();
|
||||
this.loadTokenFromStorage();
|
||||
}
|
||||
|
||||
protected notifyActionListers(action: IPAIAAuthAction) {
|
||||
this._authSubjectV2.next(action);
|
||||
this._authSubject.notify(action);
|
||||
|
||||
/* eslint-disable unicorn/no-useless-undefined */
|
||||
switch (action.action) {
|
||||
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:
|
||||
this._tokenSubject.next(action.tokenResponse);
|
||||
this._authenticatedSubject.next(true);
|
||||
break;
|
||||
case AuthActions.LoadTokenFromStorageSuccess:
|
||||
this._tokenSubject.next(action.tokenResponse);
|
||||
this._authenticatedSubject.next(true);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
protected setupAuthorizationNotifier() {
|
||||
const notifier = new PAIAAuthorizationNotifier();
|
||||
this.requestHandler.setAuthorizationNotifier(notifier);
|
||||
notifier.setAuthorizationListener((request, response, error) =>
|
||||
this.onAuthorizationNotification(request, response, error),
|
||||
);
|
||||
}
|
||||
|
||||
protected onAuthorizationNotification(
|
||||
request: AuthorizationRequest,
|
||||
response: PAIAAuthorizationResponse | 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, response.patron, 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 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,
|
||||
patron: string,
|
||||
codeVerifier?: string,
|
||||
): Promise<void> {
|
||||
const requestJSON: PAIATokenRequestJson = {
|
||||
code: code,
|
||||
patron: patron,
|
||||
extras: codeVerifier
|
||||
? {
|
||||
code_verifier: codeVerifier,
|
||||
}
|
||||
: {},
|
||||
};
|
||||
|
||||
const token: PAIATokenResponse =
|
||||
await this.tokenHandler.performTokenRequest(
|
||||
await this.configuration,
|
||||
new PAIATokenRequest(requestJSON),
|
||||
);
|
||||
await this.storage.setItem(TOKEN_KEY, JSON.stringify(token.toJson()));
|
||||
this.notifyActionListers(PAIAAuthActionBuilder.SignInSuccess(token));
|
||||
}
|
||||
|
||||
public async revokeTokens() {
|
||||
// Note: only locally
|
||||
await this.storage.removeItem(TOKEN_KEY);
|
||||
this.notifyActionListers(PAIAAuthActionBuilder.RevokeTokensSuccess());
|
||||
}
|
||||
|
||||
public async signOut() {
|
||||
await this.revokeTokens().catch(error =>
|
||||
this.notifyActionListers(PAIAAuthActionBuilder.SignOutFailed(error)),
|
||||
);
|
||||
this.notifyActionListers(PAIAAuthActionBuilder.SignOutSuccess());
|
||||
}
|
||||
|
||||
protected async internalLoadTokenFromStorage() {
|
||||
let token: PAIATokenResponse | undefined;
|
||||
const tokenResponseString: string | null = await this.storage.getItem(
|
||||
TOKEN_KEY,
|
||||
);
|
||||
|
||||
if (tokenResponseString != undefined) {
|
||||
token = new PAIATokenResponse(JSON.parse(tokenResponseString));
|
||||
|
||||
if (token) {
|
||||
return this.notifyActionListers(
|
||||
PAIAAuthActionBuilder.LoadTokenFromStorageSuccess(token),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('No Token In Storage');
|
||||
}
|
||||
|
||||
protected async internalRequestUserInfo() {
|
||||
if (this._tokenSubject.value) {
|
||||
const userInfo = await this.userInfoHandler.performUserInfoRequest(
|
||||
await this.configuration,
|
||||
this._tokenSubject.value,
|
||||
);
|
||||
this.notifyActionListers(
|
||||
PAIAAuthActionBuilder.LoadUserInfoSuccess(userInfo),
|
||||
);
|
||||
} else {
|
||||
throw new Error('No Token Available');
|
||||
}
|
||||
}
|
||||
|
||||
public async loadTokenFromStorage() {
|
||||
await this.internalLoadTokenFromStorage().catch(error => {
|
||||
this.notifyActionListers(
|
||||
PAIAAuthActionBuilder.LoadTokenFromStorageFailed(error),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public async signIn(authExtras?: StringMap, state?: string) {
|
||||
await this.performAuthorizationRequest(authExtras, state).catch(error => {
|
||||
this.notifyActionListers(PAIAAuthActionBuilder.SignInFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async loadUserInfo() {
|
||||
await this.internalRequestUserInfo().catch(error => {
|
||||
this.notifyActionListers(PAIAAuthActionBuilder.LoadUserInfoFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public authorizationCallback(callbackUrl: string): void {
|
||||
this.internalAuthorizationCallback(callbackUrl).catch(error => {
|
||||
this.notifyActionListers(PAIAAuthActionBuilder.SignInFailed(error));
|
||||
});
|
||||
}
|
||||
|
||||
public async getValidToken(
|
||||
buffer: number = AUTH_EXPIRY_BUFFER,
|
||||
): Promise<PAIATokenResponse> {
|
||||
if (this._tokenSubject.value && this._tokenSubject.value.isValid(buffer)) {
|
||||
return this._tokenSubject.value;
|
||||
}
|
||||
|
||||
throw new Error('Unable To Obtain Valid Token');
|
||||
}
|
||||
}
|
||||
23
src/app/modules/auth/paia/paia-authorization-listener.ts
Normal file
23
src/app/modules/auth/paia/paia-authorization-listener.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {AuthorizationError, AuthorizationRequest} from '@openid/appauth';
|
||||
import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
||||
|
||||
export type PAIAAuthorizationListener = (
|
||||
request: AuthorizationRequest,
|
||||
response: PAIAAuthorizationResponse | null,
|
||||
error: AuthorizationError | null,
|
||||
) => void;
|
||||
40
src/app/modules/auth/paia/paia-authorization-notifier.ts
Normal file
40
src/app/modules/auth/paia/paia-authorization-notifier.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {PAIAAuthorizationListener} from './paia-authorization-listener';
|
||||
import {AuthorizationError, AuthorizationRequest} from '@openid/appauth';
|
||||
import {PAIAAuthorizationResponse} from './paia-authorization-response';
|
||||
|
||||
export class PAIAAuthorizationNotifier {
|
||||
// eslint-disable-next-line unicorn/no-null
|
||||
private listener: PAIAAuthorizationListener | null = null;
|
||||
|
||||
setAuthorizationListener(listener: PAIAAuthorizationListener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
/**
|
||||
* The authorization complete callback.
|
||||
*/
|
||||
onAuthorizationComplete(
|
||||
request: AuthorizationRequest,
|
||||
response: PAIAAuthorizationResponse | null,
|
||||
error: AuthorizationError | null,
|
||||
): void {
|
||||
if (this.listener) {
|
||||
// complete authorization request
|
||||
this.listener(request, response, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/app/modules/auth/paia/paia-authorization-response.ts
Normal file
38
src/app/modules/auth/paia/paia-authorization-response.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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/>.
|
||||
*/
|
||||
|
||||
export interface PAIAAuthorizationResponseJson {
|
||||
code: string;
|
||||
state: string;
|
||||
patron: string;
|
||||
}
|
||||
|
||||
export class PAIAAuthorizationResponse {
|
||||
code: string;
|
||||
|
||||
state: string;
|
||||
|
||||
patron: string;
|
||||
|
||||
constructor(response: PAIAAuthorizationResponseJson) {
|
||||
this.code = response.code;
|
||||
this.state = response.state;
|
||||
this.patron = response.patron;
|
||||
}
|
||||
|
||||
toJson(): PAIAAuthorizationResponseJson {
|
||||
return {code: this.code, state: this.state, patron: this.patron};
|
||||
}
|
||||
}
|
||||
65
src/app/modules/auth/paia/paia-token-request.ts
Normal file
65
src/app/modules/auth/paia/paia-token-request.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {StringMap} from '@openid/appauth';
|
||||
|
||||
// TODO: add documentation
|
||||
export interface PAIATokenRequestJson {
|
||||
code: string;
|
||||
patron: string;
|
||||
extras?: StringMap;
|
||||
}
|
||||
|
||||
export class PAIATokenRequest {
|
||||
code: string;
|
||||
|
||||
patron: string;
|
||||
|
||||
extras?: StringMap;
|
||||
|
||||
constructor(request: PAIATokenRequestJson) {
|
||||
this.code = request.code;
|
||||
this.patron = request.patron;
|
||||
this.extras = request.extras;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serializes a TokenRequest to a JavaScript object.
|
||||
*/
|
||||
toJson(): PAIATokenRequestJson {
|
||||
return {
|
||||
code: this.code,
|
||||
patron: this.patron,
|
||||
extras: this.extras,
|
||||
};
|
||||
}
|
||||
|
||||
toStringMap(): StringMap {
|
||||
const map: StringMap = {
|
||||
patron: this.patron,
|
||||
code: this.code,
|
||||
};
|
||||
|
||||
// copy over extras
|
||||
if (this.extras) {
|
||||
for (const extra in this.extras) {
|
||||
if (this.extras.hasOwnProperty(extra) && !map.hasOwnProperty(extra)) {
|
||||
// check before inserting to requestMap
|
||||
map[extra] = this.extras[extra];
|
||||
}
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
54
src/app/modules/auth/paia/paia-token-response.ts
Normal file
54
src/app/modules/auth/paia/paia-token-response.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {TokenResponse, TokenResponseJson} from '@openid/appauth';
|
||||
import {nowInSeconds} from '@openid/appauth';
|
||||
|
||||
const AUTH_EXPIRY_BUFFER = 10 * 60 * -1; // 10 mins in seconds
|
||||
|
||||
export interface PAIATokenResponseJson extends TokenResponseJson {
|
||||
patron: string;
|
||||
}
|
||||
|
||||
export class PAIATokenResponse extends TokenResponse {
|
||||
patron: string;
|
||||
|
||||
constructor(response: PAIATokenResponseJson) {
|
||||
super(response);
|
||||
this.patron = response.patron;
|
||||
}
|
||||
|
||||
toJson(): PAIATokenResponseJson {
|
||||
return {
|
||||
access_token: this.accessToken,
|
||||
id_token: this.idToken,
|
||||
refresh_token: this.refreshToken,
|
||||
scope: this.scope,
|
||||
token_type: this.tokenType,
|
||||
issued_at: this.issuedAt,
|
||||
expires_in: this.expiresIn?.toString(),
|
||||
patron: this.patron,
|
||||
};
|
||||
}
|
||||
|
||||
isValid(buffer: number = AUTH_EXPIRY_BUFFER): boolean {
|
||||
if (this.expiresIn) {
|
||||
const now = nowInSeconds();
|
||||
return now < this.issuedAt + this.expiresIn + buffer;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/app/modules/auth/paia/paia-user-info-handler.ts
Normal file
44
src/app/modules/auth/paia/paia-user-info-handler.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {AuthorizationServiceConfiguration, Requestor} from '@openid/appauth';
|
||||
import {PAIATokenResponse} from './paia-token-response';
|
||||
|
||||
export interface UserInfoHandler {
|
||||
performUserInfoRequest(
|
||||
configuration: AuthorizationServiceConfiguration,
|
||||
token: PAIATokenResponse,
|
||||
): Promise<unknown>;
|
||||
}
|
||||
|
||||
export class PAIAUserInfoHandler implements UserInfoHandler {
|
||||
constructor(private requestor: Requestor) {}
|
||||
|
||||
public async performUserInfoRequest(
|
||||
configuration: AuthorizationServiceConfiguration,
|
||||
token: PAIATokenResponse,
|
||||
): Promise<unknown> {
|
||||
const settings: JQueryAjaxSettings = {
|
||||
url: `${configuration.userInfoEndpoint}/${token.patron}`,
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `${
|
||||
token.tokenType == 'bearer' ? 'Bearer' : token.tokenType
|
||||
} ${token.accessToken}`,
|
||||
},
|
||||
};
|
||||
|
||||
return this.requestor.xhr(settings);
|
||||
}
|
||||
}
|
||||
84
src/app/modules/auth/paia/token-request-handler.ts
Normal file
84
src/app/modules/auth/paia/token-request-handler.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright (C) 2021 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 {
|
||||
TokenErrorJson,
|
||||
AuthorizationServiceConfiguration,
|
||||
RevokeTokenRequest,
|
||||
Requestor,
|
||||
JQueryRequestor,
|
||||
BasicQueryStringUtils,
|
||||
QueryStringUtils,
|
||||
AppAuthError,
|
||||
TokenError,
|
||||
} from '@openid/appauth';
|
||||
import {PAIATokenRequest} from './paia-token-request';
|
||||
import {PAIATokenResponse, PAIATokenResponseJson} from './paia-token-response';
|
||||
|
||||
export class PAIATokenRequestHandler {
|
||||
constructor(
|
||||
public readonly requestor: Requestor = new JQueryRequestor(),
|
||||
public readonly utils: QueryStringUtils = new BasicQueryStringUtils(),
|
||||
) {}
|
||||
|
||||
private isTokenResponse(
|
||||
response: PAIATokenResponseJson | TokenErrorJson,
|
||||
): response is PAIATokenResponseJson {
|
||||
return (response as TokenErrorJson).error === undefined;
|
||||
}
|
||||
|
||||
performRevokeTokenRequest(
|
||||
configuration: AuthorizationServiceConfiguration,
|
||||
request: RevokeTokenRequest,
|
||||
): Promise<boolean> {
|
||||
const revokeTokenResponse = this.requestor.xhr<boolean>({
|
||||
url: configuration.revocationEndpoint,
|
||||
method: 'GET',
|
||||
// headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
data: this.utils.stringify(request.toStringMap()),
|
||||
});
|
||||
|
||||
return revokeTokenResponse.then(_response => {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
performTokenRequest(
|
||||
configuration: AuthorizationServiceConfiguration,
|
||||
request: PAIATokenRequest,
|
||||
): Promise<PAIATokenResponse> {
|
||||
const tokenResponse = this.requestor.xhr<
|
||||
PAIATokenResponseJson | TokenErrorJson
|
||||
>({
|
||||
url: configuration.tokenEndpoint,
|
||||
method: 'POST',
|
||||
data: {
|
||||
patron: request.patron,
|
||||
grant_type: 'client_credentials',
|
||||
},
|
||||
headers: {
|
||||
'Authorization': `Basic ${request.code}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
return tokenResponse.then(response => {
|
||||
return this.isTokenResponse(response)
|
||||
? new PAIATokenResponse(response)
|
||||
: Promise.reject<PAIATokenResponse>(
|
||||
new AppAuthError(response.error, new TokenError(response)),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
21
src/app/modules/auth/user-info.model.ts
Normal file
21
src/app/modules/auth/user-info.model.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
export interface IUserInfo {
|
||||
display_name: string;
|
||||
role: string;
|
||||
email: string;
|
||||
user_name: string;
|
||||
}
|
||||
Reference in New Issue
Block a user