keycloak-express-middleware 6.3.0 → 6.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/package.json +29 -3
- package/test/helpers/ensure-test-config.js +114 -0
- package/oidc-methods.js +0 -327
- package/test/config/secrets.json +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [6.3.2] - 2026-03-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Automatic test config bootstrap via `test/helpers/ensure-test-config.js`.
|
|
9
|
+
- `npm run setup-keycloak` now ensures missing test config files are recreated before interactive setup:
|
|
10
|
+
- `test/config/secrets.json`
|
|
11
|
+
- `test/docker-keycloak/.env`
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
- npm package `files` whitelist keeps `test/` assets available on install while excluding sensitive/generated files (`test/config/secrets.json`, `test/docker-keycloak/.env`, local cert files).
|
|
15
|
+
|
|
16
|
+
## [6.3.1] - 2026-03-18
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- npm package content policy updated to include `test/` files in published tarball.
|
|
20
|
+
- `package.json` `files` whitelist now explicitly includes `test/**`, so test assets are available both via repository clone and `npm install`.
|
|
21
|
+
|
|
5
22
|
## [6.3.0] - 2026-03-18
|
|
6
23
|
|
|
7
24
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keycloak-express-middleware",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.2",
|
|
4
4
|
"description": "Adapter API to integrate Node.js (Express) applications with Keycloak. Provides middleware for authentication, authorization, token validation, and route protection via OpenID Connect.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"typings": "index.d.ts",
|
|
@@ -11,9 +11,35 @@
|
|
|
11
11
|
"default": "./index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
+
"files": [
|
|
15
|
+
"index.js",
|
|
16
|
+
"index.d.ts",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"CHANGELOG.md",
|
|
20
|
+
"MIGRATION_STATUS.md",
|
|
21
|
+
"OIDC_INTEGRATION_GUIDE.md",
|
|
22
|
+
"config.js",
|
|
23
|
+
"config/default.json",
|
|
24
|
+
"docs/**",
|
|
25
|
+
"test/.mocharc.json",
|
|
26
|
+
"test/*.test.js",
|
|
27
|
+
"test/package.json",
|
|
28
|
+
"test/package-lock.json",
|
|
29
|
+
"test/config/default.json",
|
|
30
|
+
"test/config/secrets.json.example",
|
|
31
|
+
"test/helpers/**",
|
|
32
|
+
"test/support/**",
|
|
33
|
+
"test/docker-keycloak/README.md",
|
|
34
|
+
"test/docker-keycloak/docker-compose.yml",
|
|
35
|
+
"test/docker-keycloak/docker-compose-https.yml",
|
|
36
|
+
"test/docker-keycloak/setup-keycloak.js",
|
|
37
|
+
"test/docker-keycloak/certs/README.md",
|
|
38
|
+
"test/docker-keycloak/certs/.gitkeep"
|
|
39
|
+
],
|
|
14
40
|
"scripts": {
|
|
15
|
-
"test": "npm --prefix test install && npm --prefix test run setup-keycloak && NODE_PATH=./test/node_modules npm --prefix test test",
|
|
16
|
-
"setup-keycloak": "eval \"$(ssh-agent -s)\" && ssh-add ~/.ssh/id_ed25519 && NODE_ENV=test node test/docker-keycloak/setup-keycloak.js"
|
|
41
|
+
"test": "node test/helpers/ensure-test-config.js && npm --prefix test install && npm --prefix test run setup-keycloak && NODE_PATH=./test/node_modules npm --prefix test test",
|
|
42
|
+
"setup-keycloak": "node test/helpers/ensure-test-config.js && eval \"$(ssh-agent -s)\" && ssh-add ~/.ssh/id_ed25519 && NODE_ENV=test node test/docker-keycloak/setup-keycloak.js"
|
|
17
43
|
},
|
|
18
44
|
"dependencies": {
|
|
19
45
|
"express-session": "^1.19.0",
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ensures local test configuration files exist with valid defaults.
|
|
5
|
+
*
|
|
6
|
+
* Generated files (if missing):
|
|
7
|
+
* - test/config/secrets.json
|
|
8
|
+
* - test/docker-keycloak/.env
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node test/helpers/ensure-test-config.js
|
|
12
|
+
* node test/helpers/ensure-test-config.js --regenerate
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const force = process.argv.includes('--regenerate');
|
|
19
|
+
|
|
20
|
+
const repoRoot = path.join(__dirname, '..', '..');
|
|
21
|
+
const secretsPath = path.join(repoRoot, 'test', 'config', 'secrets.json');
|
|
22
|
+
const dockerEnvPath = path.join(repoRoot, 'test', 'docker-keycloak', '.env');
|
|
23
|
+
|
|
24
|
+
function ensureDir(filePath) {
|
|
25
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJsonSafe(filePath) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
31
|
+
} catch (_err) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function writeJson(filePath, data) {
|
|
37
|
+
ensureDir(filePath);
|
|
38
|
+
fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ensureSecrets() {
|
|
42
|
+
const defaults = {
|
|
43
|
+
test: {
|
|
44
|
+
keycloak: {
|
|
45
|
+
adminPassword: 'admin',
|
|
46
|
+
clientSecret: 'test-client-secret',
|
|
47
|
+
testPassword: 'test-password'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (!fs.existsSync(secretsPath) || force) {
|
|
53
|
+
writeJson(secretsPath, defaults);
|
|
54
|
+
console.log(`[ensure-test-config] ${force ? 'Regenerated' : 'Created'} test/config/secrets.json`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const current = readJsonSafe(secretsPath);
|
|
59
|
+
if (!current) {
|
|
60
|
+
writeJson(secretsPath, defaults);
|
|
61
|
+
console.log('[ensure-test-config] Repaired invalid test/config/secrets.json');
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const merged = {
|
|
66
|
+
...current,
|
|
67
|
+
test: {
|
|
68
|
+
...(current.test || {}),
|
|
69
|
+
keycloak: {
|
|
70
|
+
...defaults.test.keycloak,
|
|
71
|
+
...((current.test && current.test.keycloak) || {})
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Update only if required keys are missing.
|
|
77
|
+
const currentString = JSON.stringify(current);
|
|
78
|
+
const mergedString = JSON.stringify(merged);
|
|
79
|
+
if (currentString !== mergedString) {
|
|
80
|
+
writeJson(secretsPath, merged);
|
|
81
|
+
console.log('[ensure-test-config] Added missing keys in test/config/secrets.json');
|
|
82
|
+
} else {
|
|
83
|
+
console.log('[ensure-test-config] test/config/secrets.json already valid');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function ensureDockerEnv() {
|
|
88
|
+
const content = [
|
|
89
|
+
'KEYCLOAK_CERT_PATH=./certs',
|
|
90
|
+
'KEYCLOAK_HOSTNAME=localhost'
|
|
91
|
+
].join('\n') + '\n';
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(dockerEnvPath) || force) {
|
|
94
|
+
ensureDir(dockerEnvPath);
|
|
95
|
+
fs.writeFileSync(dockerEnvPath, content, 'utf8');
|
|
96
|
+
console.log(`[ensure-test-config] ${force ? 'Regenerated' : 'Created'} test/docker-keycloak/.env`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const existing = fs.readFileSync(dockerEnvPath, 'utf8');
|
|
101
|
+
if (!existing.includes('KEYCLOAK_CERT_PATH=') || !existing.includes('KEYCLOAK_HOSTNAME=')) {
|
|
102
|
+
fs.writeFileSync(dockerEnvPath, content, 'utf8');
|
|
103
|
+
console.log('[ensure-test-config] Repaired test/docker-keycloak/.env');
|
|
104
|
+
} else {
|
|
105
|
+
console.log('[ensure-test-config] test/docker-keycloak/.env already valid');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function main() {
|
|
110
|
+
ensureSecrets();
|
|
111
|
+
ensureDockerEnv();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
main();
|
package/oidc-methods.js
DELETED
|
@@ -1,327 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OIDC Authentication Methods for keycloak-express-middleware
|
|
3
|
-
*
|
|
4
|
-
* These methods provide OAuth2 OIDC token endpoint helpers:
|
|
5
|
-
* - generateAuthorizationUrl(): Generate PKCE authorization URL
|
|
6
|
-
* - loginWithCredentials(): Exchange credentials for tokens (generic OIDC grant)
|
|
7
|
-
* - loginPKCE(): Exchange authorization code for tokens (PKCE flow)
|
|
8
|
-
*
|
|
9
|
-
* Integration Instructions:
|
|
10
|
-
* 1. Copy these methods into the keycloakExpressMiddleware class in index.js
|
|
11
|
-
* 2. In the constructor, save the keycloakConfig:
|
|
12
|
-
* this.keycloakConfig = keycloakConfig;
|
|
13
|
-
* this.clientId = keycloakConfig.resource || keycloakOptions.clientId;
|
|
14
|
-
* this.clientSecret = keycloakConfig.credentials?.secret || keycloakOptions.clientSecret;
|
|
15
|
-
* 3. Run tests to verify: npm test
|
|
16
|
-
* 4. No external dependencies needed (crypto is built-in, fetch is global in Node 18+)
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const crypto = require('crypto');
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Helper: Base64url encode for PKCE
|
|
23
|
-
* @private
|
|
24
|
-
*/
|
|
25
|
-
function base64url(buffer) {
|
|
26
|
-
return buffer
|
|
27
|
-
.toString('base64')
|
|
28
|
-
.replace(/\+/g, '-')
|
|
29
|
-
.replace(/\//g, '_')
|
|
30
|
-
.replace(/=/g, '');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Generate Authorization URL + PKCE pair for initiating OAuth2 flow
|
|
35
|
-
*
|
|
36
|
-
* This method generates everything needed to start the PKCE flow:
|
|
37
|
-
* - Authorization URL with code_challenge and state
|
|
38
|
-
* - PKCE code_verifier (to exchange code later)
|
|
39
|
-
* - State parameter (for CSRF protection)
|
|
40
|
-
*
|
|
41
|
-
* Store state + codeVerifier in session server-side, redirect user to authUrl
|
|
42
|
-
*
|
|
43
|
-
* @param {Object} options - Configuration options
|
|
44
|
-
* @param {string} options.redirect_uri - Redirect URI (where user returns after login) - REQUIRED
|
|
45
|
-
* @param {string} options.redirectUri - CamelCase alias of redirect_uri
|
|
46
|
-
* @param {string} [options.scope] - Space-separated scopes (default: 'openid profile email')
|
|
47
|
-
* @param {string} [options.state] - Custom state value (auto-generated if not provided)
|
|
48
|
-
*
|
|
49
|
-
* @returns {Object} PKCE initialization data:
|
|
50
|
-
* - authUrl: Ready-to-use authorization URL
|
|
51
|
-
* - state: CSRF token (store in session)
|
|
52
|
-
* - codeVerifier: PKCE proof (store in session, never expose to client)
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* const pkceFlow = keycloakAdapter.generateAuthorizationUrl({
|
|
56
|
-
* redirect_uri: 'https://app.example.com/auth/callback'
|
|
57
|
-
* });
|
|
58
|
-
*
|
|
59
|
-
* // Output:
|
|
60
|
-
* // {
|
|
61
|
-
* // authUrl: 'https://keycloak.../auth?client_id=...&code_challenge=...',
|
|
62
|
-
* // state: 'random_state_value',
|
|
63
|
-
* // codeVerifier: 'random_verifier_value'
|
|
64
|
-
* // }
|
|
65
|
-
*
|
|
66
|
-
* // Store in session
|
|
67
|
-
* req.session.pkce_state = pkceFlow.state;
|
|
68
|
-
* req.session.pkce_verifier = pkceFlow.codeVerifier;
|
|
69
|
-
*
|
|
70
|
-
* // Redirect user to Keycloak
|
|
71
|
-
* res.redirect(pkceFlow.authUrl);
|
|
72
|
-
*/
|
|
73
|
-
function generateAuthorizationUrl(options = {}) {
|
|
74
|
-
if (!this.authServerUrl || !this.realmName || !this.clientId) {
|
|
75
|
-
throw new Error(
|
|
76
|
-
'generateAuthorizationUrl requires middleware to be initialized with ' +
|
|
77
|
-
'valid authServerUrl, realmName, and clientId'
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const {
|
|
82
|
-
redirect_uri,
|
|
83
|
-
redirectUri,
|
|
84
|
-
scope,
|
|
85
|
-
state: customState
|
|
86
|
-
} = options;
|
|
87
|
-
|
|
88
|
-
const resolvedRedirectUri = redirect_uri || redirectUri;
|
|
89
|
-
if (!resolvedRedirectUri) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
'generateAuthorizationUrl requires "redirect_uri" (or "redirectUri")'
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Generate PKCE pair
|
|
96
|
-
const codeVerifier = base64url(crypto.randomBytes(96));
|
|
97
|
-
const codeChallenge = base64url(
|
|
98
|
-
crypto.createHash('sha256').update(codeVerifier).digest()
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
// Generate or use provided state
|
|
102
|
-
const state = customState || base64url(crypto.randomBytes(32));
|
|
103
|
-
|
|
104
|
-
// Build authorization URL
|
|
105
|
-
const authUrl = new URL(
|
|
106
|
-
`${this.authServerUrl}realms/${this.realmName}/protocol/openid-connect/auth`
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
authUrl.searchParams.append('client_id', this.clientId);
|
|
110
|
-
authUrl.searchParams.append('response_type', 'code');
|
|
111
|
-
authUrl.searchParams.append('redirect_uri', resolvedRedirectUri);
|
|
112
|
-
authUrl.searchParams.append('code_challenge', codeChallenge);
|
|
113
|
-
authUrl.searchParams.append('code_challenge_method', 'S256');
|
|
114
|
-
authUrl.searchParams.append('state', state);
|
|
115
|
-
|
|
116
|
-
if (scope) {
|
|
117
|
-
authUrl.searchParams.append('scope', scope);
|
|
118
|
-
} else {
|
|
119
|
-
authUrl.searchParams.append('scope', 'openid profile email');
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
authUrl: authUrl.toString(),
|
|
124
|
-
state,
|
|
125
|
-
codeVerifier
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Exchange credentials for OIDC tokens (generic token endpoint helper)
|
|
131
|
-
*
|
|
132
|
-
* Supports any OAuth2 grant type:
|
|
133
|
-
* - password: Resource Owner Password Grant (username + password)
|
|
134
|
-
* - client_credentials: Client Credentials Grant
|
|
135
|
-
* - authorization_code: Authorization Code Grant (without PKCE)
|
|
136
|
-
* - refresh_token: Refresh Token Grant
|
|
137
|
-
*
|
|
138
|
-
* The method automatically appends clientId/clientSecret if configured and not overridden.
|
|
139
|
-
*
|
|
140
|
-
* @param {Object} credentials - OIDC token request parameters
|
|
141
|
-
* @param {string} credentials.grant_type - OAuth2 grant type (required)
|
|
142
|
-
* @param {string} [credentials.username] - Username (for password grant)
|
|
143
|
-
* @param {string} [credentials.password] - Password (for password grant)
|
|
144
|
-
* @param {string} [credentials.client_id] - Client ID (uses middleware config if not provided)
|
|
145
|
-
* @param {string} [credentials.client_secret] - Client secret (uses middleware config if not provided)
|
|
146
|
-
* @param {string} [credentials.refresh_token] - Refresh token (for refresh_token grant)
|
|
147
|
-
* @param {string} [credentials.code] - Authorization code (for authorization_code grant)
|
|
148
|
-
* @param {string} [credentials.redirect_uri] - Redirect URI (for authorization_code grant)
|
|
149
|
-
* @param {string} [credentials.scope] - OAuth2 scope
|
|
150
|
-
*
|
|
151
|
-
* @returns {Promise<Object>} Token response from Keycloak:
|
|
152
|
-
* - access_token: JWT access token
|
|
153
|
-
* - refresh_token: Refresh token (if configured)
|
|
154
|
-
* - id_token: ID token (if openid scope requested)
|
|
155
|
-
* - expires_in: Token expiration in seconds
|
|
156
|
-
* - token_type: Always "Bearer"
|
|
157
|
-
*
|
|
158
|
-
* @throws {Error} If token request fails
|
|
159
|
-
*
|
|
160
|
-
* @example
|
|
161
|
-
* // Resource Owner Password Grant
|
|
162
|
-
* const tokens = await keycloakAdapter.loginWithCredentials({
|
|
163
|
-
* grant_type: 'password',
|
|
164
|
-
* username: 'user@example.com',
|
|
165
|
-
* password: 'password123',
|
|
166
|
-
* scope: 'openid profile email'
|
|
167
|
-
* });
|
|
168
|
-
*
|
|
169
|
-
* @example
|
|
170
|
-
* // Client Credentials Grant
|
|
171
|
-
* const tokens = await keycloakAdapter.loginWithCredentials({
|
|
172
|
-
* grant_type: 'client_credentials',
|
|
173
|
-
* scope: 'openid profile'
|
|
174
|
-
* });
|
|
175
|
-
*
|
|
176
|
-
* @example
|
|
177
|
-
* // Refresh Token Grant
|
|
178
|
-
* const tokens = await keycloakAdapter.loginWithCredentials({
|
|
179
|
-
* grant_type: 'refresh_token',
|
|
180
|
-
* refresh_token: oldRefreshToken
|
|
181
|
-
* });
|
|
182
|
-
*/
|
|
183
|
-
async function loginWithCredentials(credentials = {}) {
|
|
184
|
-
if (!this.authServerUrl || !this.realmName) {
|
|
185
|
-
throw new Error(
|
|
186
|
-
'loginWithCredentials requires middleware to be initialized with valid authServerUrl and realmName'
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const body = new URLSearchParams();
|
|
191
|
-
|
|
192
|
-
// Add provided credentials
|
|
193
|
-
Object.entries(credentials).forEach(([key, value]) => {
|
|
194
|
-
if (value !== undefined && value !== null) {
|
|
195
|
-
body.append(key, String(value));
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// Add clientId if not already provided and configured
|
|
200
|
-
if (this.clientId && !body.has('client_id')) {
|
|
201
|
-
body.append('client_id', this.clientId);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Add clientSecret if not already provided and configured
|
|
205
|
-
if (this.clientSecret && !body.has('client_secret')) {
|
|
206
|
-
body.append('client_secret', this.clientSecret);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const tokenUrl = `${this.authServerUrl}realms/${this.realmName}/protocol/openid-connect/token`;
|
|
210
|
-
|
|
211
|
-
const response = await fetch(tokenUrl, {
|
|
212
|
-
method: 'POST',
|
|
213
|
-
headers: {
|
|
214
|
-
'content-type': 'application/x-www-form-urlencoded'
|
|
215
|
-
},
|
|
216
|
-
body
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const responseText = await response.text();
|
|
220
|
-
const payload = responseText ? JSON.parse(responseText) : {};
|
|
221
|
-
|
|
222
|
-
if (!response.ok) {
|
|
223
|
-
const errorMessage = payload.error_description || payload.error || 'Authentication failed';
|
|
224
|
-
throw new Error(errorMessage);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return payload;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Exchange authorization code + PKCE verifier for tokens (PKCE callback)
|
|
232
|
-
*
|
|
233
|
-
* This method is specialized for the callback route after user login.
|
|
234
|
-
* It exchanges the authorization code (from redirect) + code_verifier for tokens.
|
|
235
|
-
*
|
|
236
|
-
* @param {Object} credentials - Token exchange parameters
|
|
237
|
-
* @param {string} credentials.code - Authorization code (from Keycloak redirect) - REQUIRED
|
|
238
|
-
* @param {string} credentials.redirect_uri - Redirect URI (must match authorize request) - REQUIRED
|
|
239
|
-
* @param {string} credentials.redirectUri - CamelCase alias of redirect_uri
|
|
240
|
-
* @param {string} credentials.code_verifier - PKCE code verifier (from session) - REQUIRED
|
|
241
|
-
* @param {string} credentials.codeVerifier - CamelCase alias of code_verifier
|
|
242
|
-
* @param {string} [credentials.client_id] - Client ID (uses middleware config if not provided)
|
|
243
|
-
* @param {string} [credentials.clientId] - CamelCase alias of client_id
|
|
244
|
-
* @param {string} [credentials.client_secret] - Client secret (uses middleware config if not provided)
|
|
245
|
-
* @param {string} [credentials.clientSecret] - CamelCase alias of client_secret
|
|
246
|
-
* @param {string} [credentials.scope] - Additional scope string
|
|
247
|
-
*
|
|
248
|
-
* @returns {Promise<Object>} Token response from Keycloak (same as loginWithCredentials())
|
|
249
|
-
*
|
|
250
|
-
* @throws {Error} If any required parameter is missing or token exchange fails
|
|
251
|
-
*
|
|
252
|
-
* @example
|
|
253
|
-
* app.get('/auth/callback', async (req, res) => {
|
|
254
|
-
* const { code, state } = req.query;
|
|
255
|
-
*
|
|
256
|
-
* // Validate state (CSRF protection)
|
|
257
|
-
* if (state !== req.session.pkce_state) {
|
|
258
|
-
* return res.status(400).send('CSRF attack detected');
|
|
259
|
-
* }
|
|
260
|
-
*
|
|
261
|
-
* try {
|
|
262
|
-
* // Exchange code for tokens
|
|
263
|
-
* const tokens = await keycloakAdapter.loginPKCE({
|
|
264
|
-
* code,
|
|
265
|
-
* redirect_uri: 'https://app.example.com/auth/callback',
|
|
266
|
-
* code_verifier: req.session.pkce_verifier
|
|
267
|
-
* });
|
|
268
|
-
*
|
|
269
|
-
* // Set secure cookies
|
|
270
|
-
* res.cookie('access_token', tokens.access_token, {
|
|
271
|
-
* httpOnly: true,
|
|
272
|
-
* secure: true,
|
|
273
|
-
* sameSite: 'strict'
|
|
274
|
-
* });
|
|
275
|
-
*
|
|
276
|
-
* res.redirect('/dashboard');
|
|
277
|
-
* } catch (error) {
|
|
278
|
-
* res.status(401).send('Authentication failed');
|
|
279
|
-
* }
|
|
280
|
-
* });
|
|
281
|
-
*/
|
|
282
|
-
async function loginPKCE(credentials = {}) {
|
|
283
|
-
const {
|
|
284
|
-
code,
|
|
285
|
-
redirect_uri,
|
|
286
|
-
redirectUri,
|
|
287
|
-
code_verifier,
|
|
288
|
-
codeVerifier,
|
|
289
|
-
client_id,
|
|
290
|
-
clientId,
|
|
291
|
-
client_secret,
|
|
292
|
-
clientSecret,
|
|
293
|
-
...rest
|
|
294
|
-
} = credentials;
|
|
295
|
-
|
|
296
|
-
const resolvedCode = code;
|
|
297
|
-
const resolvedRedirectUri = redirect_uri || redirectUri;
|
|
298
|
-
const resolvedCodeVerifier = code_verifier || codeVerifier;
|
|
299
|
-
const resolvedClientId = client_id || clientId;
|
|
300
|
-
const resolvedClientSecret = client_secret || clientSecret;
|
|
301
|
-
|
|
302
|
-
if (!resolvedCode) {
|
|
303
|
-
throw new Error('loginPKCE requires "code".');
|
|
304
|
-
}
|
|
305
|
-
if (!resolvedRedirectUri) {
|
|
306
|
-
throw new Error('loginPKCE requires "redirect_uri" (or "redirectUri").');
|
|
307
|
-
}
|
|
308
|
-
if (!resolvedCodeVerifier) {
|
|
309
|
-
throw new Error('loginPKCE requires "code_verifier" (or "codeVerifier").');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return this.loginWithCredentials({
|
|
313
|
-
grant_type: 'authorization_code',
|
|
314
|
-
code: resolvedCode,
|
|
315
|
-
redirect_uri: resolvedRedirectUri,
|
|
316
|
-
code_verifier: resolvedCodeVerifier,
|
|
317
|
-
...(resolvedClientId ? { client_id: resolvedClientId } : {}),
|
|
318
|
-
...(resolvedClientSecret ? { client_secret: resolvedClientSecret } : {}),
|
|
319
|
-
...rest
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
module.exports = {
|
|
324
|
-
generateAuthorizationUrl,
|
|
325
|
-
loginWithCredentials,
|
|
326
|
-
loginPKCE
|
|
327
|
-
};
|