Skip to content

Commit

Permalink
feat: punchout identity provider (#916)
Browse files Browse the repository at this point in the history
Co-authored-by: Marcus Schmidt <marcus.schmidt@intershop.de>
  • Loading branch information
Eisie96 and marschmidt89 authored Nov 22, 2021
1 parent 058533f commit 36fb90f
Show file tree
Hide file tree
Showing 15 changed files with 708 additions and 196 deletions.
1 change: 1 addition & 0 deletions docs/concepts/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Of course, the ICM server must supply appropriate REST resources to leverage fun
| orderTemplates | order templates feature |
| quickorder | quick order page and direct add to cart input |
| quoting | quoting feature |
| punchout | punchout feature |
| **B2C Features** | |
| guestCheckout | allow unregistered guest checkout |
| wishlists | wishlist product list feature |
Expand Down
40 changes: 31 additions & 9 deletions docs/guides/nginx-startup.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ nginx:
Entries of the IP whitelist are added to the nginx config as [`allow`](http://nginx.org/en/docs/http/ngx_http_access_module.html) statements, which also supports IP ranges.
Please refer to the linked nginx documentation on how to configure this.

After activating basic authentication for your setup globally you can also selectively deactivate it per site.
After globally activating basic authentication for your setup you can also disable it selectively per site.
See [Multi-Site Configurations](../guides/multi-site-configurations.md#Examples) for examples on how to do that.

### Multi-Site
Expand All @@ -53,13 +53,13 @@ For more information on the multi-site syntax, refer to [Multi-Site Configuratio

The configuration can be supplied simply by setting the environment variable `MULTI_CHANNEL`.
Alternatively, the source can be supplied by setting `MULTI_CHANNEL_SOURCE` in any [supported format by gomplate](https://docs.gomplate.ca/datasources/).
If no environment variables for multi-channel configuration are given, the configuration will fall back to the content of [`nginx/multi-channel.yaml`](../../nginx/multi-channel.yaml), which can also be customized.
If no environment variables for multi-channel configuration are provided, the configuration will fall back to the content of [`nginx/multi-channel.yaml`](../../nginx/multi-channel.yaml), which can also be customized.

> :warning: Multi-Channel configuration with context paths does not work in conjunction with [service workers](../concepts/progressive-web-app.md#service-worker)

An extended list of examples can be found in the [Multi-Site Configurations](../guides/multi-site-configurations.md#Syntax) guide.

### Ignore parameters during caching
### Ignore Parameters During Caching

Often, nginx receives requests from advertising networks or various user agents that append unused query parameters when making a request, for example `utm_source`. <br>
These parameters can lead to inefficient caching because even if the same URL is requested multiple times, if it is accessed with different query parameters, the cached version will not be used.
Expand All @@ -70,19 +70,19 @@ As with multi-site handling above, the configuration can be supplied simply by s
Alternatively, the source can be supplied by setting `CACHING_IGNORE_PARAMS_SOURCE` in any [supported format by gomplate](https://docs.gomplate.ca/datasources/).
Be aware that the supplied list of parameters must be declared under a `params` property.

If no environment variables for ignoring parameters are given, the configuration will fall back to the content of [`nginx/caching-ignore-params.yaml`](../../nginx/caching-ignore-params.yaml), which can also be customized.
If no environment variables for ignoring parameters are provided, the configuration will fall back to the content of [`nginx/caching-ignore-params.yaml`](../../nginx/caching-ignore-params.yaml), which can also be customized.

### Access ICM sitemap
### Access ICM Sitemap

Please refer to [this](https://support.intershop.com/kb/index.php/Display/23D962#ConceptXMLSitemaps-XMLSitemapsandIntershopPWAxml_sitemap_pwa) Intershop knowledge base article on how to configure ICM to generate PWA sitemap files.

```
http://pwa/sitemap_pwa.xml
```
To make above sitemap index file available under your deployment you need to add environment variable `ICM_BASE_URL` in your nginx container.
To make above sitemap index file available under your deployment you need to add the environment variable `ICM_BASE_URL` to your nginx container.
Let `ICM_BASE_URL` point to your ICM backend installation, e.g. `https://pwa-ish-demo.test.intershop.com`.
When the container is started it'll process cache-ignore and multi-channel templates as well as sitemap proxy rules like this:
When the container is started it will process cache-ignore and multi-channel templates as well as sitemap proxy rules like this:
```yaml
location /sitemap_ {
Expand All @@ -93,6 +93,28 @@ proxy_pass https://pwa-ish-demo.test.intershop.com/INTERSHOP/static/WFS/inSPIRED
The process will utilize your [Multi-Site Configuration](../guides/multi-site-configurations.md#Syntax).
Be sure to include `application` if you deviate from standard `rest` application.

### Override Identity Providers by Path

The PWA can be configured with multiple identity providers.
In some use cases a specific identity provider must be selected, when a certain route is requested.
For example, a punchout user should be logged in by the punchout identity provider requesting a punchout route.
For all other possible routes the default identity provider must be selected.
This can be done by setting only the environment variable `OVERRIDE_IDENTITY_PROVIDER`.

```yaml
nginx:
environment:
OVERRIDE_IDENTITY_PROVIDERS: |
.+:
- path: /b2b/punchout
type: PUNCHOUT
```
This setting will generate rewrite rules for the URL paths for all given domains.
Alternatively, the source can be supplied by setting `OVERRIDE_IDENTITY_PROVIDERS_SOURCE` in any supported format by gomplate.

If no environment variable is set, this feature is disabled.

### Other

The page speed configuration can also be overridden:
Expand All @@ -111,9 +133,9 @@ Built-in features can be enabled and disabled:
## Features

New features can be supplied in the folder `nginx/features`.
A file named `<feature>.conf` is included if the environment variable `<feature>` is set to `on`, `1`, `true` or `yes` (checked case in-sensitive).
A file named `<feature>.conf` is included if the environment variable `<feature>` is set to `on`, `1`, `true` or `yes` (case in-sensitive).
The feature is disabled otherwise and an optional file `<feature>-off.conf` is included in the configuration.
The feature name must be all word-characters (letters, numbers and underscore).
The feature name must only contain word characters (letters, numbers and underscore).

### Cache

Expand Down
12 changes: 11 additions & 1 deletion nginx/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ set -e

[ -z "$ICM_BASE_URL" ] && echo "ICM_BASE_URL is not set. Cannot use sitemap proxy feature."

[ -z "$OVERRIDE_IDENTITY_PROVIDERS" ] && echo "OVERRIDE_IDENTITY_PROVIDER is not set. Cannot use override identity provider feature."

[ -f "/etc/nginx/conf.d/default.conf" ] && rm /etc/nginx/conf.d/default.conf

if [ -n "$BASIC_AUTH" ]
Expand All @@ -26,6 +28,14 @@ then
fi
fi

if [ -z "$OVERRIDE_IDENTITY_PROVIDERS_SOURCE" ]
then
if [ -n "$OVERRIDE_IDENTITY_PROVIDERS" ]
then
OVERRIDE_IDENTITY_PROVIDERS_SOURCE="env:///OVERRIDE_IDENTITY_PROVIDERS?type=application/yaml"
fi
fi

if [ -z "$CACHING_IGNORE_PARAMS_SOURCE"]
then
if [ -z "$CACHING_IGNORE_PARAMS"]
Expand All @@ -36,7 +46,7 @@ then
fi
fi

/gomplate -d "domains=$MULTI_CHANNEL_SOURCE" -d "cachingIgnoreParams=$CACHING_IGNORE_PARAMS_SOURCE" -d 'ipwhitelist=env:///BASIC_AUTH_IP_WHITELIST?type=application/yaml' --input-dir="/etc/nginx/templates" --output-map='/etc/nginx/conf.d/{{ .in | strings.ReplaceAll ".conf.tmpl" ".conf" }}'
/gomplate -d "domains=$MULTI_CHANNEL_SOURCE" -d "overrideIdentityProviders=$OVERRIDE_IDENTITY_PROVIDERS_SOURCE" -d "cachingIgnoreParams=$CACHING_IGNORE_PARAMS_SOURCE" -d 'ipwhitelist=env:///BASIC_AUTH_IP_WHITELIST?type=application/yaml' --input-dir="/etc/nginx/templates" --output-map='/etc/nginx/conf.d/{{ .in | strings.ReplaceAll ".conf.tmpl" ".conf" }}'

# Generate Pagespeed config based on environment variables
env | grep NPSC_ | sed -e 's/^NPSC_//g' -e "s/\([A-Z_]*\)=/\L\1=/g" -e "s/_\([a-zA-Z]\)/\u\1/g" -e "s/^\([a-zA-Z]\)/\u\1/g" -e 's/=.*$//' -e 's/\=/ /' -e 's/^/\pagespeed /' > /tmp/pagespeed-prefix.txt
Expand Down
23 changes: 21 additions & 2 deletions nginx/templates/multi-channel.conf.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{{ define "LOCATION_TEMPLATE" }}
{{- $channel := .channel }}
{{- $application := "" }}{{ if (has . "application") }}{{ $application = join ( slice ";application" .application ) "=" }}{{ end }}
{{- $identityProvider := "" }}{{ if (has . "identityProvider") }}{{ $identityProvider = join ( slice ";identityProvider" .identityProvider ) "=" }}{{ end }}
{{- $identityProvider := "" }}{{ if (has . "identityProvider") }}{{ $identityProvider = .identityProvider }}{{ end }}
{{- $lang := "default" }}{{ if (has . "lang") }}{{ $lang = .lang }}{{ end }}
{{- $currency := "" }}{{ if (has . "currency") }}{{ $currency = join ( slice ";currency" .currency ) "=" }}{{ end }}
{{- $features := "" }}{{ if (has . "features") }}{{ $features = join ( slice ";features" .features ) "=" }}{{ end }}
Expand Down Expand Up @@ -46,7 +46,11 @@
rewrite '^(?!.*;lang=.*)(.*)$' '$1;lang={{ $lang }}';
rewrite '^(?!.*;currency=.*)(.*)$' '$1;currency={{ $currency }}';

set $default_rewrite_params ';icmHost={{ $icmHost }}{{ $icmScheme }}{{ $icmPort }};channel={{ $channel }}{{ $application }}{{ $identityProvider }}{{ $features }}{{ $theme }};baseHref={{ $baseHref | strings.ReplaceAll "/" "%2F" }};device=$ua_device';
{{ if $identityProvider }}
rewrite '^(?!.*;identityProvider=.*)(.*)$' '$1;identityProvider={{ $identityProvider }}';
{{ end }}

set $default_rewrite_params ';icmHost={{ $icmHost }}{{ $icmScheme }}{{ $icmPort }};channel={{ $channel }}{{ $application }}{{ $features }}{{ $theme }};baseHref={{ $baseHref | strings.ReplaceAll "/" "%2F" }};device=$ua_device';

rewrite '^(.*)$' '$1$default_rewrite_params' break;

Expand All @@ -56,6 +60,11 @@
proxy_busy_buffers_size 256k;
{{- end }}

{{- define "OVERRIDE_IDENTITY_PROVIDER_TEMPLATE" }}
{{- $identityProvider := join ( slice ";identityProvider" .type ) "=" }}
rewrite '^((.*){{ .path }})$' '$1{{$identityProvider}}';
{{- end}}

{{- range $domain, $mapping := (ds "domains") }}
server {
server_name ~^{{ $domain }}$;
Expand All @@ -75,6 +84,16 @@ server {
include /etc/nginx/conf.d/cache-blacklist.conf;


{{- if getenv "OVERRIDE_IDENTITY_PROVIDERS" }}
{{- range $domainOverride, $identyProviderOverride := (ds "overrideIdentityProviders") }}
{{ if eq $domain $domainOverride }}
{{- range $identyProviderOverride }}
{{- tmpl.Exec "OVERRIDE_IDENTITY_PROVIDER_TEMPLATE" . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

{{ if getenv "DEBUG" | strings.ToLower | regexp.Match "on|1|true|yes" }}
error_log /dev/stdout notice;
rewrite_log on;
Expand Down
22 changes: 3 additions & 19 deletions src/app/core/guards/identity-provider-logout.guard.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,16 @@
import { Injectable } from '@angular/core';
import { CanActivate } from '@angular/router';
import { isObservable, of } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';

import { AccountFacade } from 'ish-core/facades/account.facade';
import { IdentityProviderFactory } from 'ish-core/identity-provider/identity-provider.factory';
import { RoleToggleService } from 'ish-core/utils/role-toggle/role-toggle.service';

@Injectable({ providedIn: 'root' })
export class IdentityProviderLogoutGuard implements CanActivate {
constructor(
private identityProviderFactory: IdentityProviderFactory,
private roleToggleService: RoleToggleService,
private accountFacade: AccountFacade
) {}
constructor(private identityProviderFactory: IdentityProviderFactory) {}

canActivate() {
return this.roleToggleService.hasRole(['APP_B2B_CXML_USER', 'APP_B2B_OCI_USER']).pipe(
take(1),
switchMap(isPunchout => {
if (isPunchout) {
this.accountFacade.logoutUser();
return of(false);
}
const logoutReturn$ = this.identityProviderFactory.getInstance().triggerLogout();
return isObservable(logoutReturn$) || isPromise(logoutReturn$) ? logoutReturn$ : of(logoutReturn$);
})
);
const logoutReturn$ = this.identityProviderFactory.getInstance().triggerLogout();
return isObservable(logoutReturn$) || isPromise(logoutReturn$) ? logoutReturn$ : of(logoutReturn$);
}
}

Expand Down
4 changes: 3 additions & 1 deletion src/app/core/identity-provider.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { NgModuleWithProviders } from 'ng-mocks';
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';
Expand All @@ -21,7 +23,7 @@ export function storageFactory(platformId: string): OAuthStorage {
}

@NgModule({
imports: [OAuthModule.forRoot({ resourceServer: { sendAccessToken: false } })],
imports: [OAuthModule.forRoot({ resourceServer: { sendAccessToken: false } }), PunchoutIdentityProviderModule],
providers: [
{ provide: OAuthStorage, useFactory: storageFactory, deps: [PLATFORM_ID] },
{
Expand Down
20 changes: 16 additions & 4 deletions src/app/core/identity-provider/identity-provider.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,21 @@ import { Store, select } from '@ngrx/store';
import { noop } from 'rxjs';
import { first } from 'rxjs/operators';

import { FeatureToggleService } from 'ish-core/feature-toggle.module';
import { getIdentityProvider } from 'ish-core/store/core/configuration';
import { whenTruthy } from 'ish-core/utils/operators';

import { IdentityProvider } from './identity-provider.interface';

export const IDENTITY_PROVIDER_IMPLEMENTOR = new InjectionToken<{
interface IdentityProviderImplementor {
type: string;
implementor: Type<IdentityProvider<unknown>>;
}>('identityProviderImplementor');
feature?: string;
}

export const IDENTITY_PROVIDER_IMPLEMENTOR = new InjectionToken<IdentityProviderImplementor>(
'identityProviderImplementor'
);

@Injectable({ providedIn: 'root' })
export class IdentityProviderFactory {
Expand All @@ -22,11 +28,17 @@ export class IdentityProviderFactory {
type?: string;
};

constructor(private store: Store, private injector: Injector, @Inject(PLATFORM_ID) platformId: string) {
constructor(
private store: Store,
private injector: Injector,
private featureToggleService: FeatureToggleService,
@Inject(PLATFORM_ID) platformId: string
) {
if (isPlatformBrowser(platformId)) {
this.store.pipe(select(getIdentityProvider), whenTruthy(), first()).subscribe(config => {
const provider = this.injector
.get<{ type: string; implementor: Type<IdentityProvider<unknown>> }[]>(IDENTITY_PROVIDER_IMPLEMENTOR, [])
.get<IdentityProviderImplementor[]>(IDENTITY_PROVIDER_IMPLEMENTOR, [])
.filter(p => (p.feature ? this.featureToggleService.enabled(p.feature) : true))
.find(p => p.type === config?.type);

if (!provider) {
Expand Down
16 changes: 16 additions & 0 deletions src/app/core/store/customer/basket/basket.effects.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,22 @@ describe('Basket Effects', () => {

expect(effects.loadBasket$).toBeObservable(expected$);
});

describe('with basket-id in session storage', () => {
beforeEach(() => {
window.sessionStorage.clear();
});

it('should map to action of type LoadBasketWithId', () => {
window.sessionStorage.setItem('basket-id', 'BID');
const action = loadBasket();
const completion = loadBasketWithId({ basketId: 'BID' });
actions$ = hot('-a-a-a', { a: action });
const expected$ = cold('-c-c-c', { c: completion });

expect(effects.loadBasket$).toBeObservable(expected$);
});
});
});

describe('loadBasketWithId$', () => {
Expand Down
16 changes: 10 additions & 6 deletions src/app/core/store/customer/basket/basket.effects.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { Inject, Injectable, PLATFORM_ID } from '@angular/core';
import { Router } from '@angular/router';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { routerNavigatedAction } from '@ngrx/router-store';
Expand Down Expand Up @@ -63,7 +64,8 @@ export class BasketEffects {
private basketService: BasketService,
private apiTokenService: ApiTokenService,
private router: Router,
private store: Store
private store: Store,
@Inject(PLATFORM_ID) private platformId: string
) {}

/**
Expand All @@ -73,10 +75,12 @@ export class BasketEffects {
this.actions$.pipe(
ofType(loadBasket),
mergeMap(() =>
this.basketService.getBasket().pipe(
map(basket => loadBasketSuccess({ basket })),
mapErrorToAction(loadBasketFail)
)
isPlatformBrowser(this.platformId) && window.sessionStorage.getItem('basket-id')
? of(loadBasketWithId({ basketId: window.sessionStorage.getItem('basket-id') }))
: this.basketService.getBasket().pipe(
map(basket => loadBasketSuccess({ basket })),
mapErrorToAction(loadBasketFail)
)
)
)
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';

import { IDENTITY_PROVIDER_IMPLEMENTOR } from 'ish-core/identity-provider/identity-provider.factory';

import { PunchoutIdentityProvider } from './punchout-identity-provider';

@NgModule({
providers: [
{
provide: IDENTITY_PROVIDER_IMPLEMENTOR,
multi: true,
useValue: {
type: 'PUNCHOUT',
implementor: PunchoutIdentityProvider,
feature: 'punchout',
},
},
],
})
export class PunchoutIdentityProviderModule {}
Loading

0 comments on commit 36fb90f

Please sign in to comment.