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.
- package/Handlers/clientPoliciesHandler.js +4 -2
- package/Handlers/clientsHandler.js +1 -13
- package/Handlers/organizationsHandler.js +2 -1
- package/Handlers/realmsHandler.js +0 -1
- package/Handlers/userProfileHandler.js +2 -2
- package/OIDC_MIGRATION_PLAN.md +112 -42
- package/README.md +5 -3
- package/docs/api/configuration.md +39 -356
- package/docs/api-reference.md +7 -7
- package/docs/guides/PKCE-Login-Flow.md +13 -500
- package/index.js +131 -0
- package/package.json +1 -1
- package/test/helpers/config.js +15 -9
- package/test-output.log +0 -72
|
@@ -1,508 +1,21 @@
|
|
|
1
|
-
# PKCE Login Flow
|
|
1
|
+
# PKCE Login Flow (Deprecated In This Package)
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This package is focused on Keycloak Admin API resource management.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
PKCE and user-authentication helpers in this package are deprecated since v6.0.0 and kept only for backward compatibility:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- `generateAuthorizationUrl(options)`
|
|
8
|
+
- `loginPKCE(credentials)`
|
|
9
|
+
- `login(credentials)`
|
|
10
|
+
- `auth(credentials)`
|
|
8
11
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
+
Migration references:
|
|
74
19
|
|
|
75
|
-
|
|
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)
|