Skip to content

Commit

Permalink
feat: improve token endpoint handling
Browse files Browse the repository at this point in the history
* own service, which is responsible for all token handling with the ICM
* rename ICMTokenEndpoint Service to TokenService and moved it files to core/services folder
* create utility class to instantiate custom oauthService instance
* adapt documentation for new authentication behavior

closes: #1374
  • Loading branch information
Eisie96 authored and shauke committed Jun 16, 2023
1 parent 4d798e3 commit 2aa15ba
Show file tree
Hide file tree
Showing 22 changed files with 388 additions and 513 deletions.
42 changes: 23 additions & 19 deletions docs/concepts/authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ The following identity providers are supported: The default [ICM server](../guid
There is a lot of functionality related to authentication, e.g., logging a user in and out, registering a new user, keeping the user identified even if the user opens further browser tabs, etc.

The PWA uses the library [angular-oauth2-oidc](https://github.com/manfredsteyer/angular-oauth2-oidc#readme) to support the implementation of these functionalities.
It can be configured to provide access to identity providers.
You can find the initialization of this library in the [oauth-configuration-service.ts](../../src/app/shared/../core/utils/oauth-configuration/oauth-configuration.service.ts).
It is used to fetching data from the the [icm token endpoint service](../../src/app/core/services/token/token.service.ts) and can be configured to provide access to other identity providers.

## Implementation and Configuration of Identity Providers

Expand All @@ -31,40 +30,45 @@ To add or change the functionality of an identity provider, the following steps

In the following code you see a typical implementation of the init method of an IdP class.

Note that all authentication-related functionality must not be executed before the oAuth service has been configured.

```typescript
@Injectable({ providedIn: 'root' })
export class ExampleIdentityProvider implements IdentityProvider {
private configured$ = new BehaviorSubject<boolean>(false);

constructor(private oAuthService: OAuthService, private configService: OAuthConfigurationService) {}
constructor(
private router: Router,
private apiTokenService: ApiTokenService,
private accountFacade: AccountFacade
) {}

init() {
this.configService.config$.subscribe(config => {
this.oAuthService.configure(config);
this.configured.next(true);
this.apiTokenService.restore$().subscribe(noop);

this.apiTokenService.cookieVanishes$.subscribe(([type]) => {
this.accountFacade.logoutUser({ revokeApiToken: false });
if (type === 'user') {
this.router.navigate(['/login'], {
queryParams: { returnUrl: this.router.url, messageKey: 'session_timeout' },
});
}
});

this.configured
.pipe(
whenTruthy(),
switchMap(() => from(this.oAuthService.fetchTokenUsingGrant('anonymous')))
)
.subscribe();
}
}
```

> **Note**
>
> If a identity provider is using the OAuthService for authentication, then the identity provider have to inject the OAuthService with a new instance.
> Otherwise difficult side effects with the [TokenService](../../src/app/core/services/token/token.service.ts) will occur.
> Please checkout the [Auth0IdentityProvider](../../src/app/core/identity-provider/auth0.identity-provider.ts) for an example.
2. Register the `<idp>.identity-provider.ts` in the [`IdentityProviderModule`](../../src/app/core/identity-provider.module.ts). The `APP_INITIALIZER` injection token is used to configure and initialize the identity provider before app initialization.

3. Set the environment variables `IdentityProviders` and `IdentityProvider` accordingly.

## PWA Initialization

A PWA user has to be identified by the ICM server by a unique authentication token, even if it is an anonymous user.
Once a user opens the PWA for the first time, an authentication token is requested by the [ICM Token REST endpoint](https://support.intershop.com/kb/index.php?c=Display&q1=U29770&q2=Text).
This happens in the [`init()`](../../src/app/core/identity-provider/icm.identity-provider.ts) method of the active identity provider.
Once a unknown user create a basket in the PWA, an anonymous authentication token is requested by the [ICM Token REST endpoint](https://support.intershop.com/kb/index.php?c=Display&q1=U29770&q2=Text).
This happens in the [`apiToken http interceptor`](../../src/app/core/utils/api-token/api-token.service.ts) method.
Subsequently, this token will be saved as `apiToken` cookie and added to all REST requests in the request header, e.g.:

```typescript
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/authentication_icm.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Afterwards, the authentication token is requested from the server and the user w
Each authentication token has a predefined lifetime.
That means, the token has to be refreshed to prevent it from expiring.
Once 75% of the token's lifetime have passed (this time can be configured in the oAuth library), an info event is emitted.
This event is used to call the [refresh mechanism `setupRefreshTokenMechanism$`](../../src/app/core/utils/oauth-configuration/oauth-configuration.service.ts) of the oAuth configuration service and the authentication token will be renewed.
This event is used to call the [refresh mechanism `setupRefreshTokenMechanism$`](../../src/app/core/services/token/token.service.ts) of the oAuth configuration service and the authentication token will be renewed.
Hence, the token will not expire as long as the user keeps the PWA open in the browser.

## Logout
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/authentication_punchout.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ There is currently no possibility to register a new punchout user in the PWA.
Each authentication token has a predefined lifetime.
That means, the token has to be refreshed to prevent it from expiring.
Once 75% of the token's lifetime have passed ( this time can be configured in the oAuth library), an info event is emitted.
This event is used to call the [refresh mechanism `setupRefreshTokenMechanism$`](../../src/app/core/utils/oauth-configuration/oauth-configuration.service.ts) of the oAuth configuration service and the authentication token will be renewed.
This event is used to call the [refresh mechanism `setupRefreshTokenMechanism$`](../../src/app/core/services/token/token.service.ts) of the oAuth configuration service and the authentication token will be renewed.
Hence, the token will not expire as long as the user keeps the PWA open in the browser.

## Logout
Expand Down
5 changes: 5 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ Existing projects that do not want to use a configurable theme do not need to ap

To use the new [configurable theme](./themes.md#configurable-theme) feature, the feature toggle `extraConfiguration` needs to be enabled.

A new `TokenService` is introduced to be only responsible for fetching token data from the ICM.
However all necessary adaptions for the identity providers and the `fetchToken()` method of the UserService are removed in order to be completely independent of `TokenService`.
If your identity providers should use the `OAuthService` to handle the authentication, please make sure to instantiate a new `OAuthService` entity within the identity provider.
The `getOAuthServiceInstance()` static method from the `InstanceCreators` class can be used for that.

## 3.3 to 4.0

The Intershop PWA now uses Node.js 18.15.0 LTS with the corresponding npm version 9.5.0 and the `"lockfileVersion": 3,`.
Expand Down
26 changes: 2 additions & 24 deletions src/app/core/identity-provider.module.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { HttpHandler, HttpRequest } from '@angular/common/http';
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { BehaviorSubject, noop, of, race, timer } from 'rxjs';
import { noop } from 'rxjs';

import { PunchoutIdentityProviderModule } from '../extensions/punchout/identity-provider/punchout-identity-provider.module';

import { Auth0IdentityProvider } from './identity-provider/auth0.identity-provider';
import { ICMIdentityProvider } from './identity-provider/icm.identity-provider';
import { IDENTITY_PROVIDER_IMPLEMENTOR, IdentityProviderFactory } from './identity-provider/identity-provider.factory';
import { IdentityProviderCapabilities } from './identity-provider/identity-provider.interface';
import { OAuthConfigurationService } from './utils/oauth-configuration/oauth-configuration.service';

/**
* provider factory for storage
Expand All @@ -21,23 +20,9 @@ export function storageFactory(): OAuthStorage {
}
}

/**
* load configuration object for OAuth Service
* OAuth Service should be configured, when app is initialized
*/
function loadOAuthConfig(configService: OAuthConfigurationService) {
return () => race(configService.loadConfig$, timer(4000));
}

@NgModule({
imports: [OAuthModule.forRoot({ resourceServer: { sendAccessToken: false } }), PunchoutIdentityProviderModule],
providers: [
{
provide: APP_INITIALIZER,
useFactory: loadOAuthConfig,
deps: [OAuthConfigurationService],
multi: true,
},
{ provide: OAuthStorage, useFactory: storageFactory },
{
provide: IDENTITY_PROVIDER_IMPLEMENTOR,
Expand Down Expand Up @@ -78,13 +63,6 @@ export class IdentityProviderModule {
getType: () => 'ICM',
},
},
{
provide: OAuthConfigurationService,
useValue: {
loadConfig$: of({}),
config$: new BehaviorSubject({}),
},
},
],
};
}
Expand Down
27 changes: 13 additions & 14 deletions src/app/core/identity-provider/auth0.identity-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MockStore, provideMockStore } from '@ngrx/store/testing';
import { OAuthService } from 'angular-oauth2-oidc';
import { BehaviorSubject, of } from 'rxjs';
import { of } from 'rxjs';
import { anything, capture, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito';

import { Customer } from 'ish-core/models/customer/customer.model';
Expand All @@ -13,7 +13,7 @@ import { ApiService } from 'ish-core/services/api/api.service';
import { getSsoRegistrationCancelled, getSsoRegistrationRegistered } from 'ish-core/store/customer/sso-registration';
import { getLoggedInCustomer, getUserAuthorized, getUserLoading } from 'ish-core/store/customer/user';
import { ApiTokenService } from 'ish-core/utils/api-token/api-token.service';
import { OAuthConfigurationService } from 'ish-core/utils/oauth-configuration/oauth-configuration.service';
import { InstanceCreators } from 'ish-core/utils/instance-creators';

import { Auth0Config, Auth0IdentityProvider } from './auth0.identity-provider';

Expand All @@ -36,7 +36,7 @@ describe('Auth0 Identity Provider', () => {
const oAuthService = mock(OAuthService);
const apiService = mock(ApiService);
const apiTokenService = mock(ApiTokenService);
const oAuthConfigurationService = mock(OAuthConfigurationService);
const instanceCreators = spy(InstanceCreators);
let auth0IdentityProvider: Auth0IdentityProvider;
let store$: MockStore;
let storeSpy$: MockStore;
Expand All @@ -57,28 +57,27 @@ describe('Auth0 Identity Provider', () => {
{ provide: ApiService, useFactory: () => instance(apiService) },
{ provide: ApiTokenService, useFactory: () => instance(apiTokenService) },
{ provide: APP_BASE_HREF, useValue: baseHref },
{ provide: OAuthConfigurationService, useFactory: () => instance(oAuthConfigurationService) },
{ provide: OAuthService, useFactory: () => instance(oAuthService) },
provideMockStore(),
],
}).compileComponents();

auth0IdentityProvider = TestBed.inject(Auth0IdentityProvider);
router = TestBed.inject(Router);
store$ = TestBed.inject(MockStore);
storeSpy$ = spy(store$);
});

beforeEach(() => {
when(apiTokenService.restore$(anything(), anything())).thenReturn(of(true));
when(oAuthService.getIdToken()).thenReturn(idToken);
when(oAuthService.loadDiscoveryDocumentAndTryLogin()).thenReturn(
new Promise((res, _) => {
res(true);
})
);
when(oAuthService.state).thenReturn(undefined);
when(oAuthConfigurationService.config$).thenReturn(new BehaviorSubject({}));
when(instanceCreators.getOAuthServiceInstance(anything())).thenReturn(instance(oAuthService));

auth0IdentityProvider = TestBed.inject(Auth0IdentityProvider);
router = TestBed.inject(Router);
store$ = TestBed.inject(MockStore);
storeSpy$ = spy(store$);
});

beforeEach(() => {
when(apiTokenService.restore$(anything())).thenReturn(of(true));
when(apiService.post(anything(), anything())).thenReturn(of(userData));
});

Expand Down
Loading

0 comments on commit 2aa15ba

Please sign in to comment.