Skip to content

Commit

Permalink
feat: use new apitoken REST endpoint to get access token and revoke t…
Browse files Browse the repository at this point in the history
…oken on logout
  • Loading branch information
Eisie96 committed Dec 12, 2022
1 parent 811706f commit a9195ad
Show file tree
Hide file tree
Showing 37 changed files with 1,292 additions and 321 deletions.
5 changes: 4 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ kb_sync_latest_only
- [Guide - Formly](./guides/formly.md)
- [Guide - Field Library](./guides/field-library.md)
- [Concept - Deployment Scenarios for Angular Applications](./concepts/deployment-angular.md)
- [Concept - Single Sign-On (SSO) for PWA](./concepts/sso.md)
- [Concept - Authentication](./concepts/authentication.md)
- [Concept - Single Sign-On (SSO) for PWA](./concepts/sso.md)
- [Guide - Handling Authentication by the ICM Server](./guides/authentication_icm.md)
- [Guide - Punchout Identity Provider](./guides/punchout-identity-provider.md)

### Developing

Expand Down
93 changes: 93 additions & 0 deletions docs/concepts/authentication.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!--
kb_concepts
kb_pwa
kb_everyone
kb_sync_latest_only
-->

# Authentication Concept

## Introduction

Several ICM REST operations require an authenticated user.
Authentication also assures enterprise information security.
In the PWA a user can be verified with the help of an identity provider.
An identity provider (IdP) is a service that stores and manages digital identities.
The following identity providers are supported: The ICM server (default), the [SSO Auth0](sso.md) and the [Punchout Identity Provider](./../guides/punchout-identity-provider.md).

## Library angular-oauth2-oidc

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).

## Implementation and Configuration of Identity Providers

To add or change the functionality of an identity provider the following steps are necessary:

1. Create/change a \<idp\>.identity-provider.ts class that implements the interface [IdentityProvider](../../src/app/core/identity-provider/identity-provider.interface.ts). In this interface all methods are declared which have to be implemented in your IdP class.

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) {}

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

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

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 he is anonymous.
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.
Subsequently, this token will be saved as apiToken cookie and added to all REST requests in the request header, e.g.:

```typescript
authentication-token: encryption0@PBEWithMD5AndTripleDES:1D7T8HyFqQ0=|k3PQLgujzUq0tudtw+6HLjWnExiwrd4o9/jVU7ZH74kTfTy3RS7/sYadsg7ODRM2
```

This way it is possible to identify a user even if he is opening a new browser tab or refreshing the PWA in the browser.

If a user opens the PWA and he already has a valid apiToken cookie no new token is requested by the ICM server but this token is used in the header of the REST requests.

## Login, Registration, Token Refreshment, Logout

All these functionality strongly depends on the implementation of the used identity provider.
This is described in the appropriate identity provider guides in more detail, find the links below under further references.

## Vanishing of the 'apiToken' Cookie

The PWA should react on the situation, when the apiToken cookie is not available any more.
This could happen if a PWA is opened in many tabs and the user logs out or when the user removes him/herself the cookie.
When the cookie vanishes, the PWA emits a new value for the [cookieVanishes$ subject](../../src/app/core/utils/api-token/api-token.service.ts).
The identity provider implementation defines, how the application should behave on this event, e.g. the ICM identity provider automatically logs out the user and routes him/her to the '/login' page.

## Further References

- [Guide - ICM Identity Provider](../guides/authentication_icm.md)
- [Guide - Punchout Identity Provider](../guides/punchout-identity-provider.md)
- [Concept - Single Sign-On (SSO) for PWA](sso.md)
3 changes: 3 additions & 0 deletions docs/concepts/sso.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ The usage of identity providers can also be set in the multi-channel configurati
# Further References

