Skip to content

Commit

Permalink
feat: fetch anonymous user token only on basket creation when no apiT…
Browse files Browse the repository at this point in the history
…oken is available
  • Loading branch information
Eisie96 authored and shauke committed Jun 16, 2023
1 parent 2aa15ba commit a16501f
Show file tree
Hide file tree
Showing 6 changed files with 75 additions and 49 deletions.
2 changes: 2 additions & 0 deletions docs/guides/migrations.md
Expand Up @@ -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

Expand Down
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
2 changes: 1 addition & 1 deletion src/app/core/identity-provider/auth0.identity-provider.ts
Expand Up @@ -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(() =>
Expand Down
23 changes: 9 additions & 14 deletions src/app/core/identity-provider/icm.identity-provider.ts
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 1 addition & 6 deletions src/app/core/store/customer/user/user.effects.ts
Expand Up @@ -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)))
)
);

Expand Down
82 changes: 57 additions & 25 deletions src/app/core/utils/api-token/api-token.service.ts
Expand Up @@ -9,6 +9,7 @@ import {
ReplaySubject,
Subject,
combineLatest,
iif,
interval,
of,
race,
Expand All @@ -28,6 +29,7 @@ import {
startWith,
switchMap,
take,
tap,
withLatestFrom,
} from 'rxjs/operators';

Expand All @@ -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';

Expand Down Expand Up @@ -279,32 +286,57 @@ export class ApiTokenService {
);
}

private anonymousUserTokenMechanism(): Observable<unknown> {
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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
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);
})
)
)
)
)
);
Expand Down

0 comments on commit a16501f

Please sign in to comment.