mirror of https://github.com/abpframework/abp.git
16 changed files with 420 additions and 0 deletions
@ -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,12 @@ |
|||
{ |
|||
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json", |
|||
"dest": "../../dist/packages/core", |
|||
"lib": { |
|||
"entryFile": "src/public-api.ts" |
|||
}, |
|||
"allowedNonPeerDependencies": [ |
|||
"@abp/utils", |
|||
"@abp/ng.core", |
|||
"angular-oauth2-oidc" |
|||
] |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
{ |
|||
"name": "@abp/ng.core", |
|||
"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", |
|||
"ts-toolbelt": "6.15.4", |
|||
"tslib": "^2.0.0" |
|||
}, |
|||
"publishConfig": { |
|||
"access": "public" |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
import { ModuleWithProviders, NgModule } from '@angular/core'; |
|||
import { CommonModule } from '@angular/common'; |
|||
import { OAuthModule as OAuthModule2, OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; |
|||
import { TimeoutLimitedOAuthService } from '@abp/ng.core'; |
|||
import { storageFactory } from './utils/storage.factory'; |
|||
|
|||
@NgModule({ |
|||
imports: [CommonModule, OAuthModule2], |
|||
}) |
|||
export class OAuthModule { |
|||
static forRoot(): ModuleWithProviders<OAuthModule> { |
|||
return { |
|||
ngModule: OAuthModule, |
|||
providers: [ |
|||
OAuthModule2.forRoot().providers, |
|||
{ provide: OAuthStorage, useFactory: storageFactory }, |
|||
{ provide: OAuthService, useClass: TimeoutLimitedOAuthService }, |
|||
], |
|||
}; |
|||
} |
|||
} |
|||
@ -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,103 @@ |
|||
import { Injector } from '@angular/core'; |
|||
import { Params } from '@angular/router'; |
|||
import { AuthConfig, OAuthErrorEvent, OAuthService, OAuthStorage } from 'angular-oauth2-oidc'; |
|||
import { Observable, of } from 'rxjs'; |
|||
import { filter, switchMap, tap } from 'rxjs/operators'; |
|||
import { LoginParams } from '../models/auth'; |
|||
import { |
|||
ConfigStateService, |
|||
EnvironmentService, |
|||
HttpErrorReporterService, |
|||
SessionStateService, |
|||
TENANT_KEY, |
|||
} from '@abp/ng.core'; |
|||
import { oAuthStorage } from './oauth-storage'; |
|||
import { clearOAuthStorage } from './clear-o-auth-storage'; |
|||
|
|||
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(); |
|||
} |
|||
} |
|||
|
|||
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,101 @@ |
|||
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 { removeRememberMe, setRememberMe } from './auth-utils'; |
|||
import { LoginParams } from '../models'; |
|||
import { clearOAuthStorage } from './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(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(); |
|||
}); |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
export * from './auth-flow-strategy'; |
|||
export * from './auth-code-flow-strategy'; |
|||
export * from './auth-password-flow-strategy'; |
|||
@ -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"] |
|||
} |
|||
Loading…
Reference in new issue