@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
- id_token_hint: String(id_token_hint || ''),
51
- post_logout_redirect_uri: String(post_logout_redirect_uri || ''),
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,SAAS,EAAE,UAAU,CAAC,QAAQ;gBAC9B,aAAa,EAAE,UAAU,CAAC,YAAY;gBACtC,aAAa,EAAE,YAAY;aAC5B,CAAC,CAAC;YAEH,MAAM,cAAc,CAClB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,EAAE,IAAI,EAAE;gBACpD,OAAO,EAAE,EAAE,cAAc,EAAE,mCAAmC,EAAE;aACjE,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,CAAC,OAAO,EAAE,UAAU,CAAC,aAAa,CAAC,CAAC;QACnE,CAAC;IACH,CAAC;IAEO,iBAAiB,CAAC,OAAgB,EAAE,aAAqB;QAC/D,MAAM,EAAE,aAAa,EAAE,wBAAwB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC;QAClE,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC;YACjC,aAAa,EAAE,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC;YAC1C,wBAAwB,EAAE,MAAM,CAAC,wBAAwB,IAAI,EAAE,CAAC;SACjE,CAAC,CAAC;QACH,OAAO,GAAG,aAAa,IAAI,MAAM,EAAE,CAAC;IACtC,CAAC;CACF,CAAA;AA/CY,eAAe;IAD3B,UAAU,EAAE;IAKR,WAAA,MAAM,CAAC,2BAA2B,CAAC,CAAA;6CAEf,WAAW;QACR,cAAc;GAP7B,eAAe,CA+C3B"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platform-mesh/portal-server-lib",
3
- "version": "0.9.94",
3
+ "version": "0.9.96",
4
4
  "author": "Platform Mesh",
5
5
  "license": "Apache-2.0",
6
6
  "publishConfig": {
@@ -63,10 +63,36 @@ describe('PMLogoutService', () => {
63
63
  expect(httpService.post).toHaveBeenCalledWith(
64
64
  'https://keycloak/logout',
65
65
  expect.any(URLSearchParams),
66
- { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } },
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(request, authConfig.endSessionUrl);
54
+ return this.logoutWithIdToken(
55
+ request,
56
+ authConfig.endSessionUrl,
57
+ authConfig.clientId,
58
+ );
49
59
  }
50
60
  }
51
61
 
52
- private logoutWithIdToken(request: Request, endSessionUrl: string) {
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
- id_token_hint: String(id_token_hint || ''),
56
- post_logout_redirect_uri: String(post_logout_redirect_uri || ''),
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
  }