mirror of https://github.com/abpframework/abp.git
committed by
GitHub
76 changed files with 1099 additions and 705 deletions
@ -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