keycloak-api-manager 5.0.6 → 5.0.7

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
@@ -97,6 +97,12 @@ Configured handler namespaces:
97
97
 
98
98
  All documentation is centralized under `docs/`.
99
99
 
100
+ ### Guides (Practical Implementation)
101
+
102
+ - [PKCE Login Flow Guide](docs/guides/PKCE-Login-Flow.md) - Step-by-step guide for implementing OAuth2 Authorization Code + PKCE authentication
103
+
104
+ ### API Reference
105
+
100
106
  - [API Reference (Index)](docs/api-reference.md)
101
107
  - [API - Configuration](docs/api/configuration.md)
102
108
  - [API - Realms](docs/api/realms.md)
@@ -113,6 +119,9 @@ All documentation is centralized under `docs/`.
113
119
  - [API - User Profile](docs/api/user-profile.md)
114
120
  - [API - Client Policies](docs/api/client-policies.md)
115
121
  - [API - Server Info](docs/api/server-info.md)
122
+
123
+ ### General Documentation
124
+
116
125
  - [Architecture and Runtime](docs/architecture.md)
117
126
  - [Keycloak Setup and Feature Flags](docs/keycloak-setup.md)
118
127
  - [Testing Guide](docs/testing.md)
@@ -354,6 +354,8 @@ Perform Authorization Code + PKCE token exchange.
354
354
 
355
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
356
 
357
+ > **📖 For a complete step-by-step guide on implementing PKCE flow in your application, see [PKCE Login Flow Guide](../guides/PKCE-Login-Flow.md)**
358
+
357
359
  **Syntax:**