- PWA
- [Concept - Authentication](./authentication.md)
- [Guide - SSO with Auth0 for PWA](../guides/sso-auth0.md)
- [Guide - Building and Running Server-Side Rendering][ssr-startup]
- [Guide - Building and Running nginx Docker Image][nginx-startup]
- ICM
- [Concept - Single Sign-On (SSO)][kb-concept-sso]
- General
- [SSO with OAuth 2 and OpenId Connect](https://angular.de/artikel/oauth-odic-plugin/) (in German)

[kb-concept-sso]: https://support.intershop.com/kb/index.php/Display/29A407
[ssr-startup]: ../guides/ssr-startup.md
Expand Down
38 changes: 38 additions & 0 deletions docs/guides/authentication_icm.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!--
kb_guide
kb_pwa
kb_everyone
kb_sync_latest_only
-->

# Authentication by the ICM Server

This document describes the main authentication mechanism if the ICM server is used as identity provider.
If you need an introduction to this topic, read the [Authentication Concept](../concepts/authentication.md) first.

## Login

If the user wants to login by clicking a login link or navigating to the '/login' route, either a popup or a page is shown containing a login form.
After the user has entered his credentials (email/user name and password) and could be successfully verified by the ICM server a new token is fetched from the ICM '/token' REST endpoint.
The token of the registered user is saved as apiToken cookie and attached to the request header of the subsequent REST requests.
After logging in the pgid of the user is requested from the ICM server ( /personalization call) and the action personalizationStatusDetermined will be triggered.
If you want to request user specific non-cached data from the ICM server use the option 'sendSpgid' or 'sendPgid', respectively when you call the get method of the api token service.

## Registration

The registration of a user is similar to the login.
After the user has completed the registration form, the data are validated by the ICM server and a new user will be created.
Afterwards the authentication token is requested from the server and the user will be logged in, see above.

## Token Lifetime

Each authentication token has a predefined lifetime.
That means, the token has to be refreshed to prevent it from expiring.
When 75% of the token's lifetime is gone by ( 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.
Hence, the token won't expire as long as the user keeps the PWA open in the browser.

## Logout

When the user logs out by clicking the logout link or navigating to the '/logout' route, the configured [logout()](../../src/app/core/identity-provider/icm.identity-provider.ts) function will be executed and this will call the [revokeApiToken()](../../src/app/core/services/user/user.service.ts) user service in order to inactivate the token on server side.
Besides this, the PWA removes the token on browser side, fetches a new anonymous user token and set it as apiToken cookie.
6 changes: 6 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ After entering a desired delivery date on the checkout shipping page and after s
In case of large basket (> 20 items) this might cause (unacceptable) long response times.
You can keep the existing behavior by modifying the updateBasketItemsDesiredDeliveryDate() method of the basket service to always return an empty array without doing anything.

The user authentication process has changed.
User authentication tokens are requested from the ICM server using the token REST endpoint now.
Regarding to this, the logout action triggers a service, which revokes the current available access token on the ICM backend.
If the logout was successful, then all personalized information are removed from the ngrx store.
Please use `logoutUser({ revokeToken: false })` from the account facade or dispatch `logoutUserSuccess` instead of the `logoutUser` action to use the old behavior.

## 3.0 to 3.1

The SSR environment variable 'ICM_IDENTITY_PROVIDER' will be removed in a future release ( PWA 5.0 ).
Expand Down
37 changes: 37 additions & 0 deletions docs/guides/punchout-identity-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!--
kb_guide
kb_pwa
kb_everyone
kb_sync_latest_only
-->

# Punchout Configuration for PWA

The PWA implementation for the punchout identity provider is located in [`PunchoutIdentityProvider`](../../src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts).

For development purposes the configuration can be added to the Angular CLI environment files:

```typescript
features: [
...,
'punchout'
],
identityProvider: 'Punchout',
identityProviders: {
'Punchout': {
type: 'PUNCHOUT',
}
},
```

> **_NOTE:_** The value for the identityProvider must match a key from the configured identityProviders object. Furthermore the type 'PUNCHOUT' has to be used for the identityProviders configuration to access the implemented [`PunchoutIdentityProvider`](../../src/app/extensions/punchout/identity-provider/punchout-identity-provider.ts).
For production, this configuration should be provided to the SSR process via environment variables (see [Building and Running Server-Side Rendering][ssr-startup]).
The usage of identity providers can also be set in the multi-channel configuration (see [Building and Running nginx Docker Image][nginx-startup]).

Additionally the PWA can be configured to use the punchout identity provider only, when the user enters the punchout route.
In that case the nginx should be configured with the OVERRIDE_IDENTITY_PROVIDERS environment variable (see [Override Identity Providers by Path][nginx-startup]).
Nevertheless the SSR process needs to be provided with the punchout identity provider configuration.

[ssr-startup]: ../guides/ssr-startup.md
[nginx-startup]: ../guides/nginx-startup.md
4 changes: 2 additions & 2 deletions e2e/cypress/e2e/pages/account/login.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ export class LoginPage {
}

submit() {
cy.intercept('GET', /.*\/customers\/-.*/).as('currentCustomer');
cy.intercept('POST', /.*\/token/).as('token');
cy.wait(500);

cy.get('button[name="login"]').click();

return cy.wait('@currentCustomer');
return cy.wait('@token');
}

get errorText() {
Expand Down
9 changes: 7 additions & 2 deletions e2e/cypress/e2e/pages/header.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export class HeaderModule {

gotoLoginPage(wait: () => unknown = waitLoadingEnd) {
cy.scrollTo('top');
cy.get('[data-testing-id="user-status-desktop"] .my-account-login').click();
this.loginLink.click();
wait();
}

Expand All @@ -51,8 +51,13 @@ export class HeaderModule {
wait();
}

logout() {
logout(wait: () => unknown = waitLoadingEnd) {
cy.get('[data-testing-id="link-logout"]').first().click();
wait();
}

get loginLink() {
return cy.get('[data-testing-id="user-status-desktop"] .my-account-login');
}

get myAccountLink() {
Expand Down
8 changes: 7 additions & 1 deletion e2e/cypress/e2e/specs/account/register-user.b2b.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,16 @@ describe('New B2B User', () => {
});
});

it('should log out and log in and log out again', () => {
it('should log out', () => {
at(MyAccountPage, page => {
page.header.logout();
});
at(HomePage, page => {
page.header.loginLink.should('be.visible');
});
});

it('should log in and log out again', () => {
at(HomePage, page => {
page.header.gotoLoginPage();
});
Expand Down
4 changes: 3 additions & 1 deletion e2e/cypress/e2e/specs/system/cookie-consent.b2c.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ describe('Cookie Consent', () => {
it('should accept all cookies', () => {
at(HomePage, () => {
cy.get('[data-testing-id="acceptAllButton"]').click();
cy.wait(3000);
cy.wait(4000);

cy.getCookies().then(cookies => {
expect(cookies[cookies.length - 1]).to.have.property('name', 'cookieConsent');
cy.wait(500);
cy.get('.cookies-banner').should('not.exist');
});
});
});
Expand Down
32 changes: 26 additions & 6 deletions e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,32 +26,52 @@ describe('Returning User', () => {
at(MyAccountPage, page =>
page.header.myAccountLink.should('have.text', `${_.user.firstName} ${_.user.lastName}`)
);
cy.getCookie('apiToken').should('not.be.empty');
cy.getCookie('apiToken')
.should('not.be.empty')
.should(cookie => {
cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user');
});
});

it('should stay logged in when refreshing page once', () => {
MyAccountPage.navigateTo();
at(MyAccountPage);
cy.getCookie('apiToken').should('not.be.empty');
cy.getCookie('apiToken')
.should('not.be.empty')
.should(cookie => {
cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user');
});
});

it('should stay logged in when refreshing page twice', () => {
MyAccountPage.navigateTo();
at(MyAccountPage);
cy.getCookie('apiToken').should('not.be.empty');
cy.getCookie('apiToken')
.should('not.be.empty')
.should(cookie => {
cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user');
});
});

it('should stay logged in when refreshing page thrice', () => {
MyAccountPage.navigateTo();
at(MyAccountPage);
cy.getCookie('apiToken').should('not.be.empty');
cy.getCookie('apiToken')
.should('not.be.empty')
.should(cookie => {
cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'user');
});
});

it('should log out and loose the cookie', () => {
it('should log out and get the anonymous token', () => {
at(MyAccountPage, page => page.header.logout());
at(HomePage);
// eslint-disable-next-line unicorn/no-null
cy.getCookie('apiToken').should('equal', null);
cy.getCookie('apiToken')
.should('not.be.empty')
.should(cookie => {
cy.wrap(JSON.parse(decodeURIComponent(cookie.value))).should('have.property', 'type', 'anonymous');
});
});
});

Expand Down
21 changes: 18 additions & 3 deletions src/app/core/facades/account.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,17 +52,23 @@ import {
loadUserPaymentMethods,
loginUser,
loginUserWithToken,
logoutUser,
requestPasswordReminder,
resetPasswordReminder,
updateCustomer,
updateUser,
updateUserPassword,
updateUserPasswordByPasswordReminder,
updateUserPreferredPayment,
fetchAnonymousUserToken,
logoutUser,
logoutUserSuccess,
} from 'ish-core/store/customer/user';
import { whenTruthy } from 'ish-core/utils/operators';

interface LogoutUserOptions {
revokeApiToken: boolean;
}

/* eslint-disable @typescript-eslint/member-ordering */
@Injectable({ providedIn: 'root' })
export class AccountFacade {
Expand Down Expand Up @@ -92,8 +98,17 @@ export class AccountFacade {
this.store.dispatch(loginUserWithToken({ token }));
}

logoutUser() {
this.store.dispatch(logoutUser());
/**
* Trigger logout action
*
* @param revokeToken option to revoke api token on server side before logout success action is dispatched
*/
logoutUser(options: LogoutUserOptions = { revokeApiToken: true }) {
options?.revokeApiToken ? this.store.dispatch(logoutUser()) : this.store.dispatch(logoutUserSuccess());
}

fetchAnonymousToken() {
this.store.dispatch(fetchAnonymousUserToken());
}

createUser(body: CustomerRegistrationType) {
Expand Down
Loading

0 comments on commit a9195ad

Please sign in to comment.