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.
@@ -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
- `login(credentials)` is the preferred OIDC token endpoint helper for login/token grant flows (user/client).
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: `5.0.1`
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)
@@ -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": "5.0.6",
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": {