358
360
  ```javascript
359
361
  await KeycloakManager.loginPKCE(credentials)
@@ -4,6 +4,9 @@ Complete API documentation for keycloak-api-manager.
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ ### Guides (Practical Implementation)
8
+ - [PKCE Login Flow Guide](guides/PKCE-Login-Flow.md) - Step-by-step OAuth2 Authorization Code + PKCE implementation guide
9
+
7
10
  ### Core API
8
11
  - [Configuration & Authentication](api/configuration.md) - Setup, authentication, and lifecycle management
9
12
 
@@ -0,0 +1,610 @@
1
+ # PKCE Login Flow Guide
2
+
3
+ This guide walks you through implementing OAuth2 Authorization Code + PKCE flow in your application using Keycloak and the keycloak-api-manager library.
4
+
5
+ ## Overview
6
+
7
+ PKCE (Proof Key for Code Exchange) is the modern, secure way for browser-based and mobile applications to authenticate users through Keycloak. Unlike the legacy resource owner password grant, PKCE:
8
+
9
+ - ✅ Never exposes user passwords to your backend
10
+ - ✅ Works seamlessly with Keycloak's authorization server
11
+ - ✅ Provides built-in CSRF protection via state parameter
12
+ - ✅ Protects against authorization code interception attacks
13
+ - ✅ Is the OAuth2 standard for public clients
14
+
15
+ ## Flow Diagram
16
+
17
+ ```
18
+ ┌─────────────┐ ┌──────────────┐
19
+ │ Browser │ │ Keycloak │
20
+ │ (User) │ │ Server │
21
+ └──────┬──────┘ └──────────────┘
22
+ │ ▲
23
+ │ 1. Click "Login" │
24
+ ├─────────────────────────────────────────────────►
25
+ │ │
26
+ │ 2. Verify state, generate PKCE pair │
27
+ │ 3. Redirect to Keycloak /auth with │
28
+ │ code_challenge & state │
29
+ │ │
30
+ │◄─────────────────────────────────────────────────┤
31
+ │ Keycloak login page │
32
+ │ │
33
+ │ 4. User enters credentials │
34
+ ├─────────────────────────────────────────────────►
35
+ │ │
36
+ │ 5. Verify credentials │
37
+ │ 6. Redirect to /callback with │
38
+ │ code + state │
39
+ │ │
40
+ │◄─────────────────────────────────────────────────┤
41
+ │ code=abc123&state=xyz789 │
42
+ │ │
43
+ │ 7. Exchange code for token │
44
+ │ (with code_verifier) │
45
+ │──────────────────────────────────────────────────►
46
+ │ POST /auth/callback backend │
47
+ │ │
48
+ │ ┌────────────┐
49
+ │ │ Backend │
50
+ │ │ (Node.js) │
51
+ │ └────────────┘
52
+ │ ▲
53
+ │ │
54
+ │ │
55
+ │ 8. Call loginPKCE()
56
+ │ with code, verifier
57
+ │ │
58
+ │ Keycloak validates
59
+ │ code_challenge vs
60
+ │ code_verifier
61
+ │ │
62
+ │ 9. Return access token
63
+ │ (JWT)
64
+ │ │
65
+ │ 10. Set secure HttpOnly cookie │
66
+ │◄──────────────────────────────────────────┤
67
+ │ (with access_token + refresh_token) │
68
+ │ │
69
+ │ Browser is now authenticated! │
70
+ │ │
71
+ ```
72
+
73
+ ## Step-by-Step Implementation
74
+
75
+ ### Step 1: Generate PKCE Pair
76
+
77
+ When the user clicks "Login", your backend generates a PKCE pair and stores it securely:
78
+
79
+ ```javascript
80
+ const crypto = require('crypto');
81
+
82
+ function base64url(buf) {
83
+ return buf
84
+ .toString('base64')
85
+ .replace(/\+/g, '-')
86
+ .replace(/\//g, '_')
87
+ .replace(/=/g, '');
88
+ }
89
+
90
+ function createPkcePair() {
91
+ // Generate code_verifier (random 128-char string)
92
+ const code_verifier = base64url(crypto.randomBytes(96));
93
+
94
+ // Generate code_challenge (SHA256 hash of verifier)
95
+ const code_challenge = base64url(
96
+ crypto.createHash('sha256').update(code_verifier).digest()
97
+ );
98
+
99
+ // Generate state (CSRF protection)
100
+ const state = base64url(crypto.randomBytes(32));
101
+
102
+ return { code_verifier, code_challenge, state };
103
+ }
104
+ ```
105
+
106
+ **Why this works:**
107
+ - `code_verifier`: A random, unguessable string
108
+ - `code_challenge`: The SHA256 hash of the verifier, sent to Keycloak during login
109
+ - `state`: A random token to prevent CSRF attacks; Keycloak will return it unchanged
110
+
111
+ ### Step 2: Redirect to Keycloak Authorization Endpoint
112
+
113
+ Store the PKCE pair in the session, then redirect the user to Keycloak:
114
+
115
+ ```javascript
116
+ const express = require('express');
117
+ const session = require('express-session');
118
+ const KeycloakManager = require('keycloak-api-manager');
119
+
120
+ const app = express();
121
+
122
+ // Session configuration (store verifier securely server-side)
123
+ app.use(session({
124
+ secret: 'your-secret-key',
125
+ resave: false,
126
+ saveUninitialized: true,
127
+ cookie: { httpOnly: true, secure: true } // secure: true in production (HTTPS only)
128
+ }));
129
+
130
+ // 1. User clicks "Login" button
131
+ app.get('/auth/login', (req, res) => {
132
+ const { code_verifier, code_challenge, state } = createPkcePair();
133
+
134
+ // Store verifier and state in session (server-side only!)
135
+ req.session.pkce_verifier = code_verifier;
136
+ req.session.pkce_state = state;
137
+
138
+ // Build Keycloak authorization URL
139
+ const keycloakAuthUrl = `${process.env.KEYCLOAK_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`;
140
+ const authUrl = new URL(keycloakAuthUrl);
141
+
142
+ authUrl.searchParams.append('client_id', process.env.KEYCLOAK_CLIENT_ID);
143
+ authUrl.searchParams.append('redirect_uri', `${process.env.APP_URL}/auth/callback`);
144
+ authUrl.searchParams.append('response_type', 'code');
145
+ authUrl.searchParams.append('scope', 'openid profile email');
146
+ authUrl.searchParams.append('code_challenge', code_challenge);
147
+ authUrl.searchParams.append('code_challenge_method', 'S256'); // S256 = SHA256
148
+ authUrl.searchParams.append('state', state);
149
+
150
+ // Redirect user to Keycloak login page
151
+ res.redirect(authUrl.toString());
152
+ });
153
+ ```
154
+
155
+ **What happens:**
156
+ - User is redirected to Keycloak
157
+ - They see the login page
158
+ - They enter their username/password
159
+ - On successful auth, Keycloak redirects back to your `/auth/callback` endpoint with `code` and `state`
160
+
161
+ ### Step 3: Exchange Authorization Code for Token
162
+
163
+ When Keycloak redirects back with the authorization code, you exchange it for an access token:
164
+
165
+ ```javascript
166
+ // 2. Keycloak redirects back with authorization code
167
+ app.get('/auth/callback', async (req, res) => {
168
+ try {
169
+ const { code, state, error } = req.query;
170
+
171
+ // 1. Validate state parameter (CSRF protection)
172
+ if (state !== req.session.pkce_state) {
173
+ return res.status(400).send('Invalid state parameter - CSRF attack detected');
174
+ }
175
+
176
+ // 2. Check for authorization errors
177
+ if (error) {
178
+ return res.status(400).send(`Authorization failed: ${error}`);
179
+ }
180
+
181
+ // 3. Retrieve stored verifier from session
182
+ const code_verifier = req.session.pkce_verifier;
183
+
184
+ if (!code_verifier) {
185
+ return res.status(400).send('PKCE verifier not found in session');
186
+ }
187
+
188
+ // 4. Exchange code for token using loginPKCE()
189
+ const tokenResponse = await KeycloakManager.loginPKCE({
190
+ code,
191
+ redirect_uri: `${process.env.APP_URL}/auth/callback`,
192
+ code_verifier,
193
+ client_id: process.env.KEYCLOAK_CLIENT_ID,
194
+ client_secret: process.env.KEYCLOAK_CLIENT_SECRET
195
+ });
196
+
197
+ // 5. Set secure HTTPOnly cookie with access token
198
+ res.cookie('access_token', tokenResponse.access_token, {
199
+ httpOnly: true, // Prevent XSS access
200
+ secure: true, // HTTPS only
201
+ sameSite: 'strict', // CSRF protection
202
+ maxAge: tokenResponse.expires_in * 1000
203
+ });
204
+
205
+ // Also store refresh token separately
206
+ res.cookie('refresh_token', tokenResponse.refresh_token, {
207
+ httpOnly: true,
208
+ secure: true,
209
+ sameSite: 'strict',
210
+ maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
211
+ });
212
+
213
+ // 6. Clear sensitive session data
214
+ delete req.session.pkce_verifier;
215
+ delete req.session.pkce_state;
216
+
217
+ // Redirect to application home
218
+ res.redirect('/dashboard');
219
+
220
+ } catch (error) {
221
+ console.error('Token exchange failed:', error);
222
+ res.status(500).send('Authentication failed');
223
+ }
224
+ });
225
+ ```
226
+
227
+ **Security checks in this step:**
228
+ - ✅ Validate `state` matches what we stored (CSRF protection)
229
+ - ✅ Check for authorization errors
230
+ - ✅ Verify `code_verifier` exists in session
231
+ - ✅ Exchange code with verifier (proves we initiated the flow)
232
+ - ✅ Store token in HttpOnly cookie (prevents XSS theft)
233
+ - ✅ Clear sensitive session data
234
+
235
+ ### Step 4: Use the Access Token
236
+
237
+ Now the user has an access token in a secure cookie. Use it to access protected resources:
238
+
239
+ ```javascript
240
+ // Middleware to verify access token
241
+ app.use((req, res, next) => {
242
+ const token = req.cookies.access_token;
243
+
244
+ if (!token) {
245
+ return res.status(401).send('Not authenticated');
246
+ }
247
+
248
+ // Verify and decode the token
249
+ try {
250
+ const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY);
251
+ req.user = decoded; // User data available in request
252
+ next();
253
+ } catch (error) {
254
+ return res.status(401).send('Invalid token');
255
+ }
256
+ });
257
+
258
+ // Protected route
259
+ app.get('/dashboard', (req, res) => {
260
+ res.send(`Welcome, ${req.user.preferred_username}!`);
261
+ });
262
+ ```
263
+
264
+ ## Complete Working Example
265
+
266
+ Here's a complete Express.js application with PKCE flow:
267
+
268
+ ```javascript
269
+ const express = require('express');
270
+ const session = require('express-session');
271
+ const crypto = require('crypto');
272
+ const jwt = require('jsonwebtoken');
273
+ const cookieParser = require('cookie-parser');
274
+ const KeycloakManager = require('keycloak-api-manager');
275
+
276
+ const app = express();
277
+
278
+ // Middleware
279
+ app.use(express.json());
280
+ app.use(cookieParser());
281
+ app.use(session({
282
+ secret: process.env.SESSION_SECRET || 'dev-secret',
283
+ resave: false,
284
+ saveUninitialized: true,
285
+ cookie: { httpOnly: true, secure: false } // Set secure: true in production
286
+ }));
287
+
288
+ // Initialize Keycloak Manager
289
+ KeycloakManager.configure({
290
+ realmName: process.env.KEYCLOAK_REALM,
291
+ clientId: process.env.KEYCLOAK_CLIENT_ID,
292
+ clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
293
+ keycloakUrl: process.env.KEYCLOAK_URL
294
+ });
295
+
296
+ // Helper functions
297
+ function base64url(buf) {
298
+ return buf
299
+ .toString('base64')
300
+ .replace(/\+/g, '-')
301
+ .replace(/\//g, '_')
302
+ .replace(/=/g, '');
303
+ }
304
+
305
+ function createPkcePair() {
306
+ const code_verifier = base64url(crypto.randomBytes(96));
307
+ const code_challenge = base64url(
308
+ crypto.createHash('sha256').update(code_verifier).digest()
309
+ );
310
+ const state = base64url(crypto.randomBytes(32));
311
+ return { code_verifier, code_challenge, state };
312
+ }
313
+
314
+ // Routes
315
+ app.get('/auth/login', (req, res) => {
316
+ const { code_verifier, code_challenge, state } = createPkcePair();
317
+
318
+ req.session.pkce_verifier = code_verifier;
319
+ req.session.pkce_state = state;
320
+
321
+ const keycloakAuthUrl = `${process.env.KEYCLOAK_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`;
322
+ const authUrl = new URL(keycloakAuthUrl);
323
+
324
+ authUrl.searchParams.append('client_id', process.env.KEYCLOAK_CLIENT_ID);
325
+ authUrl.searchParams.append('redirect_uri', `${process.env.APP_URL}/auth/callback`);
326
+ authUrl.searchParams.append('response_type', 'code');
327
+ authUrl.searchParams.append('scope', 'openid profile email');
328
+ authUrl.searchParams.append('code_challenge', code_challenge);
329
+ authUrl.searchParams.append('code_challenge_method', 'S256');
330
+ authUrl.searchParams.append('state', state);
331
+
332
+ res.redirect(authUrl.toString());
333
+ });
334
+
335
+ app.get('/auth/callback', async (req, res) => {
336
+ try {
337
+ const { code, state, error } = req.query;
338
+
339
+ if (state !== req.session.pkce_state) {
340
+ return res.status(400).send('Invalid state parameter');
341
+ }
342
+
343
+ if (error) {
344
+ return res.status(400).send(`Authorization failed: ${error}`);
345
+ }
346
+
347
+ const code_verifier = req.session.pkce_verifier;
348
+ if (!code_verifier) {
349
+ return res.status(400).send('PKCE verifier not found');
350
+ }
351
+
352
+ // Exchange code for token
353
+ const tokenResponse = await KeycloakManager.loginPKCE({
354
+ code,
355
+ redirect_uri: `${process.env.APP_URL}/auth/callback`,
356
+ code_verifier,
357
+ client_id: process.env.KEYCLOAK_CLIENT_ID,
358
+ client_secret: process.env.KEYCLOAK_CLIENT_SECRET
359
+ });
360
+
361
+ // Set secure cookies
362
+ res.cookie('access_token', tokenResponse.access_token, {
363
+ httpOnly: true,
364
+ secure: true,
365
+ sameSite: 'strict',
366
+ maxAge: tokenResponse.expires_in * 1000
367
+ });
368
+
369
+ res.cookie('refresh_token', tokenResponse.refresh_token, {
370
+ httpOnly: true,
371
+ secure: true,
372
+ sameSite: 'strict',
373
+ maxAge: 7 * 24 * 60 * 60 * 1000
374
+ });
375
+
376
+ delete req.session.pkce_verifier;
377
+ delete req.session.pkce_state;
378
+
379
+ res.redirect('/dashboard');
380
+
381
+ } catch (error) {
382
+ console.error('Token exchange failed:', error);
383
+ res.status(500).send('Authentication failed');
384
+ }
385
+ });
386
+
387
+ app.get('/dashboard', (req, res) => {
388
+ const token = req.cookies.access_token;
389
+ if (!token) {
390
+ return res.redirect('/auth/login');
391
+ }
392
+
393
+ try {
394
+ const decoded = jwt.decode(token); // Or verify with public key in production
395
+ res.send(`Welcome, ${decoded.preferred_username}!`);
396
+ } catch (error) {
397
+ res.redirect('/auth/login');
398
+ }
399
+ });
400
+
401
+ app.get('/logout', (req, res) => {
402
+ res.clearCookie('access_token');
403
+ res.clearCookie('refresh_token');
404
+ req.session.destroy();
405
+ res.redirect('/');
406
+ });
407
+
408
+ // Start server
409
+ app.listen(3000, () => {
410
+ console.log('Server running on http://localhost:3000');
411
+ console.log('Login at http://localhost:3000/auth/login');
412
+ });
413
+ ```
414
+
415
+ ## Environment Variables
416
+
417
+ ```bash
418
+ KEYCLOAK_URL=http://localhost:8080
419
+ KEYCLOAK_REALM=master
420
+ KEYCLOAK_CLIENT_ID=my-app
421
+ KEYCLOAK_CLIENT_SECRET=your-client-secret
422
+ APP_URL=http://localhost:3000
423
+ SESSION_SECRET=your-session-secret
424
+ ```
425
+
426
+ ## Security Best Practices
427
+
428
+ ### ✅ DO
429
+
430
+ - ✅ Store PKCE verifier in **secure server-side session**, never in cookies
431
+ - ✅ Validate state parameter every time (CSRF protection)
432
+ - ✅ Use **HttpOnly cookies** for tokens (prevents XSS theft)
433
+ - ✅ Use **Secure flag** on cookies (HTTPS only in production)
434
+ - ✅ Use **SameSite=strict** on cookies (CSRF protection)
435
+ - ✅ Clear sensitive data from session after token exchange
436
+ - ✅ Use SHA256 for code_challenge (`S256` method)
437
+ - ✅ Generate cryptographically secure random values (use `crypto` module)
438
+
439
+ ### ❌ DON'T
440
+
441
+ - ❌ Store code_verifier in browser (localStorage, sessionStorage, etc.)
442
+ - ❌ Skip state parameter validation
443
+ - ❌ Use weak random generators
444
+ - ❌ Expose tokens in URL parameters
445
+ - ❌ Store tokens in browser storage (vulnerable to XSS)
446
+ - ❌ Use plain HTTP (always HTTPS in production)
447
+ - ❌ Reuse PKCE pairs or state values
448
+
449
+ ## Common Issues & Troubleshooting
450
+
451
+ ### "Invalid state parameter - CSRF attack detected"
452
+
453
+ **Cause:** Browser tabs have separate sessions, or cookies are not being persisted.
454
+
455
+ **Solution:**
456
+ ```javascript
457
+ // Ensure session cookies are sent with redirects
458
+ app.use(session({
459
+ cookie: {
460
+ httpOnly: true,
461
+ secure: true,
462
+ sameSite: 'lax' // Allow cross-site for redirects
463
+ }
464
+ }));
465
+ ```
466
+
467
+ ### "PKCE verifier not found in session"
468
+
469
+ **Cause:** Session was lost between `/auth/login` and `/auth/callback`.
470
+
471
+ **Debug:**
472
+ ```javascript
473
+ app.get('/auth/callback', (req, res) => {
474
+ console.log('Session ID:', req.sessionID);
475
+ console.log('Session data:', req.session);
476
+ // ... rest of callback
477
+ });
478
+ ```
479
+
480
+ ### "Invalid code_challenge"
481
+
482
+ **Cause:** The code_challenge calculated by Keycloak doesn't match our SHA256 hash.
483
+
484
+ **Verify:** The `createPkcePair()` function uses S256 (SHA256). Ensure code_challenge is:
485
+ 1. Lowercase base64url-encoded
486
+ 2. Derived from code_verifier with SHA256, not any other hash
487
+
488
+ ### Access token not being set
489
+
490
+ **Cause:** `loginPKCE()` threw an error that wasn't caught, or response format is different.
491
+
492
+ **Debug:**
493
+ ```javascript
494
+ const tokenResponse = await KeycloakManager.loginPKCE({...});
495
+ console.log('Token response:', JSON.stringify(tokenResponse, null, 2));
496
+ ```
497
+
498
+ Expected response:
499
+ ```json
500
+ {
501
+ "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
502
+ "expires_in": 300,
503
+ "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
504
+ "token_type": "Bearer",
505
+ "scope": "openid profile email"
506
+ }
507
+ ```
508
+
509
+ ## Production Considerations
510
+
511
+ ### 1. Session Storage
512
+
513
+ For production, use Redis instead of in-memory sessions:
514
+
515
+ ```javascript
516
+ const RedisStore = require('connect-redis').default;
517
+ const { createClient } = require('redis');
518
+
519
+ const redisClient = createClient();
520
+ redisClient.connect();
521
+
522
+ app.use(session({
523
+ store: new RedisStore({ client: redisClient }),
524
+ secret: process.env.SESSION_SECRET,
525
+ resave: false,
526
+ saveUninitialized: false,
527
+ cookie: {
528
+ httpOnly: true,
529
+ secure: true,
530
+ sameSite: 'strict',
531
+ maxAge: 1000 * 60 * 60 * 24 // 24 hours
532
+ }
533
+ }));
534
+ ```
535
+
536
+ ### 2. Token Refresh
537
+
538
+ Access tokens expire. Implement refresh token logic:
539
+
540
+ ```javascript
541
+ function isTokenExpiring(token) {
542
+ const decoded = jwt.decode(token);
543
+ const now = Math.floor(Date.now() / 1000);
544
+ return decoded.exp - now < 60; // Refresh if less than 60 seconds left
545
+ }
546
+
547
+ app.use(async (req, res, next) => {
548
+ const token = req.cookies.access_token;
549
+ const refreshToken = req.cookies.refresh_token;
550
+
551
+ if (!token) return next();
552
+
553
+ if (isTokenExpiring(token) && refreshToken) {
554
+ try {
555
+ const newTokens = await KeycloakManager.login({
556
+ grant_type: 'refresh_token',
557
+ refresh_token: refreshToken,
558
+ client_id: process.env.KEYCLOAK_CLIENT_ID,
559
+ client_secret: process.env.KEYCLOAK_CLIENT_SECRET
560
+ });
561
+
562
+ res.cookie('access_token', newTokens.access_token, { httpOnly: true, secure: true });
563
+ res.cookie('refresh_token', newTokens.refresh_token, { httpOnly: true, secure: true });
564
+ } catch (error) {
565
+ res.clearCookie('access_token');
566
+ res.clearCookie('refresh_token');
567
+ }
568
+ }
569
+
570
+ next();
571
+ });
572
+ ```
573
+
574
+ ### 3. Token Verification
575
+
576
+ Always verify tokens using Keycloak's public key:
577
+
578
+ ```javascript
579
+ const jwksClient = require('jwks-rsa');
580
+
581
+ const client = jwksClient({
582
+ jwksUri: `${process.env.KEYCLOAK_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`
583
+ });
584
+
585
+ function getKey(header, callback) {
586
+ client.getSigningKey(header.kid, (err, key) => {
587
+ if (err) return callback(err);
588
+ callback(null, key.getPublicKey());
589
+ });
590
+ }
591
+
592
+ function verifyToken(token) {
593
+ return new Promise((resolve, reject) => {
594
+ jwt.verify(token, getKey, {
595
+ algorithms: ['RS256'],
596
+ issuer: `${process.env.KEYCLOAK_URL}/auth/realms/${process.env.KEYCLOAK_REALM}`,
597
+ audience: process.env.KEYCLOAK_CLIENT_ID
598
+ }, (err, decoded) => {
599
+ err ? reject(err) : resolve(decoded);
600
+ });
601
+ });
602
+ }
603
+ ```
604
+
605
+ ## Related Documentation
606
+
607
+ - [loginPKCE() API Reference](../api/configuration.md#loginpkce)
608
+ - [login() API Reference](../api/configuration.md#login)
609
+ - [OAuth2 PKCE Specification](https://tools.ietf.org/html/rfc7636)
610
+ - [Keycloak Authorization Code Flow](https://www.keycloak.org/docs/latest/server_admin/#_oidc)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keycloak-api-manager",
3
- "version": "5.0.6",
3
+ "version": "5.0.7",
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": {