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 './auth.guard'; |
|||
export * from './auth.service'; |
|||
|
|||
@ -1,2 +1 @@ |
|||
export * from './auth.guard'; |
|||
export * from './permission.guard'; |
|||
|
|||
@ -1,2 +1 @@ |
|||
export * from './oauth-configuration.handler'; |
|||
export * from './routes.handler'; |
|||
|
|||
@ -1,53 +1,24 @@ |
|||
import { HttpHandler, HttpHeaders, HttpInterceptor, HttpRequest } from '@angular/common/http'; |
|||
import { Inject, Injectable } from '@angular/core'; |
|||
import { OAuthService } from 'angular-oauth2-oidc'; |
|||
import { Injectable } from '@angular/core'; |
|||
import { finalize } from 'rxjs/operators'; |
|||
import { SessionStateService } from '../services/session-state.service'; |
|||
import { HttpWaitService } from '../services/http-wait.service'; |
|||
import { TENANT_KEY } from '../tokens/tenant-key.token'; |
|||
import { HttpWaitService } from '../services'; |
|||
|
|||
@Injectable({ |
|||
providedIn: 'root', |
|||
}) |
|||
export class ApiInterceptor implements HttpInterceptor { |
|||
constructor( |
|||
private oAuthService: OAuthService, |
|||
private sessionState: SessionStateService, |
|||
private httpWaitService: HttpWaitService, |
|||
@Inject(TENANT_KEY) private tenantKey: string, |
|||
) {} |
|||
export class ApiInterceptor implements IApiInterceptor { |
|||
constructor(private httpWaitService: HttpWaitService) {} |
|||
|
|||
getAdditionalHeaders(existingHeaders?: HttpHeaders) { |
|||
return existingHeaders; |
|||
} |
|||
|
|||
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))); |
|||
return next.handle(request).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; |
|||
} |
|||
export interface IApiInterceptor extends HttpInterceptor { |
|||
getAdditionalHeaders(existingHeaders?: HttpHeaders): HttpHeaders; |
|||
} |
|||
|
|||
@ -1,6 +1,17 @@ |
|||
import { UnaryFunction } from 'rxjs'; |
|||
import { Injector } from '@angular/core'; |
|||
|
|||
export interface LoginParams { |
|||
username: string; |
|||
password: string; |
|||
rememberMe?: boolean; |
|||
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 { EnvironmentService } from '../services/environment.service'; |
|||
import { InjectionToken } from '@angular/core'; |
|||
|
|||
export const NAVIGATE_TO_MANAGE_PROFILE = new InjectionToken<() => void>( |
|||
'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 { OAuthService } from 'angular-oauth2-oidc'; |
|||
import { AuthGuard } from '../guards/auth.guard'; |
|||
import { AuthService } from '../services/auth.service'; |
|||
import { AbpOAuthGuard } from '../guards/oauth.guard'; |
|||
import { AuthService } from '@Abp/ng.core'; |
|||
|
|||
describe('AuthGuard', () => { |
|||
let spectator: SpectatorService<AuthGuard>; |
|||
let guard: AuthGuard; |
|||
let spectator: SpectatorService<AbpOAuthGuard>; |
|||
let guard: AbpOAuthGuard; |
|||
const createService = createServiceFactory({ |
|||
service: AuthGuard, |
|||
service: AbpOAuthGuard, |
|||
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