Browse Source

Merge pull request #18606 from abpframework/auto-merge/rel-7-4/2395

Merge branch rel-8.0 with rel-7.4
pull/18609/head
Masum ULU 3 years ago
committed by GitHub
parent
commit
d26d888443
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 130
      docs/en/UI/Angular/Authorization.md
  2. 40
      npm/ng-packs/packages/core/src/lib/abstracts/auth-error-filter.ts
  3. 3
      npm/ng-packs/packages/core/src/lib/abstracts/auth.service.ts
  4. 1
      npm/ng-packs/packages/core/src/lib/abstracts/index.ts
  5. 1
      npm/ng-packs/packages/core/src/lib/core.module.ts
  6. 54
      npm/ng-packs/packages/core/src/lib/models/auth-events.ts
  7. 11
      npm/ng-packs/packages/core/src/lib/models/auth.ts
  8. 1
      npm/ng-packs/packages/core/src/lib/models/index.ts
  9. 6
      npm/ng-packs/packages/oauth/src/lib/oauth.module.ts
  10. 1
      npm/ng-packs/packages/oauth/src/lib/services/index.ts
  11. 45
      npm/ng-packs/packages/oauth/src/lib/services/oauth-error-filter.service.ts
  12. 6
      npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts
  13. 21
      npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts

130
docs/en/UI/Angular/Authorization.md

