keycloak-api-manager 6.0.0 → 6.0.2

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.
@@ -1,508 +1,21 @@
1
- # PKCE Login Flow Guide
1
+ # PKCE Login Flow (Deprecated In This Package)
2
2
 
3
- This guide walks you through implementing OAuth2 Authorization Code + PKCE flow in your application using Keycloak and the keycloak-api-manager library.
3
+ This package is focused on Keycloak Admin API resource management.
4
4
 
5
- ## Overview
5
+ PKCE and user-authentication helpers in this package are deprecated since v6.0.0 and kept only for backward compatibility:
6
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:
7
+ - `generateAuthorizationUrl(options)`
8
+ - `loginPKCE(credentials)`
9
+ - `login(credentials)`
10
+ - `auth(credentials)`
8
11
 
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
12
+ These methods are planned for removal in v7.0.0.
14
13
 
15
- ## Flow Diagram
14
+ For production user authentication flows (including Authorization Code + PKCE), use:
16
15
 
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
- ```
16
+ - https://github.com/smartenv-crs4/keycloak-express-middleware
72
17
 
73
- ## Step-by-Step Implementation
18
+ Migration references:
74
19
 
75
- ### Step 1: Generate Authorization URL
76
-
77
- When the user clicks "Login", use `generateAuthorizationUrl()` from keycloak-api-manager to generate the PKCE pair and authorization URL:
78
-
79
- ```javascript
80
- const pkceFlow = KeycloakManager.generateAuthorizationUrl({
81
- redirect_uri: `${process.env.APP_URL}/auth/callback`,
82
- scope: 'openid profile email' // optional, defaults to 'openid profile email'
83
- });
84
-
85
- // Result contains:
86
- // {
87
- // authUrl: 'https://keycloak.example.com/realms/my-realm/protocol/openid-connect/auth?...',
88
- // state: 'random_state_value',
89
- // codeVerifier: 'random_verifier_value'
90
- // }
91
- ```
92
-
93
- Store the `state` and `codeVerifier` in your session, then redirect the user to the authorization URL:
94
-
95
- ```javascript
96
- app.get('/auth/login', (req, res) => {
97
- const pkceFlow = KeycloakManager.generateAuthorizationUrl({
98
- redirect_uri: `${process.env.APP_URL}/auth/callback`
99
- });
100
-
101
- // Store state and verifier in session for callback validation
102
- req.session.pkce_state = pkceFlow.state;
103
- req.session.pkce_verifier = pkceFlow.codeVerifier;
104
-
105
- // Redirect user to Keycloak login
106
- res.redirect(pkceFlow.authUrl);
107
- });
108
- ```
109
-
110
- **Why this works:**
111
- - `generateAuthorizationUrl()` handles all PKCE complexity internally
112
- - Returns a ready-to-use authorization URL
113
- - State parameter provides CSRF protection
114
- - Code verifier is stored server-side (never exposed to client)
115
-
116
- When Keycloak redirects back with the authorization code, you exchange it for an access token using `loginPKCE()`:
117
-
118
- ```javascript
119
- // User callback after Keycloak login
120
- app.get('/auth/callback', async (req, res) => {
121
- try {
122
- const { code, state, error } = req.query;
123
-
124
- // 1. Validate state parameter (CSRF protection)
125
- if (state !== req.session.pkce_state) {
126
- return res.status(400).send('Invalid state parameter - CSRF attack detected');
127
- }
128
-
129
- // 2. Check for authorization errors
130
- if (error) {
131
- return res.status(400).send(`Authorization failed: ${error}`);
132
- }
133
-
134
- // 3. Retrieve stored verifier from session
135
- const code_verifier = req.session.pkce_verifier;
136
- if (!code_verifier) {
137
- return res.status(400).send('PKCE verifier not found in session');
138
- }
139
-
140
- // 4. Exchange code for token using keycloak-api-manager
141
- const tokenResponse = await KeycloakManager.loginPKCE({
142
- code,
143
- redirect_uri: `${process.env.APP_URL}/auth/callback`,
144
- code_verifier
145
- });
146
-
147
- // 5. Set secure HTTPOnly cookie with access token
148
- res.cookie('access_token', tokenResponse.access_token, {
149
- httpOnly: true, // Prevent XSS access
150
- secure: true, // HTTPS only
151
- sameSite: 'strict', // CSRF protection
152
- maxAge: tokenResponse.expires_in * 1000
153
- });
154
-
155
- // Also store refresh token separately
156
- res.cookie('refresh_token', tokenResponse.refresh_token, {
157
- httpOnly: true,
158
- secure: true,
159
- sameSite: 'strict',
160
- maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
161
- });
162
-
163
- // 6. Clear sensitive session data
164
- delete req.session.pkce_verifier;
165
- delete req.session.pkce_state;
166
-
167
- // Redirect to application home
168
- res.redirect('/dashboard');
169
-
170
- } catch (error) {
171
- console.error('Token exchange failed:', error);
172
- res.status(500).send('Authentication failed');
173
- }
174
- });
175
- ```
176
-
177
- **Security checks in this step:**
178
- - ✅ Validate `state` matches what we stored (CSRF protection)
179
- - ✅ Check for authorization errors
180
- - ✅ Verify `code_verifier` exists in session
181
- - ✅ Exchange code with verifier (proves we initiated the flow)
182
- - ✅ Store token in HttpOnly cookie (prevents XSS theft)
183
- - ✅ Clear sensitive session data
184
-
185
- ## Complete Working Example
186
-
187
- Here's a complete Express.js application with PKCE flow:
188
-
189
- ```javascript
190
- const express = require('express');
191
- const session = require('express-session');
192
- const jwt = require('jsonwebtoken');
193
- const cookieParser = require('cookie-parser');
194
- const KeycloakManager = require('keycloak-api-manager');
195
-
196
- const app = express();
197
-
198
- // Middleware
199
- app.use(express.json());
200
- app.use(cookieParser());
201
- app.use(session({
202
- secret: process.env.SESSION_SECRET || 'dev-secret',
203
- resave: false,
204
- saveUninitialized: true,
205
- cookie: { httpOnly: true, secure: false } // Set secure: true in production
206
- }));
207
-
208
- // Initialize Keycloak Manager
209
- KeycloakManager.configure({
210
- baseUrl: process.env.KEYCLOAK_URL,
211
- realmName: process.env.KEYCLOAK_REALM,
212
- clientId: process.env.KEYCLOAK_CLIENT_ID,
213
- clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
214
- grantType: 'client_credentials'
215
- });
216
-
217
- // Routes
218
- app.get('/auth/login', (req, res) => {
219
- // Generate PKCE pair and authorization URL
220
- const pkceFlow = KeycloakManager.generateAuthorizationUrl({
221
- redirect_uri: `${process.env.APP_URL}/auth/callback`,
222
- scope: 'openid profile email'
223
- });
224
-
225
- // Store state and verifier in session (server-side only!)
226
- req.session.pkce_state = pkceFlow.state;
227
- req.session.pkce_verifier = pkceFlow.codeVerifier;
228
-
229
- // Redirect user to Keycloak login
230
- res.redirect(pkceFlow.authUrl);
231
- });
232
-
233
- app.get('/auth/callback', async (req, res) => {
234
- try {
235
- const { code, state, error } = req.query;
236
-
237
- // Validate state (CSRF protection)
238
- if (state !== req.session.pkce_state) {
239
- return res.status(400).send('Invalid state parameter');
240
- }
241
-
242
- if (error) {
243
- return res.status(400).send(`Authorization failed: ${error}`);
244
- }
245
-
246
- const code_verifier = req.session.pkce_verifier;
247
- if (!code_verifier) {
248
- return res.status(400).send('PKCE verifier not found');
249
- }
250
-
251
- // Exchange code for token (client_id + client_secret from configure())
252
- const tokenResponse = await KeycloakManager.loginPKCE({
253
- code,
254
- redirect_uri: `${process.env.APP_URL}/auth/callback`,
255
- code_verifier
256
- });
257
-
258
- // Set secure cookies
259
- res.cookie('access_token', tokenResponse.access_token, {
260
- httpOnly: true,
261
- secure: true,
262
- sameSite: 'strict',
263
- maxAge: tokenResponse.expires_in * 1000
264
- });
265
-
266
- res.cookie('refresh_token', tokenResponse.refresh_token, {
267
- httpOnly: true,
268
- secure: true,
269
- sameSite: 'strict',
270
- maxAge: 7 * 24 * 60 * 60 * 1000
271
- });
272
-
273
- // Clear sensitive data
274
- delete req.session.pkce_verifier;
275
- delete req.session.pkce_state;
276
-
277
- res.redirect('/dashboard');
278
-
279
- } catch (error) {
280
- console.error('Token exchange failed:', error);
281
- res.status(500).send('Authentication failed');
282
- }
283
- });
284
-
285
- app.get('/dashboard', (req, res) => {
286
- const token = req.cookies.access_token;
287
- if (!token) {
288
- return res.redirect('/auth/login');
289
- }
290
-
291
- try {
292
- const decoded = jwt.decode(token); // Or verify with public key in production
293
- res.send(`Welcome, ${decoded.preferred_username}!`);
294
- } catch (error) {
295
- res.redirect('/auth/login');
296
- }
297
- });
298
-
299
- app.get('/logout', (req, res) => {
300
- res.clearCookie('access_token');
301
- res.clearCookie('refresh_token');
302
- req.session.destroy();
303
- res.redirect('/');
304
- });
305
-
306
- // Start server
307
- app.listen(3000, () => {
308
- console.log('Server running on http://localhost:3000');
309
- console.log('Login at http://localhost:3000/auth/login');
310
- });
311
- ```
312
-
313
- ## Environment Variables
314
-
315
- ```bash
316
- KEYCLOAK_URL=http://localhost:8080
317
- KEYCLOAK_REALM=master
318
- KEYCLOAK_CLIENT_ID=my-app
319
- KEYCLOAK_CLIENT_SECRET=your-client-secret
320
- APP_URL=http://localhost:3000
321
- SESSION_SECRET=your-session-secret
322
- ```
323
-
324
- ## Security Best Practices
325
-
326
- ### ✅ DO
327
-
328
- - ✅ Store PKCE verifier in **secure server-side session**, never in cookies
329
- - ✅ Validate state parameter every time (CSRF protection)
330
- - ✅ Use **HttpOnly cookies** for tokens (prevents XSS theft)
331
- - ✅ Use **Secure flag** on cookies (HTTPS only in production)
332
- - ✅ Use **SameSite=strict** on cookies (CSRF protection)
333
- - ✅ Clear sensitive data from session after token exchange
334
- - ✅ Use SHA256 for code_challenge (`S256` method)
335
- - ✅ Generate cryptographically secure random values (use `crypto` module)
336
-
337
- ### ❌ DON'T
338
-
339
- - ❌ Store code_verifier in browser (localStorage, sessionStorage, etc.)
340
- - ❌ Skip state parameter validation
341
- - ❌ Use weak random generators
342
- - ❌ Expose tokens in URL parameters
343
- - ❌ Store tokens in browser storage (vulnerable to XSS)
344
- - ❌ Use plain HTTP (always HTTPS in production)
345
- - ❌ Reuse PKCE pairs or state values
346
-
347
- ## Common Issues & Troubleshooting
348
-
349
- ### "Invalid state parameter - CSRF attack detected"
350
-
351
- **Cause:** Browser tabs have separate sessions, or cookies are not being persisted.
352
-
353
- **Solution:**
354
- ```javascript
355
- // Ensure session cookies are sent with redirects
356
- app.use(session({
357
- cookie: {
358
- httpOnly: true,
359
- secure: true,
360
- sameSite: 'lax' // Allow cross-site for redirects
361
- }
362
- }));
363
- ```
364
-
365
- ### "PKCE verifier not found in session"
366
-
367
- **Cause:** Session was lost between `/auth/login` and `/auth/callback`.
368
-
369
- **Debug:**
370
- ```javascript
371
- app.get('/auth/callback', (req, res) => {
372
- console.log('Session ID:', req.sessionID);
373
- console.log('Session data:', req.session);
374
- // ... rest of callback
375
- });
376
- ```
377
-
378
- ### "Invalid code_challenge"
379
-
380
- **Cause:** The code_challenge calculated by Keycloak doesn't match our SHA256 hash.
381
-
382
- **Verify:** The `createPkcePair()` function uses S256 (SHA256). Ensure code_challenge is:
383
- 1. Lowercase base64url-encoded
384
- 2. Derived from code_verifier with SHA256, not any other hash
385
-
386
- ### Access token not being set
387
-
388
- **Cause:** `loginPKCE()` threw an error that wasn't caught, or response format is different.
389
-
390
- **Debug:**
391
- ```javascript
392
- const tokenResponse = await KeycloakManager.loginPKCE({...});
393
- console.log('Token response:', JSON.stringify(tokenResponse, null, 2));
394
- ```
395
-
396
- Expected response:
397
- ```json
398
- {
399
- "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
400
- "expires_in": 300,
401
- "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
402
- "token_type": "Bearer",
403
- "scope": "openid profile email"
404
- }
405
- ```
406
-
407
- ## Production Considerations
408
-
409
- ### 1. Session Storage
410
-
411
- For production, use Redis instead of in-memory sessions:
412
-
413
- ```javascript
414
- const RedisStore = require('connect-redis').default;
415
- const { createClient } = require('redis');
416
-
417
- const redisClient = createClient();
418
- redisClient.connect();
419
-
420
- app.use(session({
421
- store: new RedisStore({ client: redisClient }),
422
- secret: process.env.SESSION_SECRET,
423
- resave: false,
424
- saveUninitialized: false,
425
- cookie: {
426
- httpOnly: true,
427
- secure: true,
428
- sameSite: 'strict',
429
- maxAge: 1000 * 60 * 60 * 24 // 24 hours
430
- }
431
- }));
432
- ```
433
-
434
- ### 2. Token Refresh
435
-
436
- Access tokens expire. Implement refresh token logic:
437
-
438
- ```javascript
439
- function isTokenExpiring(token) {
440
- const decoded = jwt.decode(token);
441
- const now = Math.floor(Date.now() / 1000);
442
- return decoded.exp - now < 60; // Refresh if less than 60 seconds left
443
- }
444
-
445
- app.use(async (req, res, next) => {
446
- const token = req.cookies.access_token;
447
- const refreshToken = req.cookies.refresh_token;
448
-
449
- if (!token) return next();
450
-
451
- if (isTokenExpiring(token) && refreshToken) {
452
- try {
453
- const newTokens = await KeycloakManager.login({
454
- grant_type: 'refresh_token',
455
- refresh_token: refreshToken,
456
- client_id: process.env.KEYCLOAK_CLIENT_ID,
457
- client_secret: process.env.KEYCLOAK_CLIENT_SECRET
458
- });
459
-
460
- res.cookie('access_token', newTokens.access_token, { httpOnly: true, secure: true });
461
- res.cookie('refresh_token', newTokens.refresh_token, { httpOnly: true, secure: true });
462
- } catch (error) {
463
- res.clearCookie('access_token');
464
- res.clearCookie('refresh_token');
465
- }
466
- }
467
-
468
- next();
469
- });
470
- ```
471
-
472
- ### 3. Token Verification
473
-
474
- Always verify tokens using Keycloak's public key:
475
-
476
- ```javascript
477
- const jwksClient = require('jwks-rsa');
478
-
479
- const client = jwksClient({
480
- jwksUri: `${process.env.KEYCLOAK_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/certs`
481
- });
482
-
483
- function getKey(header, callback) {
484
- client.getSigningKey(header.kid, (err, key) => {
485
- if (err) return callback(err);
486
- callback(null, key.getPublicKey());
487
- });
488
- }
489
-
490
- function verifyToken(token) {
491
- return new Promise((resolve, reject) => {
492
- jwt.verify(token, getKey, {
493
- algorithms: ['RS256'],
494
- issuer: `${process.env.KEYCLOAK_URL}/auth/realms/${process.env.KEYCLOAK_REALM}`,
495
- audience: process.env.KEYCLOAK_CLIENT_ID
496
- }, (err, decoded) => {
497
- err ? reject(err) : resolve(decoded);
498
- });
499
- });
500
- }
501
- ```
502
-
503
- ## Related Documentation
504
-
505
- - [loginPKCE() API Reference](../api/configuration.md#loginpkce)
506
- - [login() API Reference](../api/configuration.md#login)
507
- - [OAuth2 PKCE Specification](https://tools.ietf.org/html/rfc7636)
508
- - [Keycloak Authorization Code Flow](https://www.keycloak.org/docs/latest/server_admin/#_oidc)
20
+ - [OIDC Migration Plan](../../OIDC_MIGRATION_PLAN.md)
21
+ - [Configuration & Authentication](../api/configuration.md)