keycloak-api-manager 5.0.3 → 5.0.5

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.
package/README.md CHANGED
@@ -65,10 +65,16 @@ In Keycloak 26.x, management-permissions APIs used by group/user fine-grained te
65
65
  - `configure(credentials)`
66
66
  - `setConfig(overrides)`
67
67
  - `getToken()`
68
+ - `login(credentials)`
69
+ - `loginPKCE(credentials)`
68
70
  - `auth(credentials)`
69
71
  - `stop()`
70
72
 
71
- `auth(credentials)` is an OIDC token endpoint helper for login/token grant flows (user/client). It does not replace the internal admin session configured by `configure()`.
73
+ `login(credentials)` is the preferred OIDC token endpoint helper for login/token grant flows (user/client).
74
+
75
+ `loginPKCE(credentials)` is a specialized helper for Authorization Code + PKCE token exchange.
76
+
77
+ `auth(credentials)` is kept as backward-compatible alias and does not replace the internal admin session configured by `configure()`.
72
78
 
73
79
  Configured handler namespaces:
74
80
 
@@ -7,6 +7,8 @@ Core API methods for initializing and managing the Keycloak Admin Client connect
7
7
  - [configure()](#configure)
8
8
  - [setConfig()](#setconfig)
9
9
  - [getToken()](#gettoken)
10
+ - [login()](#login)
11
+ - [loginPKCE()](#loginpkce)
10
12
  - [auth()](#auth)
11
13
  - [stop()](#stop)
12
14
 
@@ -236,6 +238,23 @@ const response = await axios.get('https://keycloak.example.com/admin/realms/mast
236
238
 
237
239
  ## auth()
238
240
 
241
+ Backward-compatible alias of `login()`.
242
+
243
+ Use `login()` in new code for clearer intent.
244
+
245
+ **Syntax:**
246
+ ```javascript
247
+ await KeycloakManager.auth(credentials)
248
+ ```
249
+
250
+ ### Returns
251
+
252
+ **Promise\<Object\>** - Same response as `login()`
253
+
254
+ ---
255
+
256
+ ## login()
257
+
239
258
  Request tokens from Keycloak via the OIDC token endpoint.
240
259
 
241
260
  This method is intended for application-level login/token flows (for users, service clients, or third-party integrations) using this package as a wrapper.
@@ -244,7 +263,7 @@ It does **not** reconfigure or replace the internal admin session created by `co
244
263
 
245
264
  **Syntax:**
246
265
  ```javascript
247
- await KeycloakManager.auth(credentials)
266
+ await KeycloakManager.login(credentials)
248
267
  ```
249
268
 
250
269
  ### Parameters
@@ -287,7 +306,7 @@ await KeycloakManager.configure({
287
306
  });
288
307
 
289
308
  // Application login/token request for an end-user
290
- const tokenResponse = await KeycloakManager.auth({
309
+ const tokenResponse = await KeycloakManager.login({
291
310
  grant_type: 'password',
292
311
  username: 'end-user',
293
312
  password: 'user-password',
@@ -300,7 +319,7 @@ console.log(tokenResponse.access_token);
300
319
  #### Client Credentials Login
301
320
 
302
321
  ```javascript
303
- const tokenResponse = await KeycloakManager.auth({
322
+ const tokenResponse = await KeycloakManager.login({
304
323
  grant_type: 'client_credentials',
305
324
  client_id: 'my-public-api',
306
325
  client_secret: process.env.API_CLIENT_SECRET
@@ -312,7 +331,7 @@ console.log(tokenResponse.access_token);
312
331
  #### Refresh Token Flow
313
332
 
314
333
  ```javascript
315
- const refreshed = await KeycloakManager.auth({
334
+ const refreshed = await KeycloakManager.login({
316
335
  grant_type: 'refresh_token',
317
336
  refresh_token: oldRefreshToken
318
337
  });
@@ -322,13 +341,67 @@ console.log(refreshed.access_token);
322
341
 
323
342
  ### Notes
324
343
 
325
- - `auth()` posts to `${baseUrl}/realms/${realmName}/protocol/openid-connect/token`.
326
- - `auth()` returns raw token endpoint payload and throws on non-2xx responses.
327
- - `auth()` does not change handler wiring, runtime config, or the internal admin refresh timer.
344
+ - `login()` posts to `${baseUrl}/realms/${realmName}/protocol/openid-connect/token`.
345
+ - `login()` returns raw token endpoint payload and throws on non-2xx responses.
346
+ - `login()`/`auth()` do not change handler wiring, runtime config, or the internal admin refresh timer.
328
347
  - Runtime `clientId`/`clientSecret` are appended automatically if configured and not overridden in request payload.
329
348
 
330
349
  ---
331
350
 
351
+ ## loginPKCE()
352
+
353
+ Perform Authorization Code + PKCE token exchange.
354
+
355
+ This helper is intended for the callback step after user login on Keycloak, where your backend receives an authorization `code` and exchanges it with `code_verifier`.
356
+
357
+ **Syntax:**
358
+ ```javascript
359
+ await KeycloakManager.loginPKCE(credentials)
360
+ ```
361
+
362
+ ### Parameters
363
+
364
+ #### credentials (Object) ⚠️ Required
365
+
366
+ | Property | Type | Required | Description |
367
+ |----------|------|----------|-------------|
368
+ | `code` | string | ⚠️ Yes | Authorization code returned by Keycloak |
369
+ | `redirect_uri` | string | ⚠️ Yes* | Redirect URI used in authorize request |
370
+ | `redirectUri` | string | ⚠️ Yes* | CamelCase alias of `redirect_uri` |
371
+ | `code_verifier` | string | ⚠️ Yes* | PKCE code verifier |
372
+ | `codeVerifier` | string | ⚠️ Yes* | CamelCase alias of `code_verifier` |
373
+ | `client_id` | string | 📋 Optional | Overrides runtime `clientId` |
374
+ | `clientId` | string | 📋 Optional | CamelCase alias of `client_id` |
375
+ | `client_secret` | string | 📋 Optional | Overrides runtime `clientSecret` |
376
+ | `clientSecret` | string | 📋 Optional | CamelCase alias of `client_secret` |
377
+ | `scope` | string | 📋 Optional | Additional scope string |
378
+
379
+ `*` required with either snake_case or camelCase form.
380
+
381
+ ### Returns
382
+
383
+ **Promise\<Object\>** - Token payload returned by Keycloak (`access_token`, `refresh_token`, `id_token`, `expires_in`, ...)
384
+
385
+ ### Example
386
+
387
+ ```javascript
388
+ const tokenResponse = await KeycloakManager.loginPKCE({
389
+ code: authorizationCode,
390
+ redirectUri: 'https://my-app.example.com/auth/callback',
391
+ codeVerifier: pkceCodeVerifier
392
+ });
393
+
394
+ console.log(tokenResponse.access_token);
395
+ ```
396
+
397
+ ### Notes
398
+
399
+ - `loginPKCE()` forces `grant_type=authorization_code`.
400
+ - If `client_id/client_secret` are omitted, runtime values from `configure()` are used.
401
+ - `loginPKCE()` does not generate authorize URLs, `state`, or PKCE challenge; it only performs token exchange.
402
+
403
+ ---
404
+
332
405
  ## stop()
333
406
 
334
407
  Stop the automatic token refresh timer and cleanup resources. Call this when shutting down your application.
@@ -76,7 +76,9 @@ KeycloakManager.stop();
76
76
  | `configure()` | Authentication and setup | Core |
77
77
  | `setConfig()` | Runtime configuration | Core |
78
78
  | `getToken()` | Get current access token | Core |
79
- | `auth()` | OIDC token grant/login endpoint wrapper | Core |
79
+ | `login()` | Preferred OIDC token grant/login endpoint wrapper | Core |
80
+ | `loginPKCE()` | Authorization Code + PKCE token exchange helper | Core |
81
+ | `auth()` | Backward-compatible alias of `login()` | Core |
80
82
  | `stop()` | Stop token refresh timer | Core |
81
83
  | `realms` | Realm management | realmsHandler |
82
84
  | `users` | User management | usersHandler |
@@ -14,10 +14,11 @@ This package exposes a single runtime (`index.js`) that initializes one Keycloak
14
14
  - Updates realm/baseUrl/request options at runtime.
15
15
  - Keeps active session/token management in place.
16
16
 
17
- 3. `auth(credentials)`
17
+ 3. `login(credentials)`
18
18
  - Direct token endpoint call for explicit OIDC login/token flows.
19
19
  - Intended for application-level third-party authentication use-cases.
20
20
  - Does not mutate the internal admin client session established by `configure()`.
21
+ - `auth(credentials)` remains available as backward-compatible alias.
21
22
 
22
23
  4. `stop()`
23
24
  - Stops refresh timer for clean process termination.
package/index.js CHANGED
@@ -138,7 +138,7 @@ exports.stop = function stop() {
138
138
  clearRefreshTimer();
139
139
  };
140
140
 
141
- exports.auth = async function auth(credentials = {}) {
141
+ async function requestOidcToken(credentials = {}) {
142
142
  assertConfigured();
143
143
 
144
144
  const body = new URLSearchParams();
@@ -148,10 +148,10 @@ exports.auth = async function auth(credentials = {}) {
148
148
  }
149
149
  });
150
150
 
151
- if (runtimeConfig.clientId) {
151
+ if (runtimeConfig.clientId && !body.has('client_id')) {
152
152
  body.append('client_id', runtimeConfig.clientId);
153
153
  }
154
- if (runtimeConfig.clientSecret) {
154
+ if (runtimeConfig.clientSecret && !body.has('client_secret')) {
155
155
  body.append('client_secret', runtimeConfig.clientSecret);
156
156
  }
157
157
 
@@ -175,4 +175,53 @@ exports.auth = async function auth(credentials = {}) {
175
175
  }
176
176
 
177
177
  return payload;
178
+ }
179
+
180
+ exports.auth = async function auth(credentials = {}) {
181
+ return requestOidcToken(credentials);
182
+ };
183
+
184
+ exports.login = async function login(credentials = {}) {
185
+ return requestOidcToken(credentials);
186
+ };
187
+
188
+ exports.loginPKCE = async function loginPKCE(credentials = {}) {
189
+ const {
190
+ code,
191
+ redirect_uri,
192
+ redirectUri,
193
+ code_verifier,
194
+ codeVerifier,
195
+ client_id,
196
+ clientId,
197
+ client_secret,
198
+ clientSecret,
199
+ ...rest
200
+ } = credentials;
201
+
202
+ const resolvedCode = code;
203
+ const resolvedRedirectUri = redirect_uri || redirectUri;
204
+ const resolvedCodeVerifier = code_verifier || codeVerifier;
205
+ const resolvedClientId = client_id || clientId;
206
+ const resolvedClientSecret = client_secret || clientSecret;
207
+
208
+ if (!resolvedCode) {
209
+ throw new Error('loginPKCE requires "code".');
210
+ }
211
+ if (!resolvedRedirectUri) {
212
+ throw new Error('loginPKCE requires "redirect_uri" (or "redirectUri").');
213
+ }
214
+ if (!resolvedCodeVerifier) {
215
+ throw new Error('loginPKCE requires "code_verifier" (or "codeVerifier").');
216
+ }
217
+
218
+ return requestOidcToken({
219
+ grant_type: 'authorization_code',
220
+ code: resolvedCode,
221
+ redirect_uri: resolvedRedirectUri,
222
+ code_verifier: resolvedCodeVerifier,
223
+ ...(resolvedClientId ? { client_id: resolvedClientId } : {}),
224
+ ...(resolvedClientSecret ? { client_secret: resolvedClientSecret } : {}),
225
+ ...rest
226
+ });
178
227
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keycloak-api-manager",
3
- "version": "5.0.3",
3
+ "version": "5.0.5",
4
4
  "description": "Enhanced Node.js wrapper for Keycloak Admin REST API. Professional alternative to @keycloak/keycloak-admin-client with advanced features, bug fixes, automatic token refresh, Organizations API support, fine-grained permissions, and comprehensive resource management. Battle-tested with 113+ integration tests.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -76,4 +76,70 @@ describe('Authentication - client_credentials grant', function () {
76
76
  expect(token).to.have.property('accessToken');
77
77
  expect(token.accessToken).to.be.a('string').and.to.have.length.greaterThan(0);
78
78
  });
79
+
80
+ it('supports login() for direct token grant', async function () {
81
+ if (!testClientId || !testClientSecret) {
82
+ this.skip();
83
+ return;
84
+ }
85
+
86
+ keycloakManager.setConfig({ realmName: TEST_REALM });
87
+
88
+ const tokenPayload = await keycloakManager.login({
89
+ grant_type: 'client_credentials',
90
+ client_id: testClientId,
91
+ client_secret: testClientSecret,
92
+ });
93
+
94
+ expect(tokenPayload).to.have.property('access_token');
95
+ expect(tokenPayload.access_token).to.be.a('string').and.to.have.length.greaterThan(0);
96
+ expect(tokenPayload).to.have.property('token_type');
97
+ });
98
+
99
+ it('validates loginPKCE required parameters', async function () {
100
+ try {
101
+ await keycloakManager.loginPKCE({});
102
+ throw new Error('Expected loginPKCE to fail when code is missing');
103
+ } catch (error) {
104
+ expect(error.message).to.include('requires "code"');
105
+ }
106
+
107
+ try {
108
+ await keycloakManager.loginPKCE({ code: 'dummy-code' });
109
+ throw new Error('Expected loginPKCE to fail when redirect_uri is missing');
110
+ } catch (error) {
111
+ expect(error.message).to.include('requires "redirect_uri"');
112
+ }
113
+
114
+ try {
115
+ await keycloakManager.loginPKCE({
116
+ code: 'dummy-code',
117
+ redirect_uri: 'https://example.local/callback',
118
+ });
119
+ throw new Error('Expected loginPKCE to fail when code_verifier is missing');
120
+ } catch (error) {
121
+ expect(error.message).to.include('requires "code_verifier"');
122
+ }
123
+ });
124
+
125
+ it('attempts PKCE token exchange and returns OIDC error for invalid code', async function () {
126
+ if (!testClientId || !testClientSecret) {
127
+ this.skip();
128
+ return;
129
+ }
130
+
131
+ try {
132
+ await keycloakManager.loginPKCE({
133
+ code: 'invalid-authorization-code',
134
+ redirect_uri: 'https://example.local/callback',
135
+ code_verifier: 'plain-verifier-for-test-only',
136
+ client_id: testClientId,
137
+ client_secret: testClientSecret,
138
+ });
139
+ throw new Error('Expected loginPKCE to fail with invalid authorization code');
140
+ } catch (error) {
141
+ expect(error).to.be.instanceOf(Error);
142
+ expect(error.message).to.be.a('string').and.to.have.length.greaterThan(0);
143
+ }
144
+ });
79
145
  });