keycloak-api-manager 5.0.6 → 6.0.0
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/OIDC_MIGRATION_PLAN.md +214 -0
- package/README.md +26 -9
- package/docs/api/configuration.md +92 -0
- package/docs/api-reference.md +4 -0
- package/docs/guides/PKCE-Login-Flow.md +508 -0
- package/index.js +108 -0
- package/package.json +1 -1
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# OIDC Methods Migration Plan - keycloak-api-manager
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
L'architettura prevede una **migrazione pianificata** dei metodi OIDC (`auth()`, `login()`, `loginPKCE()`) da `keycloak-api-manager` a `keycloak-express-middleware`.
|
|
6
|
+
|
|
7
|
+
## Current State (v5.0.8)
|
|
8
|
+
|
|
9
|
+
**In keycloak-api-manager:**
|
|
10
|
+
- ✅ `auth(credentials)` - OIDC token endpoint wrapper
|
|
11
|
+
- ✅ `login(credentials)` - Generic token grant helper
|
|
12
|
+
- ✅ `generateAuthorizationUrl(options)` - PKCE URL generator
|
|
13
|
+
- ✅ `loginPKCE(credentials)` - PKCE token exchange
|
|
14
|
+
|
|
15
|
+
## Why This Migration?
|
|
16
|
+
|
|
17
|
+
### Separation of Concerns
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
keycloak-api-manager (Admin API Management)
|
|
21
|
+
├── configure() ← Admin setup
|
|
22
|
+
├── Users.find() ← Admin operations
|
|
23
|
+
├── Realms.create() ← Admin operations
|
|
24
|
+
└── ❌ login() / auth() ← Should NOT be here!
|
|
25
|
+
|
|
26
|
+
keycloak-express-middleware (User Authentication)
|
|
27
|
+
├── protectMiddleware() ← Route protection
|
|
28
|
+
├── Express integration ← Session, cookies
|
|
29
|
+
└── ✅ generateAuthorizationUrl() ← Belongs here
|
|
30
|
+
└── ✅ login() ← Belongs here
|
|
31
|
+
└── ✅ loginPKCE() ← Belongs here
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Why Middleware is Better
|
|
35
|
+
|
|
36
|
+
1. **Natural Context:** Express app has sessions, cookies, redirects
|
|
37
|
+
2. **No Admin Context:** Users don't need admin API management
|
|
38
|
+
3. **Tighter Integration:** Works with middleware ecosystem
|
|
39
|
+
4. **Cleaner Namespaces:** Each package has clear responsibilities
|
|
40
|
+
|
|
41
|
+
## Migration Timeline
|
|
42
|
+
|
|
43
|
+
### Phase 1: ✅ DONE (Now)
|
|
44
|
+
- [x] Create ready-to-integrate files in middleware (`oidc-methods.js`)
|
|
45
|
+
- [x] Write comprehensive tests (21 tests, all passing)
|
|
46
|
+
- [x] Document integration guide
|
|
47
|
+
- [x] Commit to middleware repo
|
|
48
|
+
|
|
49
|
+
**Status:** Ready for integration into `keycloak-express-middleware`
|
|
50
|
+
|
|
51
|
+
### Phase 2: Manual Integration (When You're Ready)
|
|
52
|
+
- [ ] Integrate `oidc-methods.js` into middleware `index.js`
|
|
53
|
+
- [ ] Run tests to verify
|
|
54
|
+
- [ ] Release `keycloak-express-middleware v6.1.0`
|
|
55
|
+
|
|
56
|
+
**Your Action:** Follow `keycloak-express-middleware/OIDC_INTEGRATION_GUIDE.md`
|
|
57
|
+
|
|
58
|
+
### Phase 3: Deprecation in keycloak-api-manager (v6.0.0)
|
|
59
|
+
|
|
60
|
+
Once middleware integration is confirmed:
|
|
61
|
+
|
|
62
|
+
1. **Mark methods as deprecated** in index.js:
|
|
63
|
+
```javascript
|
|
64
|
+
exports.auth = async function auth(credentials = {}) {
|
|
65
|
+
console.warn('⚠️ DEPRECATED: auth() is deprecated in v6.0. ' +
|
|
66
|
+
'Use keycloak-express-middleware.login() instead. ' +
|
|
67
|
+
'See: https://...');
|
|
68
|
+
return requestOidcToken(credentials);
|
|
69
|
+
};
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
2. **Update documentation:**
|
|
73
|
+
- Add migration guide in README
|
|
74
|
+
- Mark OIDC methods as deprecated in API docs
|
|
75
|
+
- Link to middleware documentation
|
|
76
|
+
|
|
77
|
+
3. **Release v6.0.0:**
|
|
78
|
+
- Update package.json version
|
|
79
|
+
- Add breaking change notice
|
|
80
|
+
- Include migration guide in release notes
|
|
81
|
+
|
|
82
|
+
4. **Future (v7.0.0):**
|
|
83
|
+
- Optionally remove these methods entirely
|
|
84
|
+
- Keep at least 1 major version for migration
|
|
85
|
+
|
|
86
|
+
## Current keycloak-api-manager Methods
|
|
87
|
+
|
|
88
|
+
### `auth(credentials)` - Generic OIDC Token Grant
|
|
89
|
+
```javascript
|
|
90
|
+
// Still works, but should use middleware
|
|
91
|
+
const token = await KeycloakManager.auth({
|
|
92
|
+
grant_type: 'password',
|
|
93
|
+
username: 'user',
|
|
94
|
+
password: 'pass'
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**Migration Path:**
|
|
99
|
+
```javascript
|
|
100
|
+
// Instead use middleware
|
|
101
|
+
const token = await keycloakMiddleware.login({
|
|
102
|
+
grant_type: 'password',
|
|
103
|
+
username: 'user',
|
|
104
|
+
password: 'pass'
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### `login(credentials)` - Preferred Alias
|
|
109
|
+
```javascript
|
|
110
|
+
// Currently the preferred way in API manager
|
|
111
|
+
const token = await KeycloakManager.login({
|
|
112
|
+
grant_type: 'client_credentials'
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Migration Path:**
|
|
117
|
+
```javascript
|
|
118
|
+
// Move to middleware
|
|
119
|
+
const token = await keycloakMiddleware.login({
|
|
120
|
+
grant_type: 'client_credentials'
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `generateAuthorizationUrl(options)` - PKCE URL Generator
|
|
125
|
+
```javascript
|
|
126
|
+
// Generates OAuth2 authorization URL + PKCE pair
|
|
127
|
+
const pkceFlow = KeycloakManager.generateAuthorizationUrl({
|
|
128
|
+
redirect_uri: 'https://app/callback'
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Migration Path:**
|
|
133
|
+
```javascript
|
|
134
|
+
// Move to middleware
|
|
135
|
+
const pkceFlow = keycloakMiddleware.generateAuthorizationUrl({
|
|
136
|
+
redirect_uri: 'https://app/callback'
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `loginPKCE(credentials)` - PKCE Token Exchange
|
|
141
|
+
```javascript
|
|
142
|
+
// Exchange auth code for tokens in callback
|
|
143
|
+
const token = await KeycloakManager.loginPKCE({
|
|
144
|
+
code: req.query.code,
|
|
145
|
+
redirect_uri: 'https://app/callback',
|
|
146
|
+
code_verifier: req.session.verifier
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Migration Path:**
|
|
151
|
+
```javascript
|
|
152
|
+
// Move to middleware
|
|
153
|
+
const token = await keycloakMiddleware.loginPKCE({
|
|
154
|
+
code: req.query.code,
|
|
155
|
+
redirect_uri: 'https://app/callback',
|
|
156
|
+
code_verifier: req.session.verifier
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## What Stays in keycloak-api-manager
|
|
161
|
+
|
|
162
|
+
✅ **These remain unchanged forever:**
|
|
163
|
+
- `configure(credentials)` - Admin authentication
|
|
164
|
+
- `setConfig(overrides)` - Configuration management
|
|
165
|
+
- `getToken()` - Get current admin token
|
|
166
|
+
- `stop()` - Cleanup
|
|
167
|
+
- **All handlers:** `users`, `realms`, `clients`, `groups`, `roles`, etc.
|
|
168
|
+
|
|
169
|
+
The package remains the **dedicated Admin API Manager** - exactly what it should be.
|
|
170
|
+
|
|
171
|
+
## Migration Guide for Users (TBD)
|
|
172
|
+
|
|
173
|
+
When ready, we'll publish:
|
|
174
|
+
|
|
175
|
+
```markdown
|
|
176
|
+
# Migrating OIDC Methods from keycloak-api-manager to keycloak-express-middleware
|
|
177
|
+
|
|
178
|
+
## Version Support
|
|
179
|
+
|
|
180
|
+
| Method | deprecated | removed | alternative |
|
|
181
|
+
|--------|-----------|---------|-------------|
|
|
182
|
+
| `auth()` | v6.0.0 | v7.0.0 | middleware.login() |
|
|
183
|
+
| `login()` | v6.0.0 | v7.0.0 | middleware.login() |
|
|
184
|
+
| `generateAuthorizationUrl()` | v6.0.0 | v7.0.0 | middleware.generateAuthorizationUrl() |
|
|
185
|
+
| `loginPKCE()` | v6.0.0 | v7.0.0 | middleware.loginPKCE() |
|
|
186
|
+
|
|
187
|
+
## Migration Steps
|
|
188
|
+
|
|
189
|
+
1. Install/update middleware
|
|
190
|
+
2. Replace method calls
|
|
191
|
+
3. Update imports
|
|
192
|
+
4. Test
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## Next Actions
|
|
196
|
+
|
|
197
|
+
### You Should Do:
|
|
198
|
+
1. Review `keycloak-express-middleware/OIDC_INTEGRATION_GUIDE.md`
|
|
199
|
+
2. Decide if you want to integrate manually or have me automate it
|
|
200
|
+
3. Once middleware is ready, let me know
|
|
201
|
+
|
|
202
|
+
### I Will Do (When You Say):
|
|
203
|
+
1. Integrate methods into middleware `index.js`
|
|
204
|
+
2. Release `keycloak-express-middleware v6.1.0`
|
|
205
|
+
3. Deprecate in `keycloak-api-manager v6.0.0`
|
|
206
|
+
4. Create migration guide
|
|
207
|
+
5. Update all documentation
|
|
208
|
+
|
|
209
|
+
## Questions?
|
|
210
|
+
|
|
211
|
+
- How should we handle the transition period?
|
|
212
|
+
- Do we support both packages simultaneously?
|
|
213
|
+
- Timeline for full migration?
|
|
214
|
+
- Any concerns about the approach?
|
package/README.md
CHANGED
|
@@ -25,6 +25,12 @@ It provides a stable, function-oriented interface for managing Keycloak resource
|
|
|
25
25
|
npm install keycloak-api-manager
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
+
> **⚠️ DEPRECATION NOTICE (v6.0.0):** The OIDC authentication methods (`login()`, `loginPKCE()`, `generateAuthorizationUrl()`, `auth()`) have been **deprecated** and moved to [`keycloak-express-middleware`](https://github.com/smartenv-crs4/keycloak-express-middleware).
|
|
29
|
+
>
|
|
30
|
+
> **This package is now exclusively for Keycloak admin resource management.** For user authentication flows, use `keycloak-express-middleware` instead.
|
|
31
|
+
>
|
|
32
|
+
> See [OIDC_MIGRATION_PLAN.md](OIDC_MIGRATION_PLAN.md) for migration details.
|
|
33
|
+
|
|
28
34
|
## Quick Start
|
|
29
35
|
|
|
30
36
|
```js
|
|
@@ -65,16 +71,13 @@ In Keycloak 26.x, management-permissions APIs used by group/user fine-grained te
|
|
|
65
71
|
- `configure(credentials)`
|
|
66
72
|
- `setConfig(overrides)`
|
|
67
73
|
- `getToken()`
|
|
68
|
-
- `login(credentials)`
|
|
69
|
-
- `loginPKCE(credentials)`
|
|
70
|
-
- `auth(credentials)`
|
|
71
74
|
- `stop()`
|
|
75
|
+
- ~~`login(credentials)`~~ **DEPRECATED** - moved to keycloak-express-middleware
|
|
76
|
+
- ~~`generateAuthorizationUrl(options)`~~ **DEPRECATED** - moved to keycloak-express-middleware
|
|
77
|
+
- ~~`loginPKCE(credentials)`~~ **DEPRECATED** - moved to keycloak-express-middleware
|
|
78
|
+
- ~~`auth(credentials)`~~ **DEPRECATED** - moved to keycloak-express-middleware
|
|
72
79
|
|
|
73
|
-
|
|
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()`.
|
|
80
|
+
**Note:** OIDC authentication methods have been deprecated in v6.0.0. Use [`keycloak-express-middleware`](https://github.com/smartenv-crs4/keycloak-express-middleware) for user authentication flows.
|
|
78
81
|
|
|
79
82
|
Configured handler namespaces:
|
|
80
83
|
|
|
@@ -97,6 +100,12 @@ Configured handler namespaces:
|
|
|
97
100
|
|
|
98
101
|
All documentation is centralized under `docs/`.
|
|
99
102
|
|
|
103
|
+
### Guides (Practical Implementation)
|
|
104
|
+
|
|
105
|
+
- [PKCE Login Flow Guide](docs/guides/PKCE-Login-Flow.md) - Step-by-step guide for implementing OAuth2 Authorization Code + PKCE authentication
|
|
106
|
+
|
|
107
|
+
### API Reference
|
|
108
|
+
|
|
100
109
|
- [API Reference (Index)](docs/api-reference.md)
|
|
101
110
|
- [API - Configuration](docs/api/configuration.md)
|
|
102
111
|
- [API - Realms](docs/api/realms.md)
|
|
@@ -113,6 +122,9 @@ All documentation is centralized under `docs/`.
|
|
|
113
122
|
- [API - User Profile](docs/api/user-profile.md)
|
|
114
123
|
- [API - Client Policies](docs/api/client-policies.md)
|
|
115
124
|
- [API - Server Info](docs/api/server-info.md)
|
|
125
|
+
|
|
126
|
+
### General Documentation
|
|
127
|
+
|
|
116
128
|
- [Architecture and Runtime](docs/architecture.md)
|
|
117
129
|
- [Keycloak Setup and Feature Flags](docs/keycloak-setup.md)
|
|
118
130
|
- [Testing Guide](docs/testing.md)
|
|
@@ -149,10 +161,15 @@ docs/ # Centralized documentation
|
|
|
149
161
|
|
|
150
162
|
## Versioning and Compatibility
|
|
151
163
|
|
|
152
|
-
- Package version: `
|
|
164
|
+
- Package version: `6.0.0`
|
|
153
165
|
- Keycloak Admin client dependency: `@keycloak/keycloak-admin-client`
|
|
154
166
|
- Main compatibility target: Keycloak 25/26
|
|
155
167
|
|
|
168
|
+
### Breaking Changes in v6.0.0
|
|
169
|
+
|
|
170
|
+
OIDC authentication methods (`login()`, `loginPKCE()`, `generateAuthorizationUrl()`, `auth()`) are now deprecated.
|
|
171
|
+
These methods will be removed in v7.0.0. Migrate to `keycloak-express-middleware` for user authentication.
|
|
172
|
+
|
|
156
173
|
## License
|
|
157
174
|
|
|
158
175
|
MIT
|
|
@@ -8,6 +8,7 @@ Core API methods for initializing and managing the Keycloak Admin Client connect
|
|
|
8
8
|
- [setConfig()](#setconfig)
|
|
9
9
|
- [getToken()](#gettoken)
|
|
10
10
|
- [login()](#login)
|
|
11
|
+
- [generateAuthorizationUrl()](#generateauthorizationurl)
|
|
11
12
|
- [loginPKCE()](#loginpkce)
|
|
12
13
|
- [auth()](#auth)
|
|
13
14
|
- [stop()](#stop)
|
|
@@ -348,12 +349,103 @@ console.log(refreshed.access_token);
|
|
|
348
349
|
|
|
349
350
|
---
|
|
350
351
|
|
|
352
|
+
## generateAuthorizationUrl()
|
|
353
|
+
|
|
354
|
+
Generate OAuth2 Authorization Code + PKCE flow initialization. Returns a ready-to-use authorization URL and PKCE pair for server-side session storage.
|
|
355
|
+
|
|
356
|
+
This helper simplifies the first step of PKCE login: generating the authorization URL with PKCE challenge and state parameter.
|
|
357
|
+
|
|
358
|
+
**Syntax:**
|
|
359
|
+
```javascript
|
|
360
|
+
const pkceFlow = KeycloakManager.generateAuthorizationUrl(options)
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Parameters
|
|
364
|
+
|
|
365
|
+
#### options (Object) ⚠️ Required
|
|
366
|
+
|
|
367
|
+
| Property | Type | Required | Description |
|
|
368
|
+
|----------|------|----------|-------------|
|
|
369
|
+
| `redirect_uri` | string | ⚠️ Yes* | Redirect URI where user returns after login |
|
|
370
|
+
| `redirectUri` | string | ⚠️ Yes* | CamelCase alias of `redirect_uri` |
|
|
371
|
+
| `scope` | string | 📋 Optional | Space-separated scopes (default: `'openid profile email'`) |
|
|
372
|
+
| `state` | string | 📋 Optional | Custom state value (auto-generated if not provided) |
|
|
373
|
+
|
|
374
|
+
`*` required with either snake_case or camelCase form.
|
|
375
|
+
|
|
376
|
+
### Returns
|
|
377
|
+
|
|
378
|
+
**Object** - PKCE flow initialization data:
|
|
379
|
+
|
|
380
|
+
```javascript
|
|
381
|
+
{
|
|
382
|
+
authUrl: 'https://keycloak.example.com/realms/my-realm/protocol/openid-connect/auth?...',
|
|
383
|
+
state: 'random_state_string',
|
|
384
|
+
codeVerifier: 'random_code_verifier_string'
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
- `authUrl`: Ready-to-use authorization URL to redirect user to
|
|
389
|
+
- `state`: CSRF token to validate in callback (store in session)
|
|
390
|
+
- `codeVerifier`: PKCE proof to exchange for code in callback (store in session, **never expose to client**)
|
|
391
|
+
|
|
392
|
+
### Example
|
|
393
|
+
|
|
394
|
+
```javascript
|
|
395
|
+
// Step 1: Generate authorization URL
|
|
396
|
+
app.get('/auth/login', (req, res) => {
|
|
397
|
+
const pkceFlow = KeycloakManager.generateAuthorizationUrl({
|
|
398
|
+
redirect_uri: `${process.env.APP_URL}/auth/callback`,
|
|
399
|
+
scope: 'openid profile email'
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Store in session server-side
|
|
403
|
+
req.session.pkce_state = pkceFlow.state;
|
|
404
|
+
req.session.pkce_verifier = pkceFlow.codeVerifier;
|
|
405
|
+
|
|
406
|
+
// Redirect user to Keycloak
|
|
407
|
+
res.redirect(pkceFlow.authUrl);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Step 2: In callback, recover verifier and exchange code
|
|
411
|
+
app.get('/auth/callback', async (req, res) => {
|
|
412
|
+
const { code, state } = req.query;
|
|
413
|
+
|
|
414
|
+
// Validate state
|
|
415
|
+
if (state !== req.session.pkce_state) {
|
|
416
|
+
return res.status(400).send('CSRF attack detected');
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Exchange code for token
|
|
420
|
+
const tokens = await KeycloakManager.loginPKCE({
|
|
421
|
+
code,
|
|
422
|
+
redirect_uri: `${process.env.APP_URL}/auth/callback`,
|
|
423
|
+
code_verifier: req.session.pkce_verifier
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Use tokens...
|
|
427
|
+
});
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Notes
|
|
431
|
+
|
|
432
|
+
- `generateAuthorizationUrl()` does **not** call Keycloak; it generates URLs and PKCE values locally.
|
|
433
|
+
- `state` and `codeVerifier` must be stored **server-side only** in session storage, never in cookies or local storage.
|
|
434
|
+
- `codeVerifier` must be kept secret and never exposed to the browser.
|
|
435
|
+
- `state` provides CSRF protection and must be validated in the callback.
|
|
436
|
+
- Uses cryptographically secure random generation for `codeVerifier` and `state`.
|
|
437
|
+
- Code challenge is SHA256-hashed (S256 method), not plain text.
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
351
441
|
## loginPKCE()
|
|
352
442
|
|
|
353
443
|
Perform Authorization Code + PKCE token exchange.
|
|
354
444
|
|
|
355
445
|
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
446
|
|
|
447
|
+
> **📖 For a complete step-by-step guide on implementing PKCE flow in your application, see [PKCE Login Flow Guide](../guides/PKCE-Login-Flow.md)**
|
|
448
|
+
|
|
357
449
|
**Syntax:**
|
|
358
450
|
```javascript
|
|
359
451
|
await KeycloakManager.loginPKCE(credentials)
|
package/docs/api-reference.md
CHANGED
|
@@ -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
|
|
|
@@ -77,6 +80,7 @@ KeycloakManager.stop();
|
|
|
77
80
|
| `setConfig()` | Runtime configuration | Core |
|
|
78
81
|
| `getToken()` | Get current access token | Core |
|
|
79
82
|
| `login()` | Preferred OIDC token grant/login endpoint wrapper | Core |
|
|
83
|
+
| `generateAuthorizationUrl()` | Generate PKCE authorization URL and verifier pair | Core |
|
|
80
84
|
| `loginPKCE()` | Authorization Code + PKCE token exchange helper | Core |
|
|
81
85
|
| `auth()` | Backward-compatible alias of `login()` | Core |
|
|
82
86
|
| `stop()` | Stop token refresh timer | Core |
|
|
@@ -0,0 +1,508 @@
|
|
|
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 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)
|
package/index.js
CHANGED
|
@@ -177,14 +177,50 @@ async function requestOidcToken(credentials = {}) {
|
|
|
177
177
|
return payload;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
/**
|
|
181
|
+
* @deprecated v6.0.0 - This method has been moved to keycloak-express-middleware.
|
|
182
|
+
* Use the middleware package for user authentication instead. See:
|
|
183
|
+
* https://github.com/smartenv-crs4/keycloak-express-middleware#oidc-authentication
|
|
184
|
+
*
|
|
185
|
+
* This API manager is intended for Keycloak admin resource management only.
|
|
186
|
+
* For user authentication flows, import from keycloak-express-middleware.
|
|
187
|
+
*
|
|
188
|
+
* @see {@link https://github.com/smartenv-crs4/keycloak-express-middleware|keycloak-express-middleware}
|
|
189
|
+
* @param {object} credentials - OIDC token request credentials
|
|
190
|
+
* @returns {Promise<object>} Token response containing access_token, refresh_token, etc.
|
|
191
|
+
*/
|
|
180
192
|
exports.auth = async function auth(credentials = {}) {
|
|
181
193
|
return requestOidcToken(credentials);
|
|
182
194
|
};
|
|
183
195
|
|
|
196
|
+
/**
|
|
197
|
+
* @deprecated v6.0.0 - This method has been moved to keycloak-express-middleware.
|
|
198
|
+
* Use the middleware package for user authentication instead. See:
|
|
199
|
+
* https://github.com/smartenv-crs4/keycloak-express-middleware#oidc-authentication
|
|
200
|
+
*
|
|
201
|
+
* This API manager is intended for Keycloak admin resource management only.
|
|
202
|
+
* For user authentication flows, import from keycloak-express-middleware.
|
|
203
|
+
*
|
|
204
|
+
* @see {@link https://github.com/smartenv-crs4/keycloak-express-middleware|keycloak-express-middleware}
|
|
205
|
+
* @param {object} credentials - OIDC token request credentials (supports any OAuth2 grant type)
|
|
206
|
+
* @returns {Promise<object>} Token response containing access_token, refresh_token, etc.
|
|
207
|
+
*/
|
|
184
208
|
exports.login = async function login(credentials = {}) {
|
|
185
209
|
return requestOidcToken(credentials);
|
|
186
210
|
};
|
|
187
211
|
|
|
212
|
+
/**
|
|
213
|
+
* @deprecated v6.0.0 - This method has been moved to keycloak-express-middleware.
|
|
214
|
+
* Use the middleware package for user authentication instead. See:
|
|
215
|
+
* https://github.com/smartenv-crs4/keycloak-express-middleware#oidc-pkce-flow
|
|
216
|
+
*
|
|
217
|
+
* This API manager is intended for Keycloak admin resource management only.
|
|
218
|
+
* For user authentication flows, import from keycloak-express-middleware.
|
|
219
|
+
*
|
|
220
|
+
* @see {@link https://github.com/smartenv-crs4/keycloak-express-middleware|keycloak-express-middleware}
|
|
221
|
+
* @param {object} credentials - PKCE authorization code exchange parameters
|
|
222
|
+
* @returns {Promise<object>} Token response containing access_token, refresh_token, etc.
|
|
223
|
+
*/
|
|
188
224
|
exports.loginPKCE = async function loginPKCE(credentials = {}) {
|
|
189
225
|
const {
|
|
190
226
|
code,
|
|
@@ -225,3 +261,75 @@ exports.loginPKCE = async function loginPKCE(credentials = {}) {
|
|
|
225
261
|
...rest
|
|
226
262
|
});
|
|
227
263
|
};
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* @deprecated v6.0.0 - This method has been moved to keycloak-express-middleware.
|
|
267
|
+
* Use the middleware package for user authentication instead. See:
|
|
268
|
+
* https://github.com/smartenv-crs4/keycloak-express-middleware#generating-authorization-urls
|
|
269
|
+
*
|
|
270
|
+
* This API manager is intended for Keycloak admin resource management only.
|
|
271
|
+
* For user authentication flows, import from keycloak-express-middleware.
|
|
272
|
+
*
|
|
273
|
+
* @see {@link https://github.com/smartenv-crs4/keycloak-express-middleware|keycloak-express-middleware}
|
|
274
|
+
* @param {object} options - Authorization URL generation options
|
|
275
|
+
* @returns {object} Object with { authUrl, state, codeVerifier } for PKCE flow
|
|
276
|
+
*/
|
|
277
|
+
exports.generateAuthorizationUrl = function generateAuthorizationUrl(options = {}) {
|
|
278
|
+
assertConfigured();
|
|
279
|
+
|
|
280
|
+
const {
|
|
281
|
+
redirect_uri,
|
|
282
|
+
redirectUri,
|
|
283
|
+
scope,
|
|
284
|
+
state: customState
|
|
285
|
+
} = options;
|
|
286
|
+
|
|
287
|
+
const resolvedRedirectUri = redirect_uri || redirectUri;
|
|
288
|
+
if (!resolvedRedirectUri) {
|
|
289
|
+
throw new Error('generateAuthorizationUrl requires "redirect_uri" (or "redirectUri").');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const crypto = require('crypto');
|
|
293
|
+
|
|
294
|
+
// Helper to encode bytes to base64url
|
|
295
|
+
function base64url(buffer) {
|
|
296
|
+
return buffer
|
|
297
|
+
.toString('base64')
|
|
298
|
+
.replace(/\+/g, '-')
|
|
299
|
+
.replace(/\//g, '_')
|
|
300
|
+
.replace(/=/g, '');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Generate PKCE pair
|
|
304
|
+
const codeVerifier = base64url(crypto.randomBytes(96));
|
|
305
|
+
const codeChallenge = base64url(
|
|
306
|
+
crypto.createHash('sha256').update(codeVerifier).digest()
|
|
307
|
+
);
|
|
308
|
+
|
|
309
|
+
// Generate or use provided state
|
|
310
|
+
const state = customState || base64url(crypto.randomBytes(32));
|
|
311
|
+
|
|
312
|
+
// Build authorization URL
|
|
313
|
+
const authUrl = new URL(
|
|
314
|
+
`${runtimeConfig.baseUrl}/realms/${runtimeConfig.realmName}/protocol/openid-connect/auth`
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
authUrl.searchParams.append('client_id', runtimeConfig.clientId);
|
|
318
|
+
authUrl.searchParams.append('response_type', 'code');
|
|
319
|
+
authUrl.searchParams.append('redirect_uri', resolvedRedirectUri);
|
|
320
|
+
authUrl.searchParams.append('code_challenge', codeChallenge);
|
|
321
|
+
authUrl.searchParams.append('code_challenge_method', 'S256');
|
|
322
|
+
authUrl.searchParams.append('state', state);
|
|
323
|
+
|
|
324
|
+
if (scope) {
|
|
325
|
+
authUrl.searchParams.append('scope', scope);
|
|
326
|
+
} else {
|
|
327
|
+
authUrl.searchParams.append('scope', 'openid profile email');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
authUrl: authUrl.toString(),
|
|
332
|
+
state,
|
|
333
|
+
codeVerifier
|
|
334
|
+
};
|
|
335
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keycloak-api-manager",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
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": {
|