mirror of https://github.com/abpframework/abp
Merge pull request #15201 from abpframework/issue/10019-move-oauth-seperated-package
Issue/10019 move oauth seperated packagepull/15239/head
commit
40029b08f1
@ -0,0 +1,14 @@
|
||||
# ABP OAuth Package
|
||||
The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0.
|
||||
|
||||
If your app is version 7.0 or higher, you should include "AbpOAuthModule.forRoot()" in your app.module.ts as an import after "CoreModule.forRoot(...)".
|
||||
|
||||
Those abstractions can be found in the @abp/ng-core packages.
|
||||
- `AuthService` (the class that implements the IAuthService interface).
|
||||
- `NAVIGATE_TO_MANAGE_PROFILE` Inject token.
|
||||
- `AuthGuard` (the class that implements the IAuthGuard interface).
|
||||
|
||||
Those base classes are overridden by the "AbpOAuthModule" for oAuth.
|
||||
If you want to make your own authentication system, you must also change these 'abstract' classes.
|
||||
|
||||
ApiInterceptor is provided by @abp/ng.oauth. The ApiInterceptor adds the token, accepted-language, and tenant id to the header of the HTTP request.
|
||||
@ -0,0 +1,13 @@
|
||||
import { CanActivate, UrlTree } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthGuard implements IAuthGuard {
|
||||
canActivate(): Observable<boolean> | boolean | UrlTree {
|
||||
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
export interface IAuthGuard extends CanActivate {}
|
||||
@ -0,0 +1,59 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Params } from '@angular/router';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { LoginParams } from '../models/auth';
|
||||
|
||||
/**
|
||||
* Abstract service for Authentication.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService implements IAuthService {
|
||||
constructor() {}
|
||||
|
||||
private warningMessage() {
|
||||
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
|
||||
}
|
||||
|
||||
init(): Promise<any> {
|
||||
this.warningMessage();
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
login(params: LoginParams): Observable<any> {
|
||||
this.warningMessage();
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
logout(queryParams?: Params): Observable<any> {
|
||||
this.warningMessage();
|
||||
return of(undefined);
|
||||
}
|
||||
|
||||
navigateToLogin(queryParams?: Params): void {}
|
||||
|
||||
get isInternalAuth() {
|
||||
throw new Error('not implemented');
|
||||
return false;
|
||||
}
|
||||
|
||||
get isAuthenticated(): boolean {
|
||||
this.warningMessage();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAuthService {
|
||||
get isInternalAuth(): boolean;
|
||||
|
||||
get isAuthenticated(): boolean;
|
||||
|
||||
init(): Promise<any>;
|
||||
|
||||
logout(queryParams?: Params): Observable<any>;
|
||||
|
||||
navigateToLogin(queryParams?: Params): void;
|
||||
|
||||
login(params: LoginParams): Observable<any>;
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from './ng-model.component';
|
||||
export * from './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