Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: token behavior improvements #1421

Merged
merged 3 commits into from
Jun 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
7 changes: 7 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ 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.
Furthermore the handling of the anonymous user token has been changed.
It will only be fetched when an anonymous user intends to create a basket.

## 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
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,13 @@ describe('Returning User', () => {
checkApiTokenCookie('user');
});

it('should log out and get the anonymous token', () => {
it('should log out and remove its apiToken cookie', () => {
at(MyAccountPage, page => page.header.logout());
at(HomePage);
// eslint-disable-next-line unicorn/no-null
checkApiTokenCookie('anonymous');

cy.getCookie('apiToken').then(cookie => {
expect(cookie).to.be.null;
});
});
});

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