keycloak-api-manager 5.0.7 → 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 +17 -9
- package/docs/api/configuration.md +90 -0
- package/docs/api-reference.md +1 -0
- package/docs/guides/PKCE-Login-Flow.md +46 -148
- 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
|
|
|
@@ -158,10 +161,15 @@ docs/ # Centralized documentation
|
|
|
158
161
|
|
|
159
162
|
## Versioning and Compatibility
|
|
160
163
|
|
|
161
|
-
- Package version: `
|
|
164
|
+
- Package version: `6.0.0`
|
|
162
165
|
- Keycloak Admin client dependency: `@keycloak/keycloak-admin-client`
|
|
163
166
|
- Main compatibility target: Keycloak 25/26
|
|
164
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
|
+
|
|
165
173
|
## License
|
|
166
174
|
|
|
167
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,6 +349,95 @@ 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.
|
package/docs/api-reference.md
CHANGED
|
@@ -80,6 +80,7 @@ KeycloakManager.stop();
|
|
|
80
80
|
| `setConfig()` | Runtime configuration | Core |
|
|
81
81
|
| `getToken()` | Get current access token | Core |
|
|
82
82
|
| `login()` | Preferred OIDC token grant/login endpoint wrapper | Core |
|
|
83
|
+
| `generateAuthorizationUrl()` | Generate PKCE authorization URL and verifier pair | Core |
|
|
83
84
|
| `loginPKCE()` | Authorization Code + PKCE token exchange helper | Core |
|
|
84
85
|
| `auth()` | Backward-compatible alias of `login()` | Core |
|
|
85
86
|
| `stop()` | Stop token refresh timer | Core |
|
|
@@ -72,98 +72,51 @@ PKCE (Proof Key for Code Exchange) is the modern, secure way for browser-based a
|
|
|
72
72
|
|
|
73
73
|
## Step-by-Step Implementation
|
|
74
74
|
|
|
75
|
-
### Step 1: Generate
|
|
75
|
+
### Step 1: Generate Authorization URL
|
|
76
76
|
|
|
77
|
-
When the user clicks "Login",
|
|
77
|
+
When the user clicks "Login", use `generateAuthorizationUrl()` from keycloak-api-manager to generate the PKCE pair and authorization URL:
|
|
78
78
|
|
|
79
79
|
```javascript
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.toString('base64')
|
|
85
|
-
.replace(/\+/g, '-')
|
|
86
|
-
.replace(/\//g, '_')
|
|
87
|
-
.replace(/=/g, '');
|
|
88
|
-
}
|
|
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
|
+
});
|
|
89
84
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
}
|
|
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
|
+
// }
|
|
104
91
|
```
|
|
105
92
|
|
|
106
|
-
|
|
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:
|
|
93
|
+
Store the `state` and `codeVerifier` in your session, then redirect the user to the authorization URL:
|
|
114
94
|
|
|
115
95
|
```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
96
|
app.get('/auth/login', (req, res) => {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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);
|
|
97
|
+
const pkceFlow = KeycloakManager.generateAuthorizationUrl({
|
|
98
|
+
redirect_uri: `${process.env.APP_URL}/auth/callback`
|
|
99
|
+
});
|
|
141
100
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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);
|
|
101
|
+
// Store state and verifier in session for callback validation
|
|
102
|
+
req.session.pkce_state = pkceFlow.state;
|
|
103
|
+
req.session.pkce_verifier = pkceFlow.codeVerifier;
|
|
149
104
|
|
|
150
|
-
// Redirect user to Keycloak login
|
|
151
|
-
res.redirect(authUrl
|
|
105
|
+
// Redirect user to Keycloak login
|
|
106
|
+
res.redirect(pkceFlow.authUrl);
|
|
152
107
|
});
|
|
153
108
|
```
|
|
154
109
|
|
|
155
|
-
**
|
|
156
|
-
-
|
|
157
|
-
-
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
### Step 3: Exchange Authorization Code for Token
|
|
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)
|
|
162
115
|
|
|
163
|
-
When Keycloak redirects back with the authorization code, you exchange it for an access token
|
|
116
|
+
When Keycloak redirects back with the authorization code, you exchange it for an access token using `loginPKCE()`:
|
|
164
117
|
|
|
165
118
|
```javascript
|
|
166
|
-
//
|
|
119
|
+
// User callback after Keycloak login
|
|
167
120
|
app.get('/auth/callback', async (req, res) => {
|
|
168
121
|
try {
|
|
169
122
|
const { code, state, error } = req.query;
|
|
@@ -180,18 +133,15 @@ app.get('/auth/callback', async (req, res) => {
|
|
|
180
133
|
|
|
181
134
|
// 3. Retrieve stored verifier from session
|
|
182
135
|
const code_verifier = req.session.pkce_verifier;
|
|
183
|
-
|
|
184
136
|
if (!code_verifier) {
|
|
185
137
|
return res.status(400).send('PKCE verifier not found in session');
|
|
186
138
|
}
|
|
187
139
|
|
|
188
|
-
// 4. Exchange code for token using
|
|
140
|
+
// 4. Exchange code for token using keycloak-api-manager
|
|
189
141
|
const tokenResponse = await KeycloakManager.loginPKCE({
|
|
190
142
|
code,
|
|
191
143
|
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
|
|
144
|
+
code_verifier
|
|
195
145
|
});
|
|
196
146
|
|
|
197
147
|
// 5. Set secure HTTPOnly cookie with access token
|
|
@@ -232,35 +182,6 @@ app.get('/auth/callback', async (req, res) => {
|
|
|
232
182
|
- ✅ Store token in HttpOnly cookie (prevents XSS theft)
|
|
233
183
|
- ✅ Clear sensitive session data
|
|
234
184
|
|
|
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
185
|
## Complete Working Example
|
|
265
186
|
|
|
266
187
|
Here's a complete Express.js application with PKCE flow:
|
|
@@ -268,7 +189,6 @@ Here's a complete Express.js application with PKCE flow:
|
|
|
268
189
|
```javascript
|
|
269
190
|
const express = require('express');
|
|
270
191
|
const session = require('express-session');
|
|
271
|
-
const crypto = require('crypto');
|
|
272
192
|
const jwt = require('jsonwebtoken');
|
|
273
193
|
const cookieParser = require('cookie-parser');
|
|
274
194
|
const KeycloakManager = require('keycloak-api-manager');
|
|
@@ -287,55 +207,34 @@ app.use(session({
|
|
|
287
207
|
|
|
288
208
|
// Initialize Keycloak Manager
|
|
289
209
|
KeycloakManager.configure({
|
|
210
|
+
baseUrl: process.env.KEYCLOAK_URL,
|
|
290
211
|
realmName: process.env.KEYCLOAK_REALM,
|
|
291
212
|
clientId: process.env.KEYCLOAK_CLIENT_ID,
|
|
292
213
|
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
|
|
293
|
-
|
|
214
|
+
grantType: 'client_credentials'
|
|
294
215
|
});
|
|
295
216
|
|
|
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
217
|
// Routes
|
|
315
218
|
app.get('/auth/login', (req, res) => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const keycloakAuthUrl = `${process.env.KEYCLOAK_URL}/auth/realms/${process.env.KEYCLOAK_REALM}/protocol/openid-connect/auth`;
|
|
322
|
-
const authUrl = new URL(keycloakAuthUrl);
|
|
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
|
+
});
|
|
323
224
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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);
|
|
225
|
+
// Store state and verifier in session (server-side only!)
|
|
226
|
+
req.session.pkce_state = pkceFlow.state;
|
|
227
|
+
req.session.pkce_verifier = pkceFlow.codeVerifier;
|
|
331
228
|
|
|
332
|
-
|
|
229
|
+
// Redirect user to Keycloak login
|
|
230
|
+
res.redirect(pkceFlow.authUrl);
|
|
333
231
|
});
|
|
334
232
|
|
|
335
233
|
app.get('/auth/callback', async (req, res) => {
|
|
336
234
|
try {
|
|
337
235
|
const { code, state, error } = req.query;
|
|
338
236
|
|
|
237
|
+
// Validate state (CSRF protection)
|
|
339
238
|
if (state !== req.session.pkce_state) {
|
|
340
239
|
return res.status(400).send('Invalid state parameter');
|
|
341
240
|
}
|
|
@@ -349,13 +248,11 @@ app.get('/auth/callback', async (req, res) => {
|
|
|
349
248
|
return res.status(400).send('PKCE verifier not found');
|
|
350
249
|
}
|
|
351
250
|
|
|
352
|
-
// Exchange code for token
|
|
251
|
+
// Exchange code for token (client_id + client_secret from configure())
|
|
353
252
|
const tokenResponse = await KeycloakManager.loginPKCE({
|
|
354
253
|
code,
|
|
355
254
|
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
|
|
255
|
+
code_verifier
|
|
359
256
|
});
|
|
360
257
|
|
|
361
258
|
// Set secure cookies
|
|
@@ -373,6 +270,7 @@ app.get('/auth/callback', async (req, res) => {
|
|
|
373
270
|
maxAge: 7 * 24 * 60 * 60 * 1000
|
|
374
271
|
});
|
|
375
272
|
|
|
273
|
+
// Clear sensitive data
|
|
376
274
|
delete req.session.pkce_verifier;
|
|
377
275
|
delete req.session.pkce_state;
|
|
378
276
|
|
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": {
|