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 +7 -1
- package/docs/api/configuration.md +80 -7
- package/docs/api-reference.md +3 -1
- package/docs/architecture.md +2 -1
- package/index.js +52 -3
- package/package.json +1 -1
- package/test/specs/clientCredentials.test.js +66 -0
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
|
-
`
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
- `
|
|
326
|
-
- `
|
|
327
|
-
- `auth()`
|
|
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.
|
package/docs/api-reference.md
CHANGED
|
@@ -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
|
-
| `
|
|
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 |
|
package/docs/architecture.md
CHANGED
|
@@ -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. `
|
|
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
|
-
|
|
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
|
+
"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
|
});
|