Skip to content

Commit 8e39582

Browse files
authored
Login when initial token is invalid (#20081)
CXSPA-9618
1 parent 153cc5f commit 8e39582

8 files changed

+285
-2
lines changed

integration-libs/punchout/root/model/punchout.model.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
export const PUNCHOUT_SESSION_KEY = 'sid';
88
export const PUNCHOUT_ERROR_PAGE_URL = '/punchout/cxml/error';
9+
export const PUNCHOUT_SESSION_PAGE_URL = '/punchout/cxml/session';
910
export const PUNCHOUT_SESSION_ID = 'punchoutSessionId';
1011

1112
export enum PunchOutLevel {

integration-libs/punchout/root/public_api.ts

+1
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './facade/index';
88
export * from './feature-name';
99
export * from './model/index';
1010
export * from './punchout.root.module';
11+
export * from './services/index';

integration-libs/punchout/root/punchout.root.module.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,13 @@
55
*/
66

77
import { NgModule } from '@angular/core';
8-
import { CmsConfig, provideDefaultConfigFactory } from '@spartacus/core';
8+
import {
9+
AuthHttpHeaderService,
10+
CmsConfig,
11+
provideDefaultConfigFactory,
12+
} from '@spartacus/core';
913
import { PUNCHOUT_FEATURE } from './feature-name';
14+
import { PunchoutAuthHttpHeaderService } from './services/punchout-auth-http-header.service';
1015

1116
export function defaultPunchoutCmsComponentsConfig(): CmsConfig {
1217
const config: CmsConfig = {
@@ -20,6 +25,12 @@ export function defaultPunchoutCmsComponentsConfig(): CmsConfig {
2025
}
2126

2227
@NgModule({
23-
providers: [provideDefaultConfigFactory(defaultPunchoutCmsComponentsConfig)],
28+
providers: [
29+
provideDefaultConfigFactory(defaultPunchoutCmsComponentsConfig),
30+
{
31+
provide: AuthHttpHeaderService,
32+
useExisting: PunchoutAuthHttpHeaderService,
33+
},
34+
],
2435
})
2536
export class PunchoutRootModule {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
export * from './punchout-auth-http-header.service';
8+
export * from './punchout-detection.service';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { TestBed } from '@angular/core/testing';
2+
import {
3+
AuthRedirectService,
4+
AuthService,
5+
AuthStorageService,
6+
AuthToken,
7+
GlobalMessageService,
8+
OAuthLibWrapperService,
9+
OccEndpointsService,
10+
RoutingService,
11+
} from '@spartacus/core';
12+
import { of } from 'rxjs';
13+
import { PunchoutAuthHttpHeaderService } from './punchout-auth-http-header.service';
14+
import { PunchoutDetectionService } from './punchout-detection.service';
15+
16+
class MockPunchoutDetectionService
17+
implements Partial<PunchoutDetectionService>
18+
{
19+
isPunchoutSessionPage(): boolean {
20+
return true;
21+
}
22+
}
23+
24+
class MockAuthService implements Partial<AuthService> {
25+
coreLogout(): Promise<void> {
26+
return Promise.resolve();
27+
}
28+
}
29+
30+
class MockAuthStorageService implements Partial<AuthStorageService> {
31+
getToken() {
32+
return of({ access_token: 'acc_token' } as AuthToken);
33+
}
34+
}
35+
36+
class MockOAuthLibWrapperService implements Partial<OAuthLibWrapperService> {}
37+
38+
class MockRoutingService implements Partial<RoutingService> {
39+
go = () => Promise.resolve(true);
40+
}
41+
42+
class MockGlobalMessageService implements Partial<GlobalMessageService> {
43+
add() {}
44+
remove() {}
45+
}
46+
47+
class MockOccEndpointsService implements Partial<OccEndpointsService> {
48+
getBaseUrl() {
49+
return 'some-server/occ';
50+
}
51+
getRawEndpointValue(): string {
52+
return 'some-endpoint';
53+
}
54+
}
55+
56+
class MockAuthRedirectService implements Partial<AuthRedirectService> {
57+
saveCurrentNavigationUrl = jasmine.createSpy('saveCurrentNavigationUrl');
58+
}
59+
60+
describe('PunchoutAuthHttpHeaderService', () => {
61+
let service: PunchoutAuthHttpHeaderService;
62+
let authService: AuthService;
63+
let punchoutDetectionService: PunchoutDetectionService;
64+
let globalMessageService: GlobalMessageService;
65+
beforeEach(() => {
66+
TestBed.configureTestingModule({
67+
imports: [],
68+
providers: [
69+
{
70+
provide: PunchoutDetectionService,
71+
useClass: MockPunchoutDetectionService,
72+
},
73+
{ provide: AuthService, useClass: MockAuthService },
74+
{ provide: GlobalMessageService, useClass: MockGlobalMessageService },
75+
{ provide: OccEndpointsService, useClass: MockOccEndpointsService },
76+
{ provide: AuthStorageService, useClass: MockAuthStorageService },
77+
{ provide: AuthRedirectService, useClass: MockAuthRedirectService },
78+
{
79+
provide: OAuthLibWrapperService,
80+
useClass: MockOAuthLibWrapperService,
81+
},
82+
{ provide: RoutingService, useClass: MockRoutingService },
83+
],
84+
});
85+
service = TestBed.inject(PunchoutAuthHttpHeaderService);
86+
authService = TestBed.inject(AuthService);
87+
punchoutDetectionService = TestBed.inject(PunchoutDetectionService);
88+
globalMessageService = TestBed.inject(GlobalMessageService);
89+
});
90+
91+
it('should be created', () => {
92+
expect(service).toBeTruthy();
93+
});
94+
95+
it('should handleExpiredRefreshToken call super coreLogout when not Punchout Session page', async () => {
96+
spyOn(authService, 'coreLogout').and.callThrough();
97+
spyOn(punchoutDetectionService, 'isPunchoutSessionPage').and.returnValue(
98+
false
99+
);
100+
spyOn(globalMessageService, 'remove').and.callThrough();
101+
102+
service.handleExpiredRefreshToken();
103+
await Promise.resolve();
104+
105+
expect(authService.coreLogout).toHaveBeenCalled();
106+
expect(globalMessageService.remove).not.toHaveBeenCalled();
107+
});
108+
109+
it('should handleExpiredRefreshToken silently logout when Punchout Session page', async () => {
110+
spyOn(authService, 'coreLogout').and.callThrough();
111+
spyOn(punchoutDetectionService, 'isPunchoutSessionPage').and.returnValue(
112+
true
113+
);
114+
spyOn(globalMessageService, 'remove').and.callThrough();
115+
116+
service.handleExpiredRefreshToken();
117+
await Promise.resolve();
118+
119+
expect(authService.coreLogout).toHaveBeenCalled();
120+
expect(globalMessageService.remove).toHaveBeenCalled();
121+
});
122+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { inject, Injectable } from '@angular/core';
8+
import {
9+
AuthHttpHeaderService,
10+
AuthRedirectService,
11+
AuthService,
12+
AuthStorageService,
13+
GlobalMessageService,
14+
GlobalMessageType,
15+
OAuthLibWrapperService,
16+
OccEndpointsService,
17+
RoutingService,
18+
} from '@spartacus/core';
19+
import { PunchoutDetectionService } from './punchout-detection.service';
20+
21+
@Injectable({
22+
providedIn: 'root',
23+
})
24+
export class PunchoutAuthHttpHeaderService extends AuthHttpHeaderService {
25+
protected punchoutInitService = inject(PunchoutDetectionService);
26+
constructor(
27+
protected authService: AuthService,
28+
protected authStorageService: AuthStorageService,
29+
protected oAuthLibWrapperService: OAuthLibWrapperService,
30+
protected routingService: RoutingService,
31+
protected globalMessageService: GlobalMessageService,
32+
protected occEndpointsService: OccEndpointsService,
33+
protected authRedirectService: AuthRedirectService
34+
) {
35+
super(
36+
authService,
37+
authStorageService,
38+
oAuthLibWrapperService,
39+
routingService,
40+
occEndpointsService,
41+
globalMessageService,
42+
authRedirectService
43+
);
44+
}
45+
46+
/**
47+
* @override
48+
*
49+
* On backend errors indicating expired `refresh_token`, we need to silently logout
50+
* by revoking invalid token and preventing Login page redirection.
51+
* It is a workaround to address CXSPA-9608 - Public pages not displayed when token is invalid.
52+
* To be removed once CXSPA-9608 is closed.
53+
*/
54+
public handleExpiredRefreshToken(): void {
55+
if (!this.punchoutInitService.isPunchoutSessionPage()) {
56+
super.handleExpiredRefreshToken();
57+
} else {
58+
this.authService.coreLogout().finally(() => {
59+
this.globalMessageService.remove(
60+
GlobalMessageType.MSG_TYPE_CONFIRMATION
61+
);
62+
});
63+
}
64+
}
65+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { Location } from '@angular/common';
2+
import { TestBed } from '@angular/core/testing';
3+
import { PUNCHOUT_SESSION_PAGE_URL } from '../model';
4+
import { PunchoutDetectionService } from './punchout-detection.service';
5+
6+
const MOCK_URL = 'https://test';
7+
const MOCK_URL_WITH_PUNCHOUT = `https://spartacus/${PUNCHOUT_SESSION_PAGE_URL}?sid=abc123`;
8+
9+
class MockLocation implements Partial<Location> {
10+
path() {
11+
return MOCK_URL;
12+
}
13+
}
14+
15+
describe('PunchoutDetectionService', () => {
16+
let service: PunchoutDetectionService;
17+
let location: Location;
18+
19+
beforeEach(() => {
20+
TestBed.configureTestingModule({
21+
imports: [],
22+
providers: [{ provide: Location, useClass: MockLocation }],
23+
});
24+
25+
location = TestBed.inject(Location);
26+
service = TestBed.inject(PunchoutDetectionService);
27+
});
28+
29+
it('should be created', () => {
30+
expect(service).toBeTruthy();
31+
});
32+
33+
it('should isPunchoutSessionPage falsy when url is not punchout session', (done) => {
34+
spyOn(location, 'path').and.returnValue(MOCK_URL);
35+
36+
const value = service.isPunchoutSessionPage();
37+
expect(value).toEqual(false);
38+
done();
39+
});
40+
41+
it('should isPunchoutSessionPage truthy when url punchout session', (done) => {
42+
spyOn(location, 'path').and.returnValue(MOCK_URL_WITH_PUNCHOUT);
43+
44+
const value = service.isPunchoutSessionPage();
45+
expect(value).toEqual(true);
46+
done();
47+
});
48+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <[email protected]>
3+
*
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import { Location } from '@angular/common';
8+
import { Injectable, inject } from '@angular/core';
9+
import { PUNCHOUT_SESSION_PAGE_URL } from '../model';
10+
11+
@Injectable({ providedIn: 'root' })
12+
export class PunchoutDetectionService {
13+
protected location = inject(Location);
14+
15+
/**
16+
* Check if browser url is the punchout initial session page.
17+
* With default config, the expected url shape is '/punchout/cxml/session?abcd'.
18+
* @returns boolean
19+
*/
20+
isPunchoutSessionPage(): boolean {
21+
const urlSections = this.location.path().split('?');
22+
return (
23+
urlSections.length > 1 &&
24+
urlSections[0].includes(PUNCHOUT_SESSION_PAGE_URL)
25+
);
26+
}
27+
}

0 commit comments

Comments
 (0)