@ -1,9 +1,9 @@
## Authorization in Angular UI ## Authorization in Angular UI
OAuth is preconfigured in Angular application templates. So, when you start a project using the CLI (or Suite, for that matter), authorization already works. ABP Angular UI packages are using [angular-oauth2-oidc library](https://github.com/manfredsteyer/angular-oauth2-oidc#logging-in) for managing OAuth in the Angular client. OAuth is preconfigured in Angular application templates. So, when you start a project using the CLI (or Suite, for that matter), authorization already works. ABP Angular UI packages are using [angular-oauth2-oidc library](https://github.com/manfredsteyer/angular-oauth2-oidc#logging-in) for managing OAuth in the Angular client.
You can find **OAuth configuration** in the _environment.ts_ files. You can find **OAuth configuration** in the _environment.ts_ files.
### Authorization Code Flow ### Authorization Code Flow
```js ```js
import { Config } from '@abp/ng.core'; import { Config } from '@abp/ng.core';
@ -29,7 +29,6 @@ export const environment = {
This configuration results in an [OAuth authorization code flow with PKCE](https://tools.ietf.org/html/rfc7636). This configuration results in an [OAuth authorization code flow with PKCE](https://tools.ietf.org/html/rfc7636).
According to this flow, the user is redirected to an external login page which is built with MVC. So, if you need **to customize the login page**, please follow [this community article](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd). According to this flow, the user is redirected to an external login page which is built with MVC. So, if you need **to customize the login page**, please follow [this community article](https://community.abp.io/articles/how-to-customize-the-login-page-for-mvc-razor-page-applications-9a40f3cd).
### Resource Owner Password Flow ### Resource Owner Password Flow
If you have used the [Angular UI account module](./Account-Module) in your project, you can switch to the resource owner password flow by changing the OAuth configuration in the _environment.ts_ files as shown below: If you have used the [Angular UI account module](./Account-Module) in your project, you can switch to the resource owner password flow by changing the OAuth configuration in the _environment.ts_ files as shown below:
@ -52,3 +51,128 @@ export const environment = {
``` ```
According to this flow, the user is redirected to the login page in the account module. According to this flow, the user is redirected to the login page in the account module.
### Error Filtering
In [AuthFlowStrategy](https://github.com/abpframework/abp/blob/21e70fd66154d4064d03b1a438f20a2e4318715e/npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts#L24) class, there is a method called `listenToOauthErrors` that listens to `OAuthErrorEvent` errors. This method clears the localStorage for OAuth keys. However, in certain cases, we might want to skip this process. To achieve this, we can use the `AuthErrorFilterService`.
The `AuthErrorFilterService` is an abstract service that needs to be replaced with a custom implementation
> By default, this service is replaced in the `@abp/ng.oauth` package
**Usage**
1.Create an auth-filter.provider
```js
import { APP_INITIALIZER, inject } from '@angular/core';
import { AuthErrorFilter, AuthErrorEvent, AuthErrorFilterService } from '@abp/ng.core';
import { eCustomersAuthFilterNames } from '../enums';
export const CUSTOMERS_AUTH_FILTER_PROVIDER = [
{ provide: APP_INITIALIZER, useFactory: configureAuthFilter, multi: true },
];
type Reason = object & { error: { grant_type: string | undefined } };
function configureAuthFilter() {
const errorFilterService = inject(
AuthErrorFilterService<AuthErrorFilter<AuthErrorEvent>, AuthErrorEvent>,
);
const filter: AuthErrorFilter = {
id: eCustomersAuthFilterNames.LinkedUser,
executable: true,
execute: (event: AuthErrorEvent) => {
const { reason } = event;
const {
error: { grant_type },
} = <Reason>(reason || {});
return !!grant_type && grant_type === eCustomersAuthFilterNames.LinkedUser;
},
};
return () => errorFilterService.add(filter);
}
```
- `AuthErrorFilter:` is a model for filter object and it have 3 properties
- `id:` a unique key in the list for the filter object
- `executable:` a status for the filter object. If it's false then it won't work, yet it'll stay in the list
- `execute:` a function that stores the skip logic
2.Add to the FeatureConfigModule
```js
import { ModuleWithProviders, NgModule } from "@angular/core";
import { CUSTOMERS_AUTH_FILTER_PROVIDER } from "./providers/auth-filter.provider";
@NgModule()
export class CustomersConfigModule {
static forRoot(): ModuleWithProviders<CustomersConfigModule> {
return {
ngModule: CustomersConfigModule,
providers: [CUSTOMERS_AUTH_FILTER_PROVIDER],
};
}
}
```
Now it'll skip the clearing of OAuth storage keys for `LinkedUser` grant_type if any `OAuthErrorEvent` occurs
**Replace with custom implementation**
- Use the `AbstractAuthErrorFilter<T,E>` class for signs of process.
**Example**
`my-auth-error-filter.service.ts`
```js
import { Injectable, signal } from '@angular/core';
import { MyAuthErrorEvent } from 'angular-my-auth-oidc';
import { AbstractAuthErrorFilter, AuthErrorFilter } from '@abp/ng.core';
@Injectable({ providedIn: 'root' })
export class OAuthErrorFilterService extends AbstractAuthErrorFilter<
AuthErrorFilter<MyAuthErrorEvent>,
MyAuthErrorEvent
> {
protected readonly _filters = signal<Array<AuthErrorFilter<MyAuthErrorEvent>>>([]);
readonly filters = this._filters.asReadonly();
get(id: string): AuthErrorFilter<MyAuthErrorEvent> {
return this._filters().find(({ id: _id }) => _id === id);
}
add(filter: AuthErrorFilter<MyAuthErrorEvent>): void {
this._filters.update(items => [...items, filter]);
}
patch(item: Partial<AuthErrorFilter<MyAuthErrorEvent>>): void {
const _item = this.filters().find(({ id }) => id === item.id);
if (!_item) {
return;
}
Object.assign(_item, item);
}
remove(id: string): void {
const item = this.filters().find(({ id: _id }) => _id === id);
if (!item) {
return;
}
this._filters.update(items => items.filter(({ id: _id }) => _id !== id));
}
run(event: MyAuthErrorEvent): boolean {
return this.filters()
.filter(({ executable }) => !!executable)
.map(({ execute }) => execute(event))
.some(item => item);
}
}
```

40
npm/ng-packs/packages/core/src/lib/abstracts/auth-error-filter.ts

@ -0,0 +1,40 @@
import { AuthErrorEvent, AuthErrorFilter } from '../models';
export abstract class AbstractAuthErrorFilter<T, E> {
abstract get(id: string): T;
abstract add(filter: T): void;
abstract patch(item: Partial<T>): void;
abstract remove(id: string): void;
abstract run(event: E): boolean;
}
export class AuthErrorFilterService<
T = AuthErrorFilter,
E = AuthErrorEvent,
> extends AbstractAuthErrorFilter<T, E> {
private warningMessage() {
console.error('You should add @abp/ng-oauth packages or create your own auth packages.');
}
get(id: string): T {
this.warningMessage();
throw new Error('not implemented');
}
add(filter: T): void {
this.warningMessage();
}
patch(item: Partial<T>): void {
this.warningMessage();
}
remove(id: string): void {
this.warningMessage();
}
run(event: E): boolean {
this.warningMessage();
throw new Error('not implemented');
}
}

3
npm/ng-packs/packages/core/src/lib/abstracts/auth.service.ts

@ -33,9 +33,8 @@ export class AuthService implements IAuthService {
navigateToLogin(queryParams?: Params): void {} navigateToLogin(queryParams?: Params): void {}
get isInternalAuth() { get isInternalAuth(): boolean {
throw new Error('not implemented'); throw new Error('not implemented');
return false;
} }
get isAuthenticated(): boolean { get isAuthenticated(): boolean {

1
npm/ng-packs/packages/core/src/lib/abstracts/index.ts

@ -3,3 +3,4 @@ export * from './ng-model.component';
export * from './auth.guard'; export * from './auth.guard';
export * from './auth.service'; export * from './auth.service';
export * from './auth-response.model'; export * from './auth-response.model';
export * from './auth-error-filter';

1
npm/ng-packs/packages/core/src/lib/core.module.ts

@ -187,6 +187,7 @@ export class CoreModule {
provide: OTHERS_GROUP, provide: OTHERS_GROUP,
useValue: options.othersGroup || 'AbpUi::OthersGroup', useValue: options.othersGroup || 'AbpUi::OthersGroup',
}, },
AuthErrorFilterService,
IncludeLocalizationResourcesProvider, IncludeLocalizationResourcesProvider,
{ {
provide: DYNAMIC_LAYOUTS_TOKEN, provide: DYNAMIC_LAYOUTS_TOKEN,

54
npm/ng-packs/packages/core/src/lib/models/auth-events.ts

@ -0,0 +1,54 @@
export type EventType =
| 'discovery_document_loaded'
| 'jwks_load_error'
| 'invalid_nonce_in_state'
| 'discovery_document_load_error'
| 'discovery_document_validation_error'
| 'user_profile_loaded'
| 'user_profile_load_error'
| 'token_received'
| 'token_error'
| 'code_error'
| 'token_refreshed'
| 'token_refresh_error'
| 'silent_refresh_error'
| 'silently_refreshed'
| 'silent_refresh_timeout'
| 'token_validation_error'
| 'token_expires'
| 'session_changed'
| 'session_error'
| 'session_terminated'
| 'session_unchanged'
| 'logout'
| 'popup_closed'
| 'popup_blocked'
| 'token_revoke_error';
export abstract class AuthEvent {
constructor(public readonly type: EventType) {
this.type = type;
}
}
export class AuthSuccessEvent extends AuthEvent {
constructor(public readonly type: EventType, public readonly info?: any) {
super(type);
}
}
export class AuthInfoEvent extends AuthEvent {
constructor(public readonly type: EventType, public readonly info?: any) {
super(type);
}
}
export class AuthErrorEvent extends AuthEvent {
constructor(
public readonly type: EventType,
public readonly reason: object,
public readonly params?: object,
) {
super(type);
}
}

11
npm/ng-packs/packages/core/src/lib/models/auth.ts

@ -1,5 +1,6 @@
import { UnaryFunction } from 'rxjs';
import { Injector } from '@angular/core'; import { Injector } from '@angular/core';
import { UnaryFunction } from 'rxjs';
import { AuthErrorEvent } from './auth-events';
export interface LoginParams { export interface LoginParams {
username: string; username: string;
@ -13,7 +14,13 @@ export type PipeToLoginFn = (
injector: Injector, injector: Injector,
) => UnaryFunction<any, any>; ) => UnaryFunction<any, any>;
/** /**
* @deprecated The interface should not be used anymore. * @deprecated The interface should not be used anymore.
*/ */
export type SetTokenResponseToStorageFn<T = any> = (tokenRes: T) => void; export type SetTokenResponseToStorageFn<T = any> = (tokenRes: T) => void;
export type CheckAuthenticationStateFn = (injector: Injector) => void; export type CheckAuthenticationStateFn = (injector: Injector) => void;
export interface AuthErrorFilter<T = AuthErrorEvent> {
id: string;
executable: boolean;
execute: (event: T) => boolean;
}

1
npm/ng-packs/packages/core/src/lib/models/index.ts

@ -7,3 +7,4 @@ export * from './rest';
export * from './session'; export * from './session';
export * from './utility'; export * from './utility';
export * from './auth'; export * from './auth';
export * from './auth-events';

6
npm/ng-packs/packages/oauth/src/lib/oauth.module.ts

@ -1,9 +1,11 @@
import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angular/core'; import { APP_INITIALIZER, ModuleWithProviders, NgModule, Provider } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc'; import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { import {
AbpLocalStorageService, AbpLocalStorageService,
ApiInterceptor, ApiInterceptor,
AuthErrorFilterService,
AuthGuard, AuthGuard,
authGuard, authGuard,
AuthService, AuthService,
@ -11,9 +13,8 @@ import {
noop, noop,
PIPE_TO_LOGIN_FN_KEY, PIPE_TO_LOGIN_FN_KEY,
} from '@abp/ng.core'; } from '@abp/ng.core';
import { AbpOAuthService } from './services'; import { AbpOAuthService, OAuthErrorFilterService } from './services';
import { OAuthConfigurationHandler } from './handlers/oauth-configuration.handler'; import { OAuthConfigurationHandler } from './handlers/oauth-configuration.handler';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { OAuthApiInterceptor } from './interceptors/api.interceptor'; import { OAuthApiInterceptor } from './interceptors/api.interceptor';
import { AbpOAuthGuard, abpOAuthGuard } from './guards/oauth.guard'; import { AbpOAuthGuard, abpOAuthGuard } from './guards/oauth.guard';
import { NavigateToManageProfileProvider } from './providers'; import { NavigateToManageProfileProvider } from './providers';
@ -65,6 +66,7 @@ export class AbpOAuthModule {
}, },
OAuthModule.forRoot().providers as Provider[], OAuthModule.forRoot().providers as Provider[],
{ provide: OAuthStorage, useClass: AbpLocalStorageService }, { provide: OAuthStorage, useClass: AbpLocalStorageService },
{ provide: AuthErrorFilterService, useExisting: OAuthErrorFilterService },
], ],
}; };
} }

1
npm/ng-packs/packages/oauth/src/lib/services/index.ts

@ -1 +1,2 @@
export * from './oauth.service'; export * from './oauth.service';
export * from './oauth-error-filter.service';

45
npm/ng-packs/packages/oauth/src/lib/services/oauth-error-filter.service.ts

@ -0,0 +1,45 @@
import { Injectable, signal } from '@angular/core';
import { OAuthErrorEvent } from 'angular-oauth2-oidc';
import { AbstractAuthErrorFilter, AuthErrorFilter } from '@abp/ng.core';
@Injectable({ providedIn: 'root' })
export class OAuthErrorFilterService extends AbstractAuthErrorFilter<
AuthErrorFilter<OAuthErrorEvent>,
OAuthErrorEvent
> {
protected readonly _filters = signal<Array<AuthErrorFilter<OAuthErrorEvent>>>([]);
readonly filters = this._filters.asReadonly();
get(id: string): AuthErrorFilter<OAuthErrorEvent> {
return this._filters().find(({ id: _id }) => _id === id);
}
add(filter: AuthErrorFilter<OAuthErrorEvent>): void {
this._filters.update(items => [...items, filter]);
}
patch(item: Partial<AuthErrorFilter<OAuthErrorEvent>>): void {
const _item = this.filters().find(({ id }) => id === item.id);
if (!_item) {
return;
}
Object.assign(_item, item);
}
remove(id: string): void {
const item = this.filters().find(({ id: _id }) => _id === id);
if (!item) {
return;
}
this._filters.update(items => items.filter(({ id: _id }) => _id !== id));
}
run(event: OAuthErrorEvent): boolean {
return this.filters()
.filter(({ executable }) => !!executable)
.map(({ execute }) => execute(event))
.some(item => item);
}
}

6
npm/ng-packs/packages/oauth/src/lib/services/oauth.service.ts

@ -44,10 +44,10 @@ export class AbpOAuthService implements IAuthService {
} }
logout(queryParams?: Params): Observable<any> { logout(queryParams?: Params): Observable<any> {
if (!this.strategy) {
if(!this.strategy){ return EMPTY;
return EMPTY
} }
return this.strategy.logout(queryParams); return this.strategy.logout(queryParams);
} }

21
npm/ng-packs/packages/oauth/src/lib/strategies/auth-flow-strategy.ts

@ -1,13 +1,16 @@
import { Injector } from '@angular/core'; import { Injector } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { Params, Router } from '@angular/router'; import { Params, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { import {
AuthConfig, AuthConfig,
OAuthErrorEvent, OAuthErrorEvent,
OAuthService as OAuthService2, OAuthService as OAuthService2,
OAuthStorage, OAuthStorage,
} from 'angular-oauth2-oidc'; } from 'angular-oauth2-oidc';
import { Observable, of } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import { import {
AbpLocalStorageService, AbpLocalStorageService,
ConfigStateService, ConfigStateService,
@ -17,9 +20,10 @@ import {
SessionStateService, SessionStateService,
TENANT_KEY, TENANT_KEY,
} from '@abp/ng.core'; } from '@abp/ng.core';
import { clearOAuthStorage } from '../utils/clear-o-auth-storage'; import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
import { oAuthStorage } from '../utils/oauth-storage'; import { oAuthStorage } from '../utils/oauth-storage';
import { HttpErrorResponse } from '@angular/common/http'; import { OAuthErrorFilterService } from '../services';
export abstract class AuthFlowStrategy { export abstract class AuthFlowStrategy {
abstract readonly isInternalAuth: boolean; abstract readonly isInternalAuth: boolean;
@ -34,6 +38,8 @@ export abstract class AuthFlowStrategy {
protected tenantKey: string; protected tenantKey: string;
protected router: Router; protected router: Router;
protected readonly oAuthErrorFilterService: OAuthErrorFilterService;
abstract checkIfInternalAuth(queryParams?: Params): boolean; abstract checkIfInternalAuth(queryParams?: Params): boolean;
abstract navigateToLogin(queryParams?: Params): void; abstract navigateToLogin(queryParams?: Params): void;
abstract logout(queryParams?: Params): Observable<any>; abstract logout(queryParams?: Params): Observable<any>;
@ -54,6 +60,7 @@ export abstract class AuthFlowStrategy {
this.oAuthConfig = this.environment.getEnvironment().oAuthConfig || {}; this.oAuthConfig = this.environment.getEnvironment().oAuthConfig || {};
this.tenantKey = injector.get(TENANT_KEY); this.tenantKey = injector.get(TENANT_KEY);
this.router = injector.get(Router); this.router = injector.get(Router);
this.oAuthErrorFilterService = injector.get(OAuthErrorFilterService);
this.listenToOauthErrors(); this.listenToOauthErrors();
} }
@ -97,6 +104,7 @@ export abstract class AuthFlowStrategy {
if (redirect_uri && redirect_uri !== '/') { if (redirect_uri && redirect_uri !== '/') {
return redirect_uri; return redirect_uri;
} }
return '/'; return '/';
}), }),
switchMap(redirectUri => switchMap(redirectUri =>
@ -118,7 +126,12 @@ export abstract class AuthFlowStrategy {
this.oAuthService.events this.oAuthService.events
.pipe( .pipe(
filter(event => event instanceof OAuthErrorEvent), filter(event => event instanceof OAuthErrorEvent),
tap(() => clearOAuthStorage()), tap((err: OAuthErrorEvent) => {
const shouldSkip = this.oAuthErrorFilterService.run(err);
if (!shouldSkip) {
clearOAuthStorage();
}
}),
switchMap(() => this.configState.refreshAppState()), switchMap(() => this.configState.refreshAppState()),
) )
.subscribe(); .subscribe();

Loading…
Cancel
Save