@platform-mesh/portal-server-lib 0.9.94 → 0.9.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -30,26 +30,34 @@ let PMLogoutService = PMLogoutService_1 = class PMLogoutService {
|
|
|
30
30
|
try {
|
|
31
31
|
const refreshToken = this.cookiesService.getAuthCookie(request);
|
|
32
32
|
const body = new URLSearchParams({
|
|
33
|
-
client_id: authConfig.clientId,
|
|
34
|
-
client_secret: authConfig.clientSecret,
|
|
35
33
|
refresh_token: refreshToken,
|
|
36
34
|
});
|
|
37
35
|
await firstValueFrom(this.httpService.post(authConfig.endSessionUrl, body, {
|
|
38
36
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
37
|
+
auth: {
|
|
38
|
+
username: authConfig.clientId,
|
|
39
|
+
password: authConfig.clientSecret,
|
|
40
|
+
},
|
|
39
41
|
}));
|
|
40
42
|
}
|
|
41
43
|
catch (error) {
|
|
42
44
|
this.logger.error('Error during keycloak logout', error?.response?.data || error.message);
|
|
43
45
|
this.logger.warn('Trying to log out with the id token');
|
|
44
|
-
return this.logoutWithIdToken(request, authConfig.endSessionUrl);
|
|
46
|
+
return this.logoutWithIdToken(request, authConfig.endSessionUrl, authConfig.clientId);
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
|
-
logoutWithIdToken(request, endSessionUrl) {
|
|
49
|
+
logoutWithIdToken(request, endSessionUrl, clientId) {
|
|
48
50
|
const { id_token_hint, post_logout_redirect_uri } = request.query;
|
|
49
|
-
const params = new URLSearchParams(
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
51
|
+
const params = new URLSearchParams();
|
|
52
|
+
if (id_token_hint) {
|
|
53
|
+
params.set('id_token_hint', String(id_token_hint));
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
params.set('client_id', clientId);
|
|
57
|
+
}
|
|
58
|
+
if (post_logout_redirect_uri) {
|
|
59
|
+
params.set('post_logout_redirect_uri', String(post_logout_redirect_uri));
|
|
60
|
+
}
|
|
53
61
|
return `${endSessionUrl}?${params}`;
|
|
54
62
|
}
|
|
55
63
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logout-callback.service.js","sourceRoot":"","sources":["../../src/portal-options/logout-callback.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EACL,2BAA2B,EAE3B,cAAc,GAEf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAG/B,IAAM,eAAe,uBAArB,MAAM,eAAe;IAKhB;IACA;IACA;IANF,MAAM,GAAW,IAAI,MAAM,CAAC,iBAAe,CAAC,IAAI,CAAC,CAAC;IAE1D,YAEU,iBAAoC,EACpC,WAAwB,EACxB,cAA8B;QAF9B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,gBAAW,GAAX,WAAW,CAAa;QACxB,mBAAc,GAAd,cAAc,CAAgB;IACrC,CAAC;IAEG,KAAK,CAAC,YAAY,CACvB,OAAgB,EAChB,QAAkB;QAElB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACvE,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAEhE,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;gBAC/B,
|
|
1
|
+
{"version":3,"file":"logout-callback.service.js","sourceRoot":"","sources":["../../src/portal-options/logout-callback.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EACL,2BAA2B,EAE3B,cAAc,GAEf,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAG/B,IAAM,eAAe,uBAArB,MAAM,eAAe;IAKhB;IACA;IACA;IANF,MAAM,GAAW,IAAI,MAAM,CAAC,iBAAe,CAAC,IAAI,CAAC,CAAC;IAE1D,YAEU,iBAAoC,EACpC,WAAwB,EACxB,cAA8B;QAF9B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,gBAAW,GAAX,WAAW,CAAa;QACxB,mBAAc,GAAd,cAAc,CAAgB;IACrC,CAAC;IAEG,KAAK,CAAC,YAAY,CACvB,OAAgB,EAChB,QAAkB;QAElB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,iBAAiB,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;QACvE,IAAI,CAAC;YACH,MAAM,YAAY,GAAG,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAEhE,MAAM,IAAI,GAAG,IAAI,eAAe,CAAC;gBAC/B,aAAa,EAAE,YAAY;aAC5B,CAAC,CAAC;YAMH,MAAM,cAAc,CAClB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,IAAI,EAAE;gBACpD,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;gBAChE,IAAI,EAAE;oBACJ,QAAQ,EAAE,UAAU,CAAC,QAAQ;oBAC7B,QAAQ,EAAE,UAAU,CAAC,YAAY;iBAClC;aACF,CAAC,CACH,CAAC;QACJ,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,KAAK,CACf,8BAA8B,EAC9B,KAAK,EAAE,QAAQ,EAAE,IAAI,IAAI,KAAK,CAAC,OAAO,CACvC,CAAC;YACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,qCAAqC,CAAC,CAAC;YACxD,OAAO,IAAI,CAAC,iBAAiB,CAC3B,OAAO,EACP,UAAU,CAAC,aAAa,EACxB,UAAU,CAAC,QAAQ,CACpB,CAAC;QACJ,CAAC;IACH,CAAC;IAEO,iBAAiB,CACvB,OAAgB,EAChB,aAAqB,EACrB,QAAgB;QAEhB,MAAM,EAAE,aAAa,EAAE,wBAAwB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QAMrC,IAAI,aAAa,EAAE,CAAC;YAClB,MAAM,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;QACrD,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,GAAG,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;QACpC,CAAC;QAED,IAAI,wBAAwB,EAAE,CAAC;YAC7B,MAAM,CAAC,GAAG,CAAC,0BAA0B,EAAE,MAAM,CAAC,wBAAwB,CAAC,CAAC,CAAC;QAC3E,CAAC;QAED,OAAO,GAAG,aAAa,IAAI,MAAM,EAAE,CAAC;IACtC,CAAC;CACF,CAAA;AAzEY,eAAe;IAD3B,UAAU,EAAE;IAKR,WAAA,MAAM,CAAC,2BAA2B,CAAC,CAAA;6CAEf,WAAW;QACR,cAAc;GAP7B,eAAe,CAyE3B"}
|
package/package.json
CHANGED
|
@@ -63,10 +63,36 @@ describe('PMLogoutService', () => {
|
|
|
63
63
|
expect(httpService.post).toHaveBeenCalledWith(
|
|
64
64
|
'https://keycloak/logout',
|
|
65
65
|
expect.any(URLSearchParams),
|
|
66
|
-
{
|
|
66
|
+
{
|
|
67
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
68
|
+
auth: { username: 'client-id', password: 'secret' },
|
|
69
|
+
},
|
|
67
70
|
);
|
|
68
71
|
});
|
|
69
72
|
|
|
73
|
+
it('should not send the client secret as a form parameter', async () => {
|
|
74
|
+
const mockRequest = {} as Request;
|
|
75
|
+
const mockResponse = {} as Response;
|
|
76
|
+
|
|
77
|
+
(authConfigService.getAuthConfig as jest.Mock).mockResolvedValue({
|
|
78
|
+
clientId: 'client-id',
|
|
79
|
+
clientSecret: 'secret',
|
|
80
|
+
endSessionUrl: 'https://keycloak/logout',
|
|
81
|
+
});
|
|
82
|
+
(cookiesService.getAuthCookie as jest.Mock).mockReturnValue(
|
|
83
|
+
'refresh-token',
|
|
84
|
+
);
|
|
85
|
+
(httpService.post as jest.Mock).mockReturnValue(of({ data: {} }));
|
|
86
|
+
|
|
87
|
+
await service.handleLogout(mockRequest, mockResponse);
|
|
88
|
+
|
|
89
|
+
const body = (httpService.post as jest.Mock).mock
|
|
90
|
+
.calls[0][1] as URLSearchParams;
|
|
91
|
+
expect(body.get('refresh_token')).toBe('refresh-token');
|
|
92
|
+
expect(body.get('client_secret')).toBeNull();
|
|
93
|
+
expect(body.get('client_id')).toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
70
96
|
it('should return logoutWithIdToken URL when logout fails', async () => {
|
|
71
97
|
const mockRequest = {
|
|
72
98
|
query: {
|
|
@@ -105,9 +131,39 @@ describe('PMLogoutService', () => {
|
|
|
105
131
|
const result = (service as any).logoutWithIdToken(
|
|
106
132
|
request,
|
|
107
133
|
'https://kc/logout',
|
|
134
|
+
'client-id',
|
|
108
135
|
);
|
|
109
136
|
expect(result).toBe(
|
|
110
137
|
'https://kc/logout?id_token_hint=token123&post_logout_redirect_uri=https%3A%2F%2Fexample.com',
|
|
111
138
|
);
|
|
112
139
|
});
|
|
140
|
+
|
|
141
|
+
it('logoutWithIdToken should fall back to client_id when no id token is present', () => {
|
|
142
|
+
const request = {
|
|
143
|
+
query: { post_logout_redirect_uri: 'https://example.com' },
|
|
144
|
+
} as unknown as Request;
|
|
145
|
+
|
|
146
|
+
const result = (service as any).logoutWithIdToken(
|
|
147
|
+
request,
|
|
148
|
+
'https://kc/logout',
|
|
149
|
+
'client-id',
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(result).toBe(
|
|
153
|
+
'https://kc/logout?client_id=client-id&post_logout_redirect_uri=https%3A%2F%2Fexample.com',
|
|
154
|
+
);
|
|
155
|
+
expect(result).not.toContain('id_token_hint');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('logoutWithIdToken should omit an absent post logout redirect uri', () => {
|
|
159
|
+
const request = { query: {} } as unknown as Request;
|
|
160
|
+
|
|
161
|
+
const result = (service as any).logoutWithIdToken(
|
|
162
|
+
request,
|
|
163
|
+
'https://kc/logout',
|
|
164
|
+
'client-id',
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
expect(result).toBe('https://kc/logout?client_id=client-id');
|
|
168
|
+
});
|
|
113
169
|
});
|
|
@@ -29,14 +29,20 @@ export class PMLogoutService implements LogoutCallback {
|
|
|
29
29
|
const refreshToken = this.cookiesService.getAuthCookie(request);
|
|
30
30
|
|
|
31
31
|
const body = new URLSearchParams({
|
|
32
|
-
client_id: authConfig.clientId,
|
|
33
|
-
client_secret: authConfig.clientSecret,
|
|
34
32
|
refresh_token: refreshToken,
|
|
35
33
|
});
|
|
36
34
|
|
|
35
|
+
// Client credentials go in the Authorization header, not the form body:
|
|
36
|
+
// the client is registered with token_endpoint_auth_method
|
|
37
|
+
// client_secret_basic, and an IdP that enforces it rejects credentials
|
|
38
|
+
// sent as form parameters with unauthorized_client.
|
|
37
39
|
await firstValueFrom(
|
|
38
40
|
this.httpService.post(authConfig.endSessionUrl, body, {
|
|
39
41
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
42
|
+
auth: {
|
|
43
|
+
username: authConfig.clientId,
|
|
44
|
+
password: authConfig.clientSecret,
|
|
45
|
+
},
|
|
40
46
|
}),
|
|
41
47
|
);
|
|
42
48
|
} catch (error: any) {
|
|
@@ -45,16 +51,36 @@ export class PMLogoutService implements LogoutCallback {
|
|
|
45
51
|
error?.response?.data || error.message,
|
|
46
52
|
);
|
|
47
53
|
this.logger.warn('Trying to log out with the id token');
|
|
48
|
-
return this.logoutWithIdToken(
|
|
54
|
+
return this.logoutWithIdToken(
|
|
55
|
+
request,
|
|
56
|
+
authConfig.endSessionUrl,
|
|
57
|
+
authConfig.clientId,
|
|
58
|
+
);
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
52
|
-
private logoutWithIdToken(
|
|
62
|
+
private logoutWithIdToken(
|
|
63
|
+
request: Request,
|
|
64
|
+
endSessionUrl: string,
|
|
65
|
+
clientId: string,
|
|
66
|
+
) {
|
|
53
67
|
const { id_token_hint, post_logout_redirect_uri } = request.query;
|
|
54
|
-
const params = new URLSearchParams(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
const params = new URLSearchParams();
|
|
69
|
+
|
|
70
|
+
// RP-initiated logout honours post_logout_redirect_uri only together with
|
|
71
|
+
// id_token_hint or client_id. An empty id_token_hint is not a no-op: the
|
|
72
|
+
// IdP rejects the request ("Missing parameters: id_token_hint") and the
|
|
73
|
+
// user is stranded on an error page instead of being signed out.
|
|
74
|
+
if (id_token_hint) {
|
|
75
|
+
params.set('id_token_hint', String(id_token_hint));
|
|
76
|
+
} else {
|
|
77
|
+
params.set('client_id', clientId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (post_logout_redirect_uri) {
|
|
81
|
+
params.set('post_logout_redirect_uri', String(post_logout_redirect_uri));
|
|
82
|
+
}
|
|
83
|
+
|
|
58
84
|
return `${endSessionUrl}?${params}`;
|
|
59
85
|
}
|
|
60
86
|
}
|