Skip to content

Commit

Permalink
feat: use ICM '/token' REST endpoint for authentication (#1156)
Browse files Browse the repository at this point in the history
* old token behavior with the token exchange for each request is replaced by the exchange via token API
* get tokens for an anonymous user and for a user with credentials
* refresh token before it is expired
* revoke token on logout

BREAKING CHANGES: PWA uses the ICM `/token` REST endpoint to retrieve user token, every anonymous user will get a anonymous user token, every identity provider has to configure the `oAuthService` with information about the token endpoint, before expiration the given token should be refreshed.
  • Loading branch information
Eisie96 committed Dec 22, 2022
1 parent ddd6e2e commit 4e02efd
Show file tree
Hide file tree
Showing 32 changed files with 1,118 additions and 321 deletions.
6 changes: 6 additions & 0 deletions docs/guides/migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ To handle the different variation select rendering types the existing `ProductVa
The rendering and behavior of the existing `ProductVariationSelectComponent` as a standard select box was moved to the new `ProductVariationSelectDefaultComponent`.
A `ProductVariationSelectSwatchComponent` for colorCode and swatchImage variation select rendering and a `ProductVariationSelectEnhancedComponent` for a select box rendering with color codes or swatch images and a mobile optimization were added.

The user authentication process has changed.
User authentication tokens are requested from the ICM server using the `/token` REST endpoint now.
Regarding 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 is 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
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
15 changes: 13 additions & 2 deletions src/app/core/facades/account.facade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
import {
createUser,
deleteUserPaymentInstrument,
fetchAnonymousUserToken,
getCustomerApprovalEmail,
getLoggedInCustomer,
getLoggedInUser,
Expand All @@ -53,6 +54,7 @@ import {
loginUser,
loginUserWithToken,
logoutUser,
logoutUserSuccess,
requestPasswordReminder,
resetPasswordReminder,
updateCustomer,
Expand Down Expand Up @@ -92,8 +94,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 = { revokeApiToken: true }) {
options?.revokeApiToken ? this.store.dispatch(logoutUser()) : this.store.dispatch(logoutUserSuccess());
}

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

createUser(body: CustomerRegistrationType) {
Expand Down
26 changes: 24 additions & 2 deletions src/app/core/identity-provider.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { HttpHandler, HttpRequest } from '@angular/common/http';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { APP_INITIALIZER, ModuleWithProviders, NgModule } from '@angular/core';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { noop } from 'rxjs';
import { BehaviorSubject, noop, of, race, timer } 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 @@ -20,9 +21,23 @@ 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 @@ -63,6 +78,13 @@ export class IdentityProviderModule {
getType: () => 'ICM',
},
},
{
provide: OAuthConfigurationService,
useValue: {
loadConfig$: of({}),
config$: new BehaviorSubject({}),
},
},
],
};
}
Expand Down
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 { of } from 'rxjs';
import { BehaviorSubject, 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,6 +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 { Auth0Config, Auth0IdentityProvider } from './auth0.identity-provider';

Expand All @@ -35,6 +36,7 @@ describe('Auth0 Identity Provider', () => {
const oAuthService = mock(OAuthService);
const apiService = mock(ApiService);
const apiTokenService = mock(ApiTokenService);
const oAuthConfigurationService = mock(OAuthConfigurationService);
let auth0IdentityProvider: Auth0IdentityProvider;
let store$: MockStore;
let storeSpy$: MockStore;
Expand All @@ -55,6 +57,7 @@ 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(),
],
Expand All @@ -67,14 +70,15 @@ describe('Auth0 Identity Provider', () => {
});

beforeEach(() => {
when(apiTokenService.restore$(anything())).thenReturn(of(true));
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(apiService.post(anything(), anything())).thenReturn(of(userData));
});

Expand Down
Loading

0 comments on commit 4e02efd

Please sign in to comment.