From a16501f19daf4f7b6129f04263c8d4ea16764c2e Mon Sep 17 00:00:00 2001 From: Marcel Eisentraut Date: Thu, 20 Apr 2023 15:53:50 +0200 Subject: [PATCH] feat: fetch anonymous user token only on basket creation when no apiToken is available --- docs/guides/migrations.md | 2 + .../retain-authentication.b2c.e2e-spec.ts | 8 +- .../auth0.identity-provider.ts | 2 +- .../icm.identity-provider.ts | 23 ++---- .../core/store/customer/user/user.effects.ts | 7 +- .../core/utils/api-token/api-token.service.ts | 82 +++++++++++++------ 6 files changed, 75 insertions(+), 49 deletions(-) diff --git a/docs/guides/migrations.md b/docs/guides/migrations.md index 65eada69f6..aab02acaf9 100644 --- a/docs/guides/migrations.md +++ b/docs/guides/migrations.md @@ -31,6 +31,8 @@ A new `TokenService` is introduced to be only responsible for fetching token dat 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 diff --git a/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts b/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts index 7a08ea092c..964c43b6a7 100644 --- a/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts +++ b/e2e/cypress/e2e/specs/system/retain-authentication.b2c.e2e-spec.ts @@ -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; + }); }); }); diff --git a/src/app/core/identity-provider/auth0.identity-provider.ts b/src/app/core/identity-provider/auth0.identity-provider.ts index 600bab7b79..58caece93e 100644 --- a/src/app/core/identity-provider/auth0.identity-provider.ts +++ b/src/app/core/identity-provider/auth0.identity-provider.ts @@ -90,7 +90,7 @@ export class Auth0IdentityProvider implements IdentityProvider { // anonymous user token should only be fetched when no user is logged in this.apiTokenService - .restore$(['user', 'order'], !this.oauthService.getIdToken()) + .restore$(['user', 'order']) .pipe( switchMap(() => from(this.oauthService.loadDiscoveryDocumentAndTryLogin())), switchMap(() => diff --git a/src/app/core/identity-provider/icm.identity-provider.ts b/src/app/core/identity-provider/icm.identity-provider.ts index 6391976180..41c7586634 100644 --- a/src/app/core/identity-provider/icm.identity-provider.ts +++ b/src/app/core/identity-provider/icm.identity-provider.ts @@ -3,7 +3,7 @@ import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Router } from '@angular/router'; import { Store, select } from '@ngrx/store'; import { Observable, noop } from 'rxjs'; -import { filter, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { AccountFacade } from 'ish-core/facades/account.facade'; import { selectQueryParam } from 'ish-core/store/core/router'; @@ -31,19 +31,14 @@ export class ICMIdentityProvider implements IdentityProvider { init() { this.apiTokenService.restore$().subscribe(noop); - this.apiTokenService.cookieVanishes$ - .pipe(withLatestFrom(this.apiTokenService.apiToken$)) - .subscribe(([type, apiToken]) => { - this.accountFacade.logoutUser({ revokeApiToken: false }); - if (!apiToken) { - this.accountFacade.fetchAnonymousToken(); - } - if (type === 'user') { - this.router.navigate(['/login'], { - queryParams: { returnUrl: this.router.url, messageKey: 'session_timeout' }, - }); - } - }); + 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' }, + }); + } + }); } triggerLogin(): TriggerReturnType { diff --git a/src/app/core/store/customer/user/user.effects.ts b/src/app/core/store/customer/user/user.effects.ts index be947bea7c..86e873a281 100644 --- a/src/app/core/store/customer/user/user.effects.ts +++ b/src/app/core/store/customer/user/user.effects.ts @@ -102,12 +102,7 @@ export class UserEffects { logoutUser$ = createEffect(() => this.actions$.pipe( ofType(logoutUser), - switchMap(() => - this.userService.logoutUser().pipe( - concatMap(() => [logoutUserSuccess(), fetchAnonymousUserToken()]), - mapErrorToAction(logoutUserFail) - ) - ) + switchMap(() => this.userService.logoutUser().pipe(map(logoutUserSuccess), mapErrorToAction(logoutUserFail))) ) ); diff --git a/src/app/core/utils/api-token/api-token.service.ts b/src/app/core/utils/api-token/api-token.service.ts index f502d8a6cb..e0ceba05ec 100644 --- a/src/app/core/utils/api-token/api-token.service.ts +++ b/src/app/core/utils/api-token/api-token.service.ts @@ -9,6 +9,7 @@ import { ReplaySubject, Subject, combineLatest, + iif, interval, of, race, @@ -28,6 +29,7 @@ import { startWith, switchMap, take, + tap, withLatestFrom, } from 'rxjs/operators'; @@ -36,7 +38,12 @@ import { User } from 'ish-core/models/user/user.model'; import { ApiService } from 'ish-core/services/api/api.service'; import { getCurrentBasket, getCurrentBasketId, loadBasketByAPIToken } from 'ish-core/store/customer/basket'; import { getOrder, getSelectedOrderId, loadOrderByAPIToken } from 'ish-core/store/customer/orders'; -import { getLoggedInUser, getUserAuthorized, loadUserByAPIToken } from 'ish-core/store/customer/user'; +import { + fetchAnonymousUserToken, + getLoggedInUser, + getUserAuthorized, + loadUserByAPIToken, +} from 'ish-core/store/customer/user'; import { CookiesService } from 'ish-core/utils/cookies/cookies.service'; import { mapToProperty, whenTruthy } from 'ish-core/utils/operators'; @@ -279,32 +286,57 @@ export class ApiTokenService { ); } + private anonymousUserTokenMechanism(): Observable { + return this.apiToken$.pipe( + switchMap( + apiToken => + iif(() => !apiToken, of(false).pipe(tap(() => this.store.dispatch(fetchAnonymousUserToken()))), of(true)) // fetch anonymous user token only when api token is not available + ), + whenTruthy(), + first() + ); + } + intercept(req: HttpRequest, next: HttpHandler): Observable> { - return this.appendAuthentication(req).pipe( - concatMap(request => - next.handle(request).pipe( - map(event => { - // remove id_token from /token response - // TODO: remove http request body adaptions if correct id_tokens are returned - if (event instanceof HttpResponse && event.url.endsWith('token') && request.body instanceof HttpParams) { - const { id_token: _, ...body } = event.body; - return event.clone({ - body, - }); - } - return event; - }), - catchError(err => { - if (this.isAuthTokenError(err)) { - this.invalidateApiToken(); + return iif( + () => req.url.endsWith('/baskets') && req.method === 'POST', // only on basket creation an anonymous user token can be created + this.anonymousUserTokenMechanism(), + of(true) + ).pipe( + switchMap(() => + this.appendAuthentication(req).pipe( + concatMap(request => + next.handle(request).pipe( + map(event => { + // remove id_token from /token response + // TODO: remove http request body adaptions if correct id_tokens are returned + if ( + event instanceof HttpResponse && + event.url.endsWith('token') && + request.body instanceof HttpParams + ) { + const { id_token: _, ...body } = event.body; + return event.clone({ + body, + }); + } + return event; + }), + catchError(err => { + if (this.isAuthTokenError(err)) { + this.invalidateApiToken(); - // retry request without auth token - const retryRequest = request.clone({ headers: request.headers.delete(ApiService.TOKEN_HEADER_KEY) }); - // timer introduced for testability - return timer(500).pipe(switchMap(() => next.handle(retryRequest))); - } - return throwError(() => err); - }) + // retry request without auth token + const retryRequest = request.clone({ + headers: request.headers.delete(ApiService.TOKEN_HEADER_KEY), + }); + // timer introduced for testability + return timer(500).pipe(switchMap(() => next.handle(retryRequest))); + } + return throwError(() => err); + }) + ) + ) ) ) );