Browse Source

Merge pull request #18609 from abpframework/auto-merge/rel-8-0/2396

Merge branch dev with rel-8.0
pull/18637/head
maliming 2 years ago
committed by GitHub
parent
commit
d66f5c3704
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
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.
### Authorization Code Flow
### Authorization Code Flow
```js
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).
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
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.
### 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 {}
get isInternalAuth() {
get isInternalAuth(): boolean {
throw new Error('not implemented');
return false;
}
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.service';
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,
useValue: options.othersGroup || 'AbpUi::OthersGroup',
},
AuthErrorFilterService,
IncludeLocalizationResourcesProvider,
{
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 { UnaryFunction } from 'rxjs';
import { AuthErrorEvent } from './auth-events';
export interface LoginParams {
username: string;
@ -13,7 +14,13 @@ export type PipeToLoginFn = (
injector: Injector,
) => 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 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 './utility';
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 { CommonModule } from '@angular/common';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import {
AbpLocalStorageService,
ApiInterceptor,
AuthErrorFilterService,
AuthGuard,
authGuard,
AuthService,
@ -11,9 +13,8 @@ import {
noop,
PIPE_TO_LOGIN_FN_KEY,
} from '@abp/ng.core';
import { AbpOAuthService } from './services';
import { AbpOAuthService, OAuthErrorFilterService } from './services';
import { OAuthConfigurationHandler } from './handlers/oauth-configuration.handler';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { OAuthApiInterceptor } from './interceptors/api.interceptor';
import { AbpOAuthGuard, abpOAuthGuard } from './guards/oauth.guard';
import { NavigateToManageProfileProvider } from './providers';
@ -65,6 +66,7 @@ export class AbpOAuthModule {
},
OAuthModule.forRoot().providers as Provider[],
{ 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-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> {
if(!this.strategy){
return EMPTY
if (!this.strategy) {
return EMPTY;
}
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 { HttpErrorResponse } from '@angular/common/http';
import { Params, Router } from '@angular/router';
import { Observable, of } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import {
AuthConfig,
OAuthErrorEvent,
OAuthService as OAuthService2,
OAuthStorage,
} from 'angular-oauth2-oidc';
import { Observable, of } from 'rxjs';
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
import {
AbpLocalStorageService,
ConfigStateService,
@ -17,9 +20,10 @@ import {
SessionStateService,
TENANT_KEY,
} from '@abp/ng.core';
import { clearOAuthStorage } from '../utils/clear-o-auth-storage';
import { oAuthStorage } from '../utils/oauth-storage';
import { HttpErrorResponse } from '@angular/common/http';
import { OAuthErrorFilterService } from '../services';
export abstract class AuthFlowStrategy {
abstract readonly isInternalAuth: boolean;
@ -34,6 +38,8 @@ export abstract class AuthFlowStrategy {
protected tenantKey: string;
protected router: Router;
protected readonly oAuthErrorFilterService: OAuthErrorFilterService;
abstract checkIfInternalAuth(queryParams?: Params): boolean;
abstract navigateToLogin(queryParams?: Params): void;
abstract logout(queryParams?: Params): Observable<any>;
@ -54,6 +60,7 @@ export abstract class AuthFlowStrategy {
this.oAuthConfig = this.environment.getEnvironment().oAuthConfig || {};
this.tenantKey = injector.get(TENANT_KEY);
this.router = injector.get(Router);
this.oAuthErrorFilterService = injector.get(OAuthErrorFilterService);
this.listenToOauthErrors();
}
@ -97,6 +104,7 @@ export abstract class AuthFlowStrategy {
if (redirect_uri && redirect_uri !== '/') {
return redirect_uri;
}
return '/';
}),
switchMap(redirectUri =>
@ -118,7 +126,12 @@ export abstract class AuthFlowStrategy {
this.oAuthService.events
.pipe(
filter(event => event instanceof OAuthErrorEvent),
tap(() => clearOAuthStorage()),
tap((err: OAuthErrorEvent) => {
const shouldSkip = this.oAuthErrorFilterService.run(err);
if (!shouldSkip) {
clearOAuthStorage();
}
}),
switchMap(() => this.configState.refreshAppState()),
)
.subscribe();

Loading…
Cancel
Save