mirror of https://github.com/abpframework/abp
Merge pull request #15201 from abpframework/issue/10019-move-oauth-seperated-package
Issue/10019 move oauth seperated packagepull/15239/head
commit
40029b08f1
@ -0,0 +1,14 @@
|
|||||||
|
# ABP OAuth Package
|
||||||
|
The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0.
|
||||||
|
|
||||||
|
If your app is version 7.0 or higher, you should include "AbpOAuthModule.forRoot()" in your app.module.ts as an import after "CoreModule.forRoot(...)".
|
||||||
|
|
||||||
|
Those abstractions can be found in the @abp/ng-core packages.
|
||||||
|
- `AuthService` (the class that implements the IAuthService interface).
|
||||||
|
- `NAVIGATE_TO_MANAGE_PROFILE` Inject token.
|
||||||
|
- `AuthGuard` (the class that implements the IAuthGuard interface).
|
||||||
|
|
||||||
|
Those base classes are overridden by the "AbpOAuthModule" for oAuth.
|
||||||
|
If you want to make your own authentication system, you must also change these 'abstract' classes.
|
||||||
|
|
||||||
|
ApiInterceptor is provided by @abp/ng.oauth. The ApiInterceptor adds the token, accepted-language, and tenant id to the header of the HTTP request.
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { CanActivate, UrlTree } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthGuard implements IAuthGuard {
|
||||||
|
canActivate(): Observable<boolean> | boolean | UrlTree {
|
||||||
|
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export interface IAuthGuard extends CanActivate {}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { LoginParams } from '../models/auth';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract service for Authentication.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AuthService implements IAuthService {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
private warningMessage() {
|
||||||
|
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): Promise<any> {
|
||||||
|
this.warningMessage();
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
login(params: LoginParams): Observable<any> {
|
||||||
|
this.warningMessage();
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(queryParams?: Params): Observable<any> {
|
||||||
|
this.warningMessage();
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToLogin(queryParams?: Params): void {}
|
||||||
|
|
||||||
|
get isInternalAuth() {
|
||||||
|
throw new Error('not implemented');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAuthenticated(): boolean {
|
||||||
|
this.warningMessage();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IAuthService {
|
||||||
|
get isInternalAuth(): boolean;
|
||||||
|
|
||||||
|
get isAuthenticated(): boolean;
|
||||||
|
|
||||||
|
init(): Promise<any>;
|
||||||
|
|
||||||
|
logout(queryParams?: Params): Observable<any>;
|
||||||
|
|
||||||
|
navigateToLogin(queryParams?: Params): void;
|
||||||
|
|
||||||
|
login(params: LoginParams): Observable<any>;
|
||||||
|
}
|
||||||
@ -1 +1,3 @@
|
|||||||
export * from './ng-model.component';
|
export * from './ng-model.component';
|
||||||
|
export * from './auth.guard';
|
||||||
|
export * from './auth.service';
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from './auth.guard';
|
|
||||||
export * from './permission.guard';
|
export * from './permission.guard';
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from './oauth-configuration.handler';
|
|
||||||
export * from './routes.handler';
|
export * from './routes.handler';
|
||||||
|
|||||||
@ -1,53 +1,24 @@
|
|||||||
import { HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
import { HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { OAuthService } from 'angular-oauth2-oidc';
|
|
||||||
import { finalize } from 'rxjs/operators';
|
import { finalize } from 'rxjs/operators';
|
||||||
import { SessionStateService } from '../services/session-state.service';
|
import { HttpWaitService } from '../services';
|
||||||
import { HttpWaitService } from '../services/http-wait.service';
|
|
||||||
import { TENANT_KEY } from '../tokens/tenant-key.token';
|
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ApiInterceptor implements HttpInterceptor {
|
export class ApiInterceptor implements IApiInterceptor {
|
||||||
constructor(
|
constructor(private httpWaitService: HttpWaitService) {}
|
||||||
private oAuthService: OAuthService,
|
|
||||||
private sessionState: SessionStateService,
|
getAdditionalHeaders(existingHeaders?: HttpHeaders) {
|
||||||
private httpWaitService: HttpWaitService,
|
return existingHeaders;
|
||||||
@Inject(TENANT_KEY) private tenantKey: string,
|
}
|
||||||
) {}
|
|
||||||
|
|
||||||
intercept(request: HttpRequest<any>, next: HttpHandler) {
|
intercept(request: HttpRequest<any>, next: HttpHandler) {
|
||||||
this.httpWaitService.addRequest(request);
|
this.httpWaitService.addRequest(request);
|
||||||
return next
|
return next.handle(request).pipe(finalize(() => this.httpWaitService.deleteRequest(request)));
|
||||||
.handle(
|
|
||||||
request.clone({
|
|
||||||
setHeaders: this.getAdditionalHeaders(request.headers),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.pipe(finalize(() => this.httpWaitService.deleteRequest(request)));
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getAdditionalHeaders(existingHeaders?: HttpHeaders) {
|
export interface IApiInterceptor extends HttpInterceptor {
|
||||||
const headers = {} as any;
|
getAdditionalHeaders(existingHeaders?: HttpHeaders): HttpHeaders;
|
||||||
|
|
||||||
const token = this.oAuthService.getAccessToken();
|
|
||||||
if (!existingHeaders?.has('Authorization') && token) {
|
|
||||||
headers['Authorization'] = `Bearer ${token}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const lang = this.sessionState.getLanguage();
|
|
||||||
if (!existingHeaders?.has('Accept-Language') && lang) {
|
|
||||||
headers['Accept-Language'] = lang;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tenant = this.sessionState.getTenant();
|
|
||||||
if (!existingHeaders?.has(this.tenantKey) && tenant?.id) {
|
|
||||||
headers[this.tenantKey] = tenant.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
headers['X-Requested-With'] = 'XMLHttpRequest';
|
|
||||||
|
|
||||||
return headers;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,17 @@
|
|||||||
|
import { UnaryFunction } from 'rxjs';
|
||||||
|
import { Injector } from '@angular/core';
|
||||||
|
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
rememberMe?: boolean;
|
rememberMe?: boolean;
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PipeToLoginFn = (
|
||||||
|
params: Pick<LoginParams, 'redirectUrl' | 'rememberMe'>,
|
||||||
|
injector: Injector,
|
||||||
|
) => UnaryFunction<any, any>;
|
||||||
|
|
||||||
|
export type SetTokenResponseToStorageFn<T = any> = (injector: Injector, tokenRes: T) => void;
|
||||||
|
export type CheckAuthenticationStateFn = (injector: Injector) => void;
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
import { Injectable, Injector } from '@angular/core';
|
|
||||||
import { Params } from '@angular/router';
|
|
||||||
import { from, Observable } from 'rxjs';
|
|
||||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
|
||||||
import { LoginParams } from '../models/auth';
|
|
||||||
import { AuthFlowStrategy, AUTH_FLOW_STRATEGY } from '../strategies/auth-flow.strategy';
|
|
||||||
import { EnvironmentService } from './environment.service';
|
|
||||||
|
|
||||||
@Injectable({
|
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class AuthService {
|
|
||||||
private strategy: AuthFlowStrategy;
|
|
||||||
|
|
||||||
get isInternalAuth() {
|
|
||||||
return this.strategy.isInternalAuth;
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(protected injector: Injector) {}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
const environmentService = this.injector.get(EnvironmentService);
|
|
||||||
|
|
||||||
return environmentService
|
|
||||||
.getEnvironment$()
|
|
||||||
.pipe(
|
|
||||||
map(env => env?.oAuthConfig),
|
|
||||||
filter(oAuthConfig => !!oAuthConfig),
|
|
||||||
tap(oAuthConfig => {
|
|
||||||
this.strategy =
|
|
||||||
oAuthConfig.responseType === 'code'
|
|
||||||
? AUTH_FLOW_STRATEGY.Code(this.injector)
|
|
||||||
: AUTH_FLOW_STRATEGY.Password(this.injector);
|
|
||||||
}),
|
|
||||||
switchMap(() => from(this.strategy.init())),
|
|
||||||
take(1),
|
|
||||||
)
|
|
||||||
.toPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(queryParams?: Params): Observable<any> {
|
|
||||||
return this.strategy.logout(queryParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToLogin(queryParams?: Params) {
|
|
||||||
this.strategy.navigateToLogin(queryParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
login(params: LoginParams) {
|
|
||||||
return this.strategy.login(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
import { Injectable, NgZone, Optional } from '@angular/core';
|
|
||||||
import { OAuthService } from 'angular-oauth2-oidc';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class TimeoutLimitedOAuthService extends OAuthService {
|
|
||||||
protected override calcTimeout(storedAt: number, expiration: number): number {
|
|
||||||
const result = super.calcTimeout(storedAt, expiration);
|
|
||||||
const MAX_TIMEOUT_DURATION = 2147483647;
|
|
||||||
return result < MAX_TIMEOUT_DURATION ? result : MAX_TIMEOUT_DURATION - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,262 +0,0 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
|
||||||
import { Injector } from '@angular/core';
|
|
||||||
import { Params, Router } from '@angular/router';
|
|
||||||
import {
|
|
||||||
AuthConfig,
|
|
||||||
OAuthErrorEvent,
|
|
||||||
OAuthInfoEvent,
|
|
||||||
OAuthService,
|
|
||||||
OAuthStorage
|
|
||||||
} from 'angular-oauth2-oidc';
|
|
||||||
import { from, Observable, of, pipe } from 'rxjs';
|
|
||||||
import { filter, switchMap, tap } from 'rxjs/operators';
|
|
||||||
import { LoginParams } from '../models/auth';
|
|
||||||
import { ConfigStateService } from '../services/config-state.service';
|
|
||||||
import { EnvironmentService } from '../services/environment.service';
|
|
||||||
import { HttpErrorReporterService } from '../services/http-error-reporter.service';
|
|
||||||
import { SessionStateService } from '../services/session-state.service';
|
|
||||||
import { TENANT_KEY } from '../tokens/tenant-key.token';
|
|
||||||
import { removeRememberMe, setRememberMe } from '../utils/auth-utils';
|
|
||||||
import { noop } from '../utils/common-utils';
|
|
||||||
|
|
||||||
export const oAuthStorage = localStorage;
|
|
||||||
|
|
||||||
export abstract class AuthFlowStrategy {
|
|
||||||
abstract readonly isInternalAuth: boolean;
|
|
||||||
|
|
||||||
protected httpErrorReporter: HttpErrorReporterService;
|
|
||||||
protected environment: EnvironmentService;
|
|
||||||
protected configState: ConfigStateService;
|
|
||||||
protected oAuthService: OAuthService;
|
|
||||||
protected oAuthConfig: AuthConfig;
|
|
||||||
protected sessionState: SessionStateService;
|
|
||||||
protected tenantKey: string;
|
|
||||||
|
|
||||||
abstract checkIfInternalAuth(queryParams?: Params): boolean;
|
|
||||||
abstract navigateToLogin(queryParams?: Params): void;
|
|
||||||
abstract logout(queryParams?: Params): Observable<any>;
|
|
||||||
abstract login(params?: LoginParams | Params): Observable<any>;
|
|
||||||
|
|
||||||
private catchError = err => {
|
|
||||||
this.httpErrorReporter.reportError(err);
|
|
||||||
return of(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(protected injector: Injector) {
|
|
||||||
this.httpErrorReporter = injector.get(HttpErrorReporterService);
|
|
||||||
this.environment = injector.get(EnvironmentService);
|
|
||||||
this.configState = injector.get(ConfigStateService);
|
|
||||||
this.oAuthService = injector.get(OAuthService);
|
|
||||||
this.sessionState = injector.get(SessionStateService);
|
|
||||||
this.oAuthConfig = this.environment.getEnvironment().oAuthConfig;
|
|
||||||
this.tenantKey = injector.get(TENANT_KEY);
|
|
||||||
|
|
||||||
this.listenToOauthErrors();
|
|
||||||
}
|
|
||||||
|
|
||||||
async init(): Promise<any> {
|
|
||||||
const shouldClear = shouldStorageClear(
|
|
||||||
this.environment.getEnvironment().oAuthConfig.clientId,
|
|
||||||
oAuthStorage,
|
|
||||||
);
|
|
||||||
if (shouldClear) clearOAuthStorage(oAuthStorage);
|
|
||||||
|
|
||||||
this.oAuthService.configure(this.oAuthConfig);
|
|
||||||
|
|
||||||
this.oAuthService.events
|
|
||||||
.pipe(filter(event => event.type === 'token_refresh_error'))
|
|
||||||
.subscribe(() => this.navigateToLogin());
|
|
||||||
|
|
||||||
return this.oAuthService
|
|
||||||
.loadDiscoveryDocument()
|
|
||||||
.then(() => {
|
|
||||||
if (this.oAuthService.hasValidAccessToken() || !this.oAuthService.getRefreshToken()) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.refreshToken();
|
|
||||||
})
|
|
||||||
.catch(this.catchError);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected refreshToken() {
|
|
||||||
return this.oAuthService.refreshToken().catch(() => clearOAuthStorage());
|
|
||||||
}
|
|
||||||
|
|
||||||
protected listenToOauthErrors() {
|
|
||||||
this.oAuthService.events
|
|
||||||
.pipe(
|
|
||||||
filter(event => event instanceof OAuthErrorEvent),
|
|
||||||
tap(() => clearOAuthStorage()),
|
|
||||||
switchMap(() => this.configState.refreshAppState()),
|
|
||||||
)
|
|
||||||
.subscribe();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AuthCodeFlowStrategy extends AuthFlowStrategy {
|
|
||||||
readonly isInternalAuth = false;
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
return super
|
|
||||||
.init()
|
|
||||||
.then(() => this.oAuthService.tryLogin().catch(noop))
|
|
||||||
.then(() => this.oAuthService.setupAutomaticSilentRefresh({}, 'access_token'));
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToLogin(queryParams?: Params) {
|
|
||||||
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfInternalAuth(queryParams?: Params) {
|
|
||||||
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(queryParams?: Params) {
|
|
||||||
return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams)));
|
|
||||||
}
|
|
||||||
|
|
||||||
login(queryParams?: Params) {
|
|
||||||
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
|
|
||||||
return of(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getCultureParams(queryParams?: Params) {
|
|
||||||
const lang = this.sessionState.getLanguage();
|
|
||||||
const culture = { culture: lang, 'ui-culture': lang };
|
|
||||||
return { ...(lang && culture), ...queryParams };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
|
|
||||||
readonly isInternalAuth = true;
|
|
||||||
private cookieKey = 'rememberMe';
|
|
||||||
private storageKey = 'passwordFlow';
|
|
||||||
|
|
||||||
private listenToTokenExpiration() {
|
|
||||||
this.oAuthService.events
|
|
||||||
.pipe(
|
|
||||||
filter(
|
|
||||||
event =>
|
|
||||||
event instanceof OAuthInfoEvent &&
|
|
||||||
event.type === 'token_expires' &&
|
|
||||||
event.info === 'access_token',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.subscribe(() => {
|
|
||||||
if (this.oAuthService.getRefreshToken()) {
|
|
||||||
this.refreshToken();
|
|
||||||
} else {
|
|
||||||
this.oAuthService.logOut();
|
|
||||||
removeRememberMe();
|
|
||||||
this.configState.refreshAppState().subscribe();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (!getCookieValueByName(this.cookieKey) && localStorage.getItem(this.storageKey)) {
|
|
||||||
this.oAuthService.logOut();
|
|
||||||
}
|
|
||||||
|
|
||||||
return super.init().then(() => this.listenToTokenExpiration());
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateToLogin(queryParams?: Params) {
|
|
||||||
const router = this.injector.get(Router);
|
|
||||||
router.navigate(['/account/login'], { queryParams });
|
|
||||||
}
|
|
||||||
|
|
||||||
checkIfInternalAuth() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
login(params: LoginParams): Observable<any> {
|
|
||||||
const tenant = this.sessionState.getTenant();
|
|
||||||
|
|
||||||
return from(
|
|
||||||
this.oAuthService.fetchTokenUsingPasswordFlow(
|
|
||||||
params.username,
|
|
||||||
params.password,
|
|
||||||
new HttpHeaders({ ...(tenant && tenant.id && { [this.tenantKey]: tenant.id }) }),
|
|
||||||
),
|
|
||||||
).pipe(this.pipeToLogin(params));
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeToLogin(params: Pick<LoginParams, 'redirectUrl' | 'rememberMe'>) {
|
|
||||||
const router = this.injector.get(Router);
|
|
||||||
|
|
||||||
return pipe(
|
|
||||||
switchMap(() => this.configState.refreshAppState()),
|
|
||||||
tap(() => {
|
|
||||||
setRememberMe(params.rememberMe);
|
|
||||||
if (params.redirectUrl) router.navigate([params.redirectUrl]);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(queryParams?: Params) {
|
|
||||||
const router = this.injector.get(Router);
|
|
||||||
|
|
||||||
return from(this.oAuthService.revokeTokenAndLogout(queryParams)).pipe(
|
|
||||||
switchMap(() => this.configState.refreshAppState()),
|
|
||||||
tap(() => {
|
|
||||||
router.navigateByUrl('/');
|
|
||||||
removeRememberMe();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected refreshToken() {
|
|
||||||
return this.oAuthService.refreshToken().catch(() => {
|
|
||||||
clearOAuthStorage();
|
|
||||||
removeRememberMe();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AUTH_FLOW_STRATEGY = {
|
|
||||||
Code(injector: Injector) {
|
|
||||||
return new AuthCodeFlowStrategy(injector);
|
|
||||||
},
|
|
||||||
Password(injector: Injector) {
|
|
||||||
return new AuthPasswordFlowStrategy(injector);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function clearOAuthStorage(storage: OAuthStorage = oAuthStorage) {
|
|
||||||
const keys = [
|
|
||||||
'access_token',
|
|
||||||
'id_token',
|
|
||||||
'refresh_token',
|
|
||||||
'nonce',
|
|
||||||
'PKCE_verifier',
|
|
||||||
'expires_at',
|
|
||||||
'id_token_claims_obj',
|
|
||||||
'id_token_expires_at',
|
|
||||||
'id_token_stored_at',
|
|
||||||
'access_token_stored_at',
|
|
||||||
'granted_scopes',
|
|
||||||
'session_state',
|
|
||||||
];
|
|
||||||
|
|
||||||
keys.forEach(key => storage.removeItem(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldStorageClear(clientId: string, storage: OAuthStorage): boolean {
|
|
||||||
const key = 'abpOAuthClientId';
|
|
||||||
if (!storage.getItem(key)) {
|
|
||||||
storage.setItem(key, clientId);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldClear = storage.getItem(key) !== clientId;
|
|
||||||
if (shouldClear) storage.setItem(key, clientId);
|
|
||||||
return shouldClear;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCookieValueByName(name: string) {
|
|
||||||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
|
||||||
return match ? match[2] : '';
|
|
||||||
}
|
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { CheckAuthenticationStateFn } from '../models/auth';
|
||||||
|
|
||||||
|
export const CHECK_AUTHENTICATION_STATE_FN_KEY = new InjectionToken<CheckAuthenticationStateFn>(
|
||||||
|
'CHECK_AUTHENTICATION_STATE_FN_KEY',
|
||||||
|
);
|
||||||
@ -1,21 +1,5 @@
|
|||||||
import { InjectionToken, inject } from '@angular/core';
|
import { InjectionToken } from '@angular/core';
|
||||||
import { EnvironmentService } from '../services/environment.service';
|
|
||||||
|
|
||||||
export const NAVIGATE_TO_MANAGE_PROFILE = new InjectionToken<() => void>(
|
export const NAVIGATE_TO_MANAGE_PROFILE = new InjectionToken<() => void>(
|
||||||
'NAVIGATE_TO_MANAGE_PROFILE',
|
'NAVIGATE_TO_MANAGE_PROFILE',
|
||||||
{
|
|
||||||
providedIn: 'root',
|
|
||||||
factory: () => {
|
|
||||||
const environment = inject(EnvironmentService);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.open(
|
|
||||||
`${environment.getEnvironment().oAuthConfig.issuer}/Account/Manage?returnUrl=${
|
|
||||||
window.location.href
|
|
||||||
}`,
|
|
||||||
'_self',
|
|
||||||
);
|
|
||||||
};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|||||||
@ -0,0 +1,4 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { PipeToLoginFn } from '../models/auth';
|
||||||
|
|
||||||
|
export const PIPE_TO_LOGIN_FN_KEY = new InjectionToken<PipeToLoginFn>('PIPE_TO_LOGIN_FN_KEY');
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import { SetTokenResponseToStorageFn } from '../models';
|
||||||
|
|
||||||
|
export const SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY = new InjectionToken<SetTokenResponseToStorageFn>(
|
||||||
|
'SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY',
|
||||||
|
);
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"extends": ["../../.eslintrc.json"],
|
||||||
|
"ignorePatterns": ["!**/*"],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ["*.ts"],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@nrwl/nx/angular",
|
||||||
|
"plugin:@angular-eslint/template/process-inline-templates"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@angular-eslint/directive-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "attribute",
|
||||||
|
"prefix": "abp",
|
||||||
|
"style": "camelCase"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@angular-eslint/component-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"prefix": "abp",
|
||||||
|
"style": "kebab-case"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": ["*.html"],
|
||||||
|
"extends": ["plugin:@nrwl/nx/angular-template"],
|
||||||
|
"rules": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,7 @@
|
|||||||
|
# oauth
|
||||||
|
|
||||||
|
This library was generated with [Nx](https://nx.dev).
|
||||||
|
|
||||||
|
## Running unit tests
|
||||||
|
|
||||||
|
Run `nx test oauth` to execute the unit tests.
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
export default {
|
||||||
|
displayName: 'oauth',
|
||||||
|
preset: '../../jest.preset.js',
|
||||||
|
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||||
|
globals: {
|
||||||
|
'ts-jest': {
|
||||||
|
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||||
|
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
coverageDirectory: '../../coverage/packages/oauth',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(ts|mjs|js|html)$': 'jest-preset-angular',
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||||
|
snapshotSerializers: [
|
||||||
|
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||||
|
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||||
|
'jest-preset-angular/build/serializers/html-comment',
|
||||||
|
],
|
||||||
|
};
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
|
||||||
|
"dest": "../../dist/packages/oauth",
|
||||||
|
"lib": {
|
||||||
|
"entryFile": "src/public-api.ts"
|
||||||
|
},
|
||||||
|
"allowedNonPeerDependencies": [
|
||||||
|
"@abp/utils",
|
||||||
|
"@abp/ng.core",
|
||||||
|
"angular-oauth2-oidc",
|
||||||
|
"just-clone",
|
||||||
|
"just-compare"
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@abp/ng.oauth",
|
||||||
|
"version": "7.0.0-rc.4",
|
||||||
|
"homepage": "https://abp.io",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/abpframework/abp.git"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@abp/utils": "~7.0.0-rc.4",
|
||||||
|
"@abp/ng.core": "~7.0.0-rc.4",
|
||||||
|
"angular-oauth2-oidc": "^15.0.1",
|
||||||
|
"just-clone": "^6.1.1",
|
||||||
|
"just-compare": "^1.4.0",
|
||||||
|
"tslib": "^2.0.0"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './oauth.guard';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './oauth-configuration.handler';
|
||||||
@ -0,0 +1,51 @@
|
|||||||
|
import { HttpHandler, HttpHeaders, HttpRequest } from '@angular/common/http';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
import { finalize } from 'rxjs/operators';
|
||||||
|
import { SessionStateService, HttpWaitService, TENANT_KEY, IApiInterceptor } from '@abp/ng.core';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class OAuthApiInterceptor implements IApiInterceptor {
|
||||||
|
constructor(
|
||||||
|
private oAuthService: OAuthService,
|
||||||
|
private sessionState: SessionStateService,
|
||||||
|
private httpWaitService: HttpWaitService,
|
||||||
|
@Inject(TENANT_KEY) private tenantKey: string,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
intercept(request: HttpRequest<any>, next: HttpHandler) {
|
||||||
|
this.httpWaitService.addRequest(request);
|
||||||
|
return next
|
||||||
|
.handle(
|
||||||
|
request.clone({
|
||||||
|
setHeaders: this.getAdditionalHeaders(request.headers),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.pipe(finalize(() => this.httpWaitService.deleteRequest(request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
getAdditionalHeaders(existingHeaders?: HttpHeaders) {
|
||||||
|
const headers = {} as any;
|
||||||
|
|
||||||
|
const token = this.oAuthService.getAccessToken();
|
||||||
|
if (!existingHeaders?.has('Authorization') && token) {
|
||||||
|
headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = this.sessionState.getLanguage();
|
||||||
|
if (!existingHeaders?.has('Accept-Language') && lang) {
|
||||||
|
headers['Accept-Language'] = lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tenant = this.sessionState.getTenant();
|
||||||
|
if (!existingHeaders?.has(this.tenantKey) && tenant?.id) {
|
||||||
|
headers[this.tenantKey] = tenant.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
headers['X-Requested-With'] = 'XMLHttpRequest';
|
||||||
|
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './api.interceptor';
|
||||||
@ -0,0 +1,71 @@
|
|||||||
|
import { APP_INITIALIZER, Injector, ModuleWithProviders, NgModule } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
|
||||||
|
import {
|
||||||
|
ApiInterceptor,
|
||||||
|
AuthGuard,
|
||||||
|
AuthService,
|
||||||
|
CHECK_AUTHENTICATION_STATE_FN_KEY,
|
||||||
|
noop,
|
||||||
|
PIPE_TO_LOGIN_FN_KEY,
|
||||||
|
SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY,
|
||||||
|
} from '@abp/ng.core';
|
||||||
|
import { storageFactory } from './utils/storage.factory';
|
||||||
|
import { AbpOAuthService } from './services';
|
||||||
|
import { OAuthConfigurationHandler } from './handlers/oauth-configuration.handler';
|
||||||
|
import { HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||||
|
import { OAuthApiInterceptor } from './interceptors/api.interceptor';
|
||||||
|
import { AbpOAuthGuard } from './guards/oauth.guard';
|
||||||
|
import { NavigateToManageProfileProvider } from './providers';
|
||||||
|
import { checkAccessToken, pipeToLogin, setTokenResponseToStorage } from './utils';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [CommonModule, OAuthModule],
|
||||||
|
})
|
||||||
|
export class AbpOAuthModule {
|
||||||
|
static forRoot(): ModuleWithProviders<AbpOAuthModule> {
|
||||||
|
return {
|
||||||
|
ngModule: AbpOAuthModule,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useClass: AbpOAuthService,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: AuthGuard,
|
||||||
|
useClass: AbpOAuthGuard,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: ApiInterceptor,
|
||||||
|
useClass: OAuthApiInterceptor,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: PIPE_TO_LOGIN_FN_KEY,
|
||||||
|
useValue: pipeToLogin,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY,
|
||||||
|
useValue: setTokenResponseToStorage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: CHECK_AUTHENTICATION_STATE_FN_KEY,
|
||||||
|
useValue: checkAccessToken,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useExisting: ApiInterceptor,
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
NavigateToManageProfileProvider,
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [OAuthConfigurationHandler],
|
||||||
|
useFactory: noop,
|
||||||
|
},
|
||||||
|
OAuthModule.forRoot().providers,
|
||||||
|
{ provide: OAuthStorage, useFactory: storageFactory },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './navigate-to-manage-profile.provider';
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { inject, Provider } from '@angular/core';
|
||||||
|
import { EnvironmentService, NAVIGATE_TO_MANAGE_PROFILE } from '@abp/ng.core';
|
||||||
|
|
||||||
|
export const NavigateToManageProfileProvider: Provider = {
|
||||||
|
provide: NAVIGATE_TO_MANAGE_PROFILE,
|
||||||
|
useFactory: () => {
|
||||||
|
const environment = inject(EnvironmentService);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const env = environment.getEnvironment();
|
||||||
|
if (!env.oAuthConfig) {
|
||||||
|
console.warn('The oAuthConfig env is missing on environment.ts');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.open(
|
||||||
|
`${env.oAuthConfig.issuer}/Account/Manage?returnUrl=${window.location.href}`,
|
||||||
|
'_self',
|
||||||
|
);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './oauth.service';
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import { Injectable, Injector } from '@angular/core';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { from, Observable, lastValueFrom } from 'rxjs';
|
||||||
|
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import { IAuthService, LoginParams } from '@abp/ng.core';
|
||||||
|
import { AuthFlowStrategy } from '../strategies';
|
||||||
|
import { EnvironmentService } from '@abp/ng.core';
|
||||||
|
import { AUTH_FLOW_STRATEGY } from '../tokens/auth-flow-strategy';
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class AbpOAuthService implements IAuthService {
|
||||||
|
private strategy: AuthFlowStrategy;
|
||||||
|
|
||||||
|
get isInternalAuth() {
|
||||||
|
return this.strategy.isInternalAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(protected injector: Injector, private oAuthService: OAuthService) {}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
const environmentService = this.injector.get(EnvironmentService);
|
||||||
|
|
||||||
|
const result$ = environmentService.getEnvironment$().pipe(
|
||||||
|
map(env => env?.oAuthConfig),
|
||||||
|
filter(oAuthConfig => !!oAuthConfig),
|
||||||
|
tap(oAuthConfig => {
|
||||||
|
this.strategy =
|
||||||
|
oAuthConfig.responseType === 'code'
|
||||||
|
? AUTH_FLOW_STRATEGY.Code(this.injector)
|
||||||
|
: AUTH_FLOW_STRATEGY.Password(this.injector);
|
||||||
|
}),
|
||||||
|
switchMap(() => from(this.strategy.init())),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
|
||||||
|
return await lastValueFrom(result$);
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(queryParams?: Params): Observable<any> {
|
||||||
|
return this.strategy.logout(queryParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToLogin(queryParams?: Params) {
|
||||||
|
this.strategy.navigateToLogin(queryParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
login(params: LoginParams) {
|
||||||
|
return this.strategy.login(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
get isAuthenticated(): boolean {
|
||||||
|
return this.oAuthService.hasValidAccessToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import { noop } from '@abp/ng.core';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import { from, of } from 'rxjs';
|
||||||
|
import { AuthFlowStrategy } from './auth-flow-strategy';
|
||||||
|
|
||||||
|
export class AuthCodeFlowStrategy extends AuthFlowStrategy {
|
||||||
|
readonly isInternalAuth = false;
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
return super
|
||||||
|
.init()
|
||||||
|
.then(() => this.oAuthService.tryLogin().catch(noop))
|
||||||
|
.then(() => this.oAuthService.setupAutomaticSilentRefresh({}, 'access_token'));
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToLogin(queryParams?: Params) {
|
||||||
|
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfInternalAuth(queryParams?: Params) {
|
||||||
|
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
logout(queryParams?: Params) {
|
||||||
|
return from(this.oAuthService.revokeTokenAndLogout(this.getCultureParams(queryParams)));
|
||||||
|
}
|
||||||
|
|
||||||
|
login(queryParams?: Params) {
|
||||||
|
this.oAuthService.initCodeFlow('', this.getCultureParams(queryParams));
|
||||||
|
return of(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getCultureParams(queryParams?: Params) {
|
||||||
|
const lang = this.sessionState.getLanguage();
|
||||||
|
const culture = { culture: lang, 'ui-culture': lang };
|
||||||
|
return { ...(lang && culture), ...queryParams };
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
import { Injector } from '@angular/core';
|
||||||
|
import { Params } from '@angular/router';
|
||||||
|
import {
|
||||||
|
AuthConfig,
|
||||||
|
OAuthErrorEvent,
|
||||||
|
OAuthService as OAuthService2,
|
||||||
|
OAuthStorage,
|
||||||
|
} from 'angular-oauth2-oidc';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { filter, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import {
|
||||||
|
LoginParams,
|
||||||
|
ConfigStateService,
|
||||||
|
EnvironmentService,
|
||||||
|
HttpErrorReporterService,
|
||||||
|
SessionStateService,
|
||||||
|
TENANT_KEY,
|
||||||
|
} from '@abp/ng.core';
|
||||||
|
import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
|
||||||
|
import { oAuthStorage } from '../utils/oauth-storage';
|
||||||
|
|
||||||
|
export abstract class AuthFlowStrategy {
|
||||||
|
abstract readonly isInternalAuth: boolean;
|
||||||
|
|
||||||
|
protected httpErrorReporter: HttpErrorReporterService;
|
||||||
|
protected environment: EnvironmentService;
|
||||||
|
protected configState: ConfigStateService;
|
||||||
|
protected oAuthService: OAuthService2;
|
||||||
|
protected oAuthConfig: AuthConfig;
|
||||||
|
protected sessionState: SessionStateService;
|
||||||
|
protected tenantKey: string;
|
||||||
|
|
||||||
|
abstract checkIfInternalAuth(queryParams?: Params): boolean;
|
||||||
|
|
||||||
|
abstract navigateToLogin(queryParams?: Params): void;
|
||||||
|
|
||||||
|
abstract logout(queryParams?: Params): Observable<any>;
|
||||||
|
|
||||||
|
abstract login(params?: LoginParams | Params): Observable<any>;
|
||||||
|
|
||||||
|
private catchError = err => {
|
||||||
|
this.httpErrorReporter.reportError(err);
|
||||||
|
return of(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(protected injector: Injector) {
|
||||||
|
this.httpErrorReporter = injector.get(HttpErrorReporterService);
|
||||||
|
this.environment = injector.get(EnvironmentService);
|
||||||
|
this.configState = injector.get(ConfigStateService);
|
||||||
|
this.oAuthService = injector.get(OAuthService2);
|
||||||
|
this.sessionState = injector.get(SessionStateService);
|
||||||
|
this.oAuthConfig = this.environment.getEnvironment().oAuthConfig;
|
||||||
|
this.tenantKey = injector.get(TENANT_KEY);
|
||||||
|
|
||||||
|
this.listenToOauthErrors();
|
||||||
|
}
|
||||||
|
|
||||||
|
async init(): Promise<any> {
|
||||||
|
const shouldClear = shouldStorageClear(
|
||||||
|
this.environment.getEnvironment().oAuthConfig.clientId,
|
||||||
|
oAuthStorage,
|
||||||
|
);
|
||||||
|
if (shouldClear) clearOAuthStorage(oAuthStorage);
|
||||||
|
|
||||||
|
this.oAuthService.configure(this.oAuthConfig);
|
||||||
|
|
||||||
|
this.oAuthService.events
|
||||||
|
.pipe(filter(event => event.type === 'token_refresh_error'))
|
||||||
|
.subscribe(() => this.navigateToLogin());
|
||||||
|
|
||||||
|
return this.oAuthService
|
||||||
|
.loadDiscoveryDocument()
|
||||||
|
.then(() => {
|
||||||
|
if (this.oAuthService.hasValidAccessToken() || !this.oAuthService.getRefreshToken()) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.refreshToken();
|
||||||
|
})
|
||||||
|
.catch(this.catchError);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected refreshToken() {
|
||||||
|
return this.oAuthService.refreshToken().catch(() => clearOAuthStorage());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected listenToOauthErrors() {
|
||||||
|
this.oAuthService.events
|
||||||
|
.pipe(
|
||||||
|
filter(event => event instanceof OAuthErrorEvent),
|
||||||
|
tap(() => clearOAuthStorage()),
|
||||||
|
switchMap(() => this.configState.refreshAppState()),
|
||||||
|
)
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldStorageClear(clientId: string, storage: OAuthStorage): boolean {
|
||||||
|
const key = 'abpOAuthClientId';
|
||||||
|
if (!storage.getItem(key)) {
|
||||||
|
storage.setItem(key, clientId);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldClear = storage.getItem(key) !== clientId;
|
||||||
|
if (shouldClear) storage.setItem(key, clientId);
|
||||||
|
return shouldClear;
|
||||||
|
}
|
||||||
@ -0,0 +1,88 @@
|
|||||||
|
import { filter, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { OAuthInfoEvent } from 'angular-oauth2-oidc';
|
||||||
|
import { Params, Router } from '@angular/router';
|
||||||
|
import { from, Observable, pipe } from 'rxjs';
|
||||||
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
|
import { AuthFlowStrategy } from './auth-flow-strategy';
|
||||||
|
import { pipeToLogin, removeRememberMe, setRememberMe } from '../utils/auth-utils';
|
||||||
|
import { LoginParams } from '@abp/ng.core';
|
||||||
|
import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
|
||||||
|
|
||||||
|
function getCookieValueByName(name: string) {
|
||||||
|
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
|
||||||
|
return match ? match[2] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthPasswordFlowStrategy extends AuthFlowStrategy {
|
||||||
|
readonly isInternalAuth = true;
|
||||||
|
private cookieKey = 'rememberMe';
|
||||||
|
private storageKey = 'passwordFlow';
|
||||||
|
|
||||||
|
private listenToTokenExpiration() {
|
||||||
|
this.oAuthService.events
|
||||||
|
.pipe(
|
||||||
|
filter(
|
||||||
|
event =>
|
||||||
|
event instanceof OAuthInfoEvent &&
|
||||||
|
event.type === 'token_expires' &&
|
||||||
|
event.info === 'access_token',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subscribe(() => {
|
||||||
|
if (this.oAuthService.getRefreshToken()) {
|
||||||
|
this.refreshToken();
|
||||||
|
} else {
|
||||||
|
this.oAuthService.logOut();
|
||||||
|
removeRememberMe();
|
||||||
|
this.configState.refreshAppState().subscribe();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!getCookieValueByName(this.cookieKey) && localStorage.getItem(this.storageKey)) {
|
||||||
|
this.oAuthService.logOut();
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.init().then(() => this.listenToTokenExpiration());
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateToLogin(queryParams?: Params) {
|
||||||
|
const router = this.injector.get(Router);
|
||||||
|
return router.navigate(['/account/login'], { queryParams });
|
||||||
|
}
|
||||||
|
|
||||||
|
checkIfInternalAuth() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
login(params: LoginParams): Observable<any> {
|
||||||
|
const tenant = this.sessionState.getTenant();
|
||||||
|
|
||||||
|
return from(
|
||||||
|
this.oAuthService.fetchTokenUsingPasswordFlow(
|
||||||
|
params.username,
|
||||||
|
params.password,
|
||||||
|
new HttpHeaders({ ...(tenant && tenant.id && { [this.tenantKey]: tenant.id }) }),
|
||||||
|
),
|
||||||
|
).pipe(pipeToLogin(params, this.injector));
|
||||||
|
}
|
||||||
|
logout(queryParams?: Params) {
|
||||||
|
const router = this.injector.get(Router);
|
||||||
|
|
||||||
|
return from(this.oAuthService.revokeTokenAndLogout(queryParams)).pipe(
|
||||||
|
switchMap(() => this.configState.refreshAppState()),
|
||||||
|
tap(() => {
|
||||||
|
router.navigateByUrl('/');
|
||||||
|
removeRememberMe();
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected refreshToken() {
|
||||||
|
return this.oAuthService.refreshToken().catch(() => {
|
||||||
|
clearOAuthStorage();
|
||||||
|
removeRememberMe();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
export * from './auth-flow-strategy';
|
||||||
|
export * from './auth-code-flow-strategy';
|
||||||
|
export * from './auth-password-flow-strategy';
|
||||||
@ -1,13 +1,13 @@
|
|||||||
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
|
||||||
import { OAuthService } from 'angular-oauth2-oidc';
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
import { AuthGuard } from '../guards/auth.guard';
|
import { AbpOAuthGuard } from '../guards/oauth.guard';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '@Abp/ng.core';
|
||||||
|
|
||||||
describe('AuthGuard', () => {
|
describe('AuthGuard', () => {
|
||||||
let spectator: SpectatorService<AuthGuard>;
|
let spectator: SpectatorService<AbpOAuthGuard>;
|
||||||
let guard: AuthGuard;
|
let guard: AbpOAuthGuard;
|
||||||
const createService = createServiceFactory({
|
const createService = createServiceFactory({
|
||||||
service: AuthGuard,
|
service: AbpOAuthGuard,
|
||||||
mocks: [OAuthService, AuthService],
|
mocks: [OAuthService, AuthService],
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
import { Component, Injector } from '@angular/core';
|
||||||
|
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CORE_OPTIONS,
|
||||||
|
EnvironmentService,
|
||||||
|
AuthService,
|
||||||
|
ConfigStateService,
|
||||||
|
AbpApplicationConfigurationService,
|
||||||
|
SessionStateService,
|
||||||
|
ApplicationConfigurationDto,
|
||||||
|
} from '@abp/ng.core';
|
||||||
|
import * as clearOAuthStorageDefault from '../utils/clear-o-auth-storage';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { checkAccessToken } from '../utils/check-access-token';
|
||||||
|
|
||||||
|
const environment = { oAuthConfig: { issuer: 'test' } };
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'abp-dummy',
|
||||||
|
template: '',
|
||||||
|
})
|
||||||
|
export class DummyComponent {}
|
||||||
|
|
||||||
|
describe('InitialUtils', () => {
|
||||||
|
let spectator: Spectator<DummyComponent>;
|
||||||
|
const createComponent = createComponentFactory({
|
||||||
|
component: DummyComponent,
|
||||||
|
mocks: [
|
||||||
|
EnvironmentService,
|
||||||
|
ConfigStateService,
|
||||||
|
AbpApplicationConfigurationService,
|
||||||
|
AuthService,
|
||||||
|
OAuthService,
|
||||||
|
SessionStateService,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CORE_OPTIONS,
|
||||||
|
useValue: {
|
||||||
|
environment,
|
||||||
|
registerLocaleFn: () => Promise.resolve(),
|
||||||
|
skipGetAppConfiguration: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => (spectator = createComponent()));
|
||||||
|
|
||||||
|
describe('#getInitialData', () => {
|
||||||
|
let mockInjector;
|
||||||
|
let configStateService;
|
||||||
|
let authService;
|
||||||
|
beforeEach(() => {
|
||||||
|
mockInjector = {
|
||||||
|
get: spectator.inject,
|
||||||
|
};
|
||||||
|
configStateService = spectator.inject(ConfigStateService);
|
||||||
|
authService = spectator.inject(AuthService);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should called configStateService.refreshAppState', async () => {
|
||||||
|
const configRefreshAppStateSpy = jest.spyOn(configStateService, 'refreshAppState');
|
||||||
|
const appConfigRes = {
|
||||||
|
currentTenant: { id: 'test', name: 'testing' },
|
||||||
|
} as ApplicationConfigurationDto;
|
||||||
|
|
||||||
|
configRefreshAppStateSpy.mockReturnValue(of(appConfigRes));
|
||||||
|
|
||||||
|
// Todo: refactor it
|
||||||
|
// await initFactory(mockInjector)();
|
||||||
|
|
||||||
|
expect(configRefreshAppStateSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#checkAccessToken', () => {
|
||||||
|
let injector;
|
||||||
|
let injectorSpy;
|
||||||
|
let clearOAuthStorageSpy;
|
||||||
|
beforeEach(() => {
|
||||||
|
injector = spectator.inject(Injector);
|
||||||
|
injectorSpy = jest.spyOn(injector, 'get');
|
||||||
|
clearOAuthStorageSpy = jest.spyOn(clearOAuthStorageDefault, 'clearOAuthStorage');
|
||||||
|
clearOAuthStorageSpy.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call logOut fn of OAuthService when token is valid and current user not found', async () => {
|
||||||
|
injectorSpy.mockReturnValueOnce({ getDeep: () => false });
|
||||||
|
injectorSpy.mockReturnValueOnce({ hasValidAccessToken: () => true });
|
||||||
|
checkAccessToken(injector);
|
||||||
|
expect(clearOAuthStorageSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not call logOut fn of OAuthService when token is invalid', async () => {
|
||||||
|
injectorSpy.mockReturnValueOnce({ getDeep: () => true });
|
||||||
|
injectorSpy.mockReturnValueOnce({ hasValidAccessToken: () => false });
|
||||||
|
checkAccessToken(injector);
|
||||||
|
expect(clearOAuthStorageSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not call logOut fn of OAuthService when token is valid but user is not found', async () => {
|
||||||
|
injectorSpy.mockReturnValueOnce({ getDeep: () => true });
|
||||||
|
injectorSpy.mockReturnValueOnce({ hasValidAccessToken: () => true });
|
||||||
|
checkAccessToken(injector);
|
||||||
|
expect(clearOAuthStorageSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
describe('Test', () => {
|
||||||
|
it('should be passed', () => {
|
||||||
|
expect(true).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { Injector } from '@angular/core';
|
||||||
|
import { AuthCodeFlowStrategy } from '../strategies/auth-code-flow-strategy';
|
||||||
|
import { AuthPasswordFlowStrategy } from '../strategies/auth-password-flow-strategy';
|
||||||
|
|
||||||
|
export const AUTH_FLOW_STRATEGY = {
|
||||||
|
Code(injector: Injector) {
|
||||||
|
return new AuthCodeFlowStrategy(injector);
|
||||||
|
},
|
||||||
|
Password(injector: Injector) {
|
||||||
|
return new AuthPasswordFlowStrategy(injector);
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './auth-flow-strategy';
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import { Injector } from '@angular/core';
|
||||||
|
import { CheckAuthenticationStateFn, ConfigStateService } from '@abp/ng.core';
|
||||||
|
import { OAuthService } from 'angular-oauth2-oidc';
|
||||||
|
import { clearOAuthStorage } from './clear-o-auth-storage';
|
||||||
|
|
||||||
|
export const checkAccessToken: CheckAuthenticationStateFn = function (injector: Injector) {
|
||||||
|
const configState = injector.get(ConfigStateService);
|
||||||
|
const oAuth = injector.get(OAuthService);
|
||||||
|
if (oAuth.hasValidAccessToken() && !configState.getDeep('currentUser.id')) {
|
||||||
|
clearOAuthStorage();
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
import { OAuthStorage } from 'angular-oauth2-oidc';
|
||||||
|
import { oAuthStorage } from './oauth-storage';
|
||||||
|
|
||||||
|
export function clearOAuthStorage(storage: OAuthStorage = oAuthStorage) {
|
||||||
|
const keys = [
|
||||||
|
'access_token',
|
||||||
|
'id_token',
|
||||||
|
'refresh_token',
|
||||||
|
'nonce',
|
||||||
|
'PKCE_verifier',
|
||||||
|
'expires_at',
|
||||||
|
'id_token_claims_obj',
|
||||||
|
'id_token_expires_at',
|
||||||
|
'id_token_stored_at',
|
||||||
|
'access_token_stored_at',
|
||||||
|
'granted_scopes',
|
||||||
|
'session_state',
|
||||||
|
];
|
||||||
|
|
||||||
|
keys.forEach(key => storage.removeItem(key));
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
export * from './oauth-storage';
|
||||||
|
export * from './storage.factory';
|
||||||
|
export * from './auth-utils';
|
||||||
|
export * from './clear-o-auth-storage';
|
||||||
|
export * from './check-access-token';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export const oAuthStorage = localStorage;
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { OAuthStorage } from 'angular-oauth2-oidc';
|
||||||
|
import { oAuthStorage } from './oauth-storage';
|
||||||
|
|
||||||
|
export function storageFactory(): OAuthStorage {
|
||||||
|
return oAuthStorage;
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
export * from './lib/oauth.module';
|
||||||
|
export * from './lib/utils';
|
||||||
|
export * from './lib/tokens';
|
||||||
|
export * from './lib/services';
|
||||||
|
export * from './lib/strategies';
|
||||||
|
export * from './lib/handlers';
|
||||||
|
export * from './lib/interceptors';
|
||||||
|
export * from './lib/guards';
|
||||||
|
export * from './lib/providers';
|
||||||
@ -0,0 +1 @@
|
|||||||
|
import 'jest-preset-angular/setup-jest';
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.base.json",
|
||||||
|
"files": [],
|
||||||
|
"include": [],
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.spec.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "es2020"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"target": "ES2022",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"inlineSources": true,
|
||||||
|
"types": [],
|
||||||
|
"lib": ["dom", "es2018"],
|
||||||
|
"useDefineForClassFields": false
|
||||||
|
},
|
||||||
|
"exclude": ["src/test-setup.ts", "**/*.spec.ts", "jest.config.ts"],
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.lib.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"declarationMap": false,
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": false
|
||||||
|
},
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"compilationMode": "partial"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "../../dist/out-tsc",
|
||||||
|
"module": "commonjs",
|
||||||
|
"types": ["jest", "node"],
|
||||||
|
"esModuleInterop": true
|
||||||
|
},
|
||||||
|
"files": ["src/test-setup.ts"],
|
||||||
|
"include": ["**/*.ts"]
|
||||||
|
}
|
||||||
@ -1,15 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"angular.ng-template",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"ms-vscode.vscode-typescript-tslint-plugin",
|
|
||||||
"visualstudioexptteam.vscodeintellicode",
|
|
||||||
"christian-kohler.path-intellisense",
|
|
||||||
"christian-kohler.npm-intellisense",
|
|
||||||
"Mikael.Angular-BeastCode",
|
|
||||||
"xabikos.JavaScriptSnippets",
|
|
||||||
"msjsdiag.debugger-for-chrome",
|
|
||||||
"donjayamanne.githistory",
|
|
||||||
"oderwat.indent-rainbow"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Loading…
Reference in new issue