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