keycloak-express-middleware 6.1.2 → 6.2.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/CHANGELOG.md +54 -0
- package/OIDC_INTEGRATION_GUIDE.md +3 -0
- package/README.md +235 -0
- package/docs/OIDC_INTEGRATION_GUIDE.md +8 -0
- package/index.d.ts +125 -0
- package/index.js +163 -0
- package/package.json +5 -16
- package/test/config/secrets.json +9 -0
- package/test/docker-keycloak/.env +2 -0
- package/test/middleware-functions.test.js +612 -0
- package/test/package-lock.json +671 -335
- package/test/package.json +7 -5
- package/.idea/keycloak-express-middleware.iml +0 -12
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## [6.2.0] - 2026-03-18
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Helper Utilities for Auth & Scope Management** — New imperative methods to simplify common token/scope operations:
|
|
9
|
+
- `getTokenClaims(req)` — Extract decoded JWT claims from request
|
|
10
|
+
- `isAuthenticated(req)` — Check if user's access token is valid
|
|
11
|
+
- `getScopes(scopeInputOrReq)` — Normalize and retrieve scope list from string, array, or request
|
|
12
|
+
- `hasScopeFromRequest(req, requiredScope)` — Check single scope from request token
|
|
13
|
+
- `hasScopesFromRequest(req, requiredScopes, mode)` — Check multiple scopes from request token (all/any mode)
|
|
14
|
+
- `requireScopes(requiredScopes, mode)` — Middleware to enforce scope requirements (returns 403 JSON if missing)
|
|
15
|
+
- **TypeScript Definitions** (`index.d.ts`) — Type coverage for all exported functions and middleware
|
|
16
|
+
- **CHANGELOG.md** — Centralized changelog for release tracking
|
|
17
|
+
- Comprehensive test coverage for all new methods (57 tests passing)
|
|
18
|
+
|
|
19
|
+
### Documentation
|
|
20
|
+
- Updated README with new API documentation and examples
|
|
21
|
+
- Clarified Direct Access Grants prerequisites for password grant flow
|
|
22
|
+
- Added OIDC integration guide notes
|
|
23
|
+
|
|
24
|
+
### Dependencies
|
|
25
|
+
- `express-session` ^1.19.0
|
|
26
|
+
- `keycloak-connect` ^26.1.1
|
|
27
|
+
|
|
28
|
+
## [6.1.3] - 2026-03-18
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
- Scope checking utilities (`hasScope`, `hasScopes`) for imperative token validation
|
|
32
|
+
- Direct Access Grants documentation and clarification in guides
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- Test workspace installation and execution flow
|
|
36
|
+
- Package.json runtime dependencies minimized to reduce surface area
|
|
37
|
+
|
|
38
|
+
## [6.1.0] — [6.1.2]
|
|
39
|
+
|
|
40
|
+
See GitHub releases for detailed history.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Versioning
|
|
45
|
+
|
|
46
|
+
This project follows [Semantic Versioning](https://semver.org/):
|
|
47
|
+
- **MAJOR** (X.0.0): Breaking API changes, major feature overhauls
|
|
48
|
+
- **MINOR** (0.X.0): New features, helper utilities, backward compatible
|
|
49
|
+
- **PATCH** (0.0.X): Bug fixes, docs, internal improvements
|
|
50
|
+
|
|
51
|
+
## Migration Guides
|
|
52
|
+
|
|
53
|
+
- See [OIDC_INTEGRATION_GUIDE.md](./docs/OIDC_INTEGRATION_GUIDE.md) for OpenID Connect setup
|
|
54
|
+
- See [README.md](./README.md) for API reference and examples
|
|
@@ -17,6 +17,9 @@ Important alignment notes with current implementation:
|
|
|
17
17
|
- Use a middleware instance created with:
|
|
18
18
|
new keycloackAdapter(app, keyCloackConfig, keyCloackOptions)
|
|
19
19
|
- For generic token endpoint exchange use loginWithCredentials, not login.
|
|
20
|
+
- If loginWithCredentials is used with grant_type=password, enable Direct Access Grants on the client.
|
|
21
|
+
- This is OAuth2 Resource Owner Password Credentials Grant support for that client.
|
|
22
|
+
- loginPKCE (authorization_code + code_verifier) does not require Direct Access Grants.
|
|
20
23
|
|
|
21
24
|
Quick verification command:
|
|
22
25
|
|
package/README.md
CHANGED
|
@@ -35,6 +35,14 @@ It is based on **'keycloak-connect'** and **'express-session'**.
|
|
|
35
35
|
- [API - loginWithCredentials](#api---loginwithcredentialscredentials)
|
|
36
36
|
- [API - loginPKCE](#api---loginpkcecredentials)
|
|
37
37
|
- [API - redirectToUserAccountConsole](#api---redirecttouseraccountconsoleres)
|
|
38
|
+
- [API - hasScope](#api---hasscopescopeinput-requiredscope)
|
|
39
|
+
- [API - hasScopes](#api---hasscopesscopeinput-requiredscopes-mode)
|
|
40
|
+
- [API - getTokenClaims](#api---gettokenclaimsreq)
|
|
41
|
+
- [API - isAuthenticated](#api---isauthenticatedreq)
|
|
42
|
+
- [API - getScopes](#api---getscopesscopeinputorreq)
|
|
43
|
+
- [API - hasScopeFromRequest](#api---hasscopefromrequestreq-requiredscope)
|
|
44
|
+
- [API - hasScopesFromRequest](#api---hasscopesfromrequestreq-requiredscopes-mode)
|
|
45
|
+
- [API - requireScopes](#api---requirescopesrequiredscopes-mode)
|
|
38
46
|
- [Handling Unauthorized Access (401/403) Gracefully](#handling-unauthorized-access-401403-gracefully)
|
|
39
47
|
- [Testing Documentation](#testing-documentation)
|
|
40
48
|
- [License](#license)
|
|
@@ -1039,6 +1047,10 @@ What `loginPKCE(...)` actually does:
|
|
|
1039
1047
|
- Calls the token endpoint with grant type `authorization_code` and PKCE verifier.
|
|
1040
1048
|
- Returns token payload for your own persistence/session strategy.
|
|
1041
1049
|
|
|
1050
|
+
Client configuration note for PKCE:
|
|
1051
|
+
|
|
1052
|
+
- `loginPKCE(...)` uses Authorization Code + PKCE and does **not** require `Direct Access Grants`.
|
|
1053
|
+
|
|
1042
1054
|
Use `loginWithCredentials(...)` when:
|
|
1043
1055
|
|
|
1044
1056
|
- You need direct token endpoint operations (e.g., refresh token, client credentials, custom grant handling).
|
|
@@ -1050,6 +1062,12 @@ What `loginWithCredentials(...)` actually does:
|
|
|
1050
1062
|
- Supports multiple grant models by payload (`password`, `client_credentials`, `authorization_code`, `refresh_token`).
|
|
1051
1063
|
- Returns raw token endpoint response or throws error on failure.
|
|
1052
1064
|
|
|
1065
|
+
Client configuration note for non-browser password login:
|
|
1066
|
+
|
|
1067
|
+
- When `loginWithCredentials(...)` is used with `grant_type=password`, the client must have `Direct Access Grants` enabled in Keycloak.
|
|
1068
|
+
- This means the client handles username/password directly and exchanges them with Keycloak token endpoint.
|
|
1069
|
+
- In OAuth2 terms, this enables `Resource Owner Password Credentials Grant` support for that client.
|
|
1070
|
+
|
|
1053
1071
|
Recommended PKCE sequence:
|
|
1054
1072
|
|
|
1055
1073
|
1. Start flow with `generateAuthorizationUrl(...)` and persist `state` + `codeVerifier` server-side.
|
|
@@ -1211,6 +1229,12 @@ Generic OAuth2 token endpoint helper supporting multiple grant types.
|
|
|
1211
1229
|
- `authorization_code`
|
|
1212
1230
|
- `refresh_token`
|
|
1213
1231
|
|
|
1232
|
+
**Client configuration requirement (`password` grant only)**
|
|
1233
|
+
|
|
1234
|
+
- If you call `loginWithCredentials(...)` with `grant_type=password`, the Keycloak client must have `Direct Access Grants` enabled.
|
|
1235
|
+
- This corresponds to OAuth2 `Resource Owner Password Credentials Grant` for that client.
|
|
1236
|
+
- This requirement does not apply to `client_credentials`, `refresh_token`, or `authorization_code` payloads.
|
|
1237
|
+
|
|
1214
1238
|
**Parameters**
|
|
1215
1239
|
|
|
1216
1240
|
| Name | Type | Required | Description |
|
|
@@ -1262,6 +1286,7 @@ Performs authorization-code + PKCE verifier exchange.
|
|
|
1262
1286
|
|
|
1263
1287
|
- PKCE mitigates intercepted authorization code reuse by binding code to `code_verifier`.
|
|
1264
1288
|
- This method encapsulates the correct grant payload shape for PKCE callback stage.
|
|
1289
|
+
- PKCE callback exchange does not require `Direct Access Grants`.
|
|
1265
1290
|
|
|
1266
1291
|
**Parameters**
|
|
1267
1292
|
|
|
@@ -1328,6 +1353,216 @@ app.get('/my-account', (req, res) => {
|
|
|
1328
1353
|
});
|
|
1329
1354
|
```
|
|
1330
1355
|
|
|
1356
|
+
#### API - hasScope(scopeInput, requiredScope)
|
|
1357
|
+
|
|
1358
|
+
**Signature**
|
|
1359
|
+
|
|
1360
|
+
```js
|
|
1361
|
+
hasScope(scopeInput, requiredScope)
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
Checks whether one scope is present in a scope string or array.
|
|
1365
|
+
|
|
1366
|
+
**What this API is for**
|
|
1367
|
+
|
|
1368
|
+
- Use this helper to check scopes without rewriting parsing logic in every route.
|
|
1369
|
+
- Works with both middleware token claims and token endpoint responses.
|
|
1370
|
+
|
|
1371
|
+
**Parameters**
|
|
1372
|
+
|
|
1373
|
+
| Name | Type | Required | Description |
|
|
1374
|
+
|---|---|---|---|
|
|
1375
|
+
| `scopeInput` | `string \| string[]` | Yes | Scope source (`"openid profile email"` or `['openid', 'profile']`). |
|
|
1376
|
+
| `requiredScope` | `string` | Yes | Scope to verify. |
|
|
1377
|
+
|
|
1378
|
+
**Returns**
|
|
1379
|
+
|
|
1380
|
+
- `boolean`: `true` if scope exists, otherwise `false`.
|
|
1381
|
+
|
|
1382
|
+
**Example**
|
|
1383
|
+
|
|
1384
|
+
```js
|
|
1385
|
+
const tokenScope = req?.kauth?.grant?.access_token?.content?.scope;
|
|
1386
|
+
const canReadEmail = keycloakInstance.hasScope(tokenScope, 'email');
|
|
1387
|
+
```
|
|
1388
|
+
|
|
1389
|
+
#### API - hasScopes(scopeInput, requiredScopes, mode)
|
|
1390
|
+
|
|
1391
|
+
**Signature**
|
|
1392
|
+
|
|
1393
|
+
```js
|
|
1394
|
+
hasScopes(scopeInput, requiredScopes, mode = 'all')
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
Checks multiple scopes with `all` (default) or `any` matching mode.
|
|
1398
|
+
|
|
1399
|
+
**Parameters**
|
|
1400
|
+
|
|
1401
|
+
| Name | Type | Required | Description |
|
|
1402
|
+
|---|---|---|---|
|
|
1403
|
+
| `scopeInput` | `string \| string[]` | Yes | Scope source (`"openid profile email"` or array). |
|
|
1404
|
+
| `requiredScopes` | `string \| string[]` | Yes | One or more scopes to evaluate. |
|
|
1405
|
+
| `mode` | `'all' \| 'any'` | No | `all` requires all scopes, `any` requires at least one. |
|
|
1406
|
+
|
|
1407
|
+
**Returns**
|
|
1408
|
+
|
|
1409
|
+
- `boolean`: scope validation result.
|
|
1410
|
+
|
|
1411
|
+
**Example**
|
|
1412
|
+
|
|
1413
|
+
```js
|
|
1414
|
+
const scopeString = tokenResponse.scope;
|
|
1415
|
+
|
|
1416
|
+
const hasAll = keycloakInstance.hasScopes(
|
|
1417
|
+
scopeString,
|
|
1418
|
+
['openid', 'profile'],
|
|
1419
|
+
'all'
|
|
1420
|
+
);
|
|
1421
|
+
|
|
1422
|
+
const hasAny = keycloakInstance.hasScopes(
|
|
1423
|
+
scopeString,
|
|
1424
|
+
['email', 'offline_access'],
|
|
1425
|
+
'any'
|
|
1426
|
+
);
|
|
1427
|
+
```
|
|
1428
|
+
|
|
1429
|
+
#### API - getTokenClaims(req)
|
|
1430
|
+
|
|
1431
|
+
**Signature**
|
|
1432
|
+
|
|
1433
|
+
```js
|
|
1434
|
+
getTokenClaims(req)
|
|
1435
|
+
```
|
|
1436
|
+
|
|
1437
|
+
Safely returns decoded access token claims from request.
|
|
1438
|
+
|
|
1439
|
+
**Returns**
|
|
1440
|
+
|
|
1441
|
+
- `Object`: token claims object, or `{}` if token is unavailable.
|
|
1442
|
+
|
|
1443
|
+
**Example**
|
|
1444
|
+
|
|
1445
|
+
```js
|
|
1446
|
+
const claims = keycloakInstance.getTokenClaims(req);
|
|
1447
|
+
const username = claims.preferred_username;
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
#### API - isAuthenticated(req)
|
|
1451
|
+
|
|
1452
|
+
**Signature**
|
|
1453
|
+
|
|
1454
|
+
```js
|
|
1455
|
+
isAuthenticated(req)
|
|
1456
|
+
```
|
|
1457
|
+
|
|
1458
|
+
Checks whether request contains a Keycloak access token.
|
|
1459
|
+
|
|
1460
|
+
**Returns**
|
|
1461
|
+
|
|
1462
|
+
- `boolean`.
|
|
1463
|
+
|
|
1464
|
+
**Example**
|
|
1465
|
+
|
|
1466
|
+
```js
|
|
1467
|
+
if (!keycloakInstance.isAuthenticated(req)) {
|
|
1468
|
+
return res.status(401).send('Not authenticated');
|
|
1469
|
+
}
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
#### API - getScopes(scopeInputOrReq)
|
|
1473
|
+
|
|
1474
|
+
**Signature**
|
|
1475
|
+
|
|
1476
|
+
```js
|
|
1477
|
+
getScopes(scopeInputOrReq)
|
|
1478
|
+
```
|
|
1479
|
+
|
|
1480
|
+
Normalizes scopes to an array from one of:
|
|
1481
|
+
|
|
1482
|
+
- scope string
|
|
1483
|
+
- scope array
|
|
1484
|
+
- Express request (`req.kauth.grant.access_token.content.scope`)
|
|
1485
|
+
|
|
1486
|
+
**Returns**
|
|
1487
|
+
|
|
1488
|
+
- `string[]`.
|
|
1489
|
+
|
|
1490
|
+
**Example**
|
|
1491
|
+
|
|
1492
|
+
```js
|
|
1493
|
+
const scopes = keycloakInstance.getScopes(req);
|
|
1494
|
+
// ['openid', 'profile', 'email']
|
|
1495
|
+
```
|
|
1496
|
+
|
|
1497
|
+
#### API - hasScopeFromRequest(req, requiredScope)
|
|
1498
|
+
|
|
1499
|
+
**Signature**
|
|
1500
|
+
|
|
1501
|
+
```js
|
|
1502
|
+
hasScopeFromRequest(req, requiredScope)
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
Convenience wrapper around `hasScope(...)` using request token claims.
|
|
1506
|
+
|
|
1507
|
+
**Returns**
|
|
1508
|
+
|
|
1509
|
+
- `boolean`.
|
|
1510
|
+
|
|
1511
|
+
**Example**
|
|
1512
|
+
|
|
1513
|
+
```js
|
|
1514
|
+
const canReadEmail = keycloakInstance.hasScopeFromRequest(req, 'email');
|
|
1515
|
+
```
|
|
1516
|
+
|
|
1517
|
+
#### API - hasScopesFromRequest(req, requiredScopes, mode)
|
|
1518
|
+
|
|
1519
|
+
**Signature**
|
|
1520
|
+
|
|
1521
|
+
```js
|
|
1522
|
+
hasScopesFromRequest(req, requiredScopes, mode = 'all')
|
|
1523
|
+
```
|
|
1524
|
+
|
|
1525
|
+
Convenience wrapper around `hasScopes(...)` using request token claims.
|
|
1526
|
+
|
|
1527
|
+
**Returns**
|
|
1528
|
+
|
|
1529
|
+
- `boolean`.
|
|
1530
|
+
|
|
1531
|
+
**Example**
|
|
1532
|
+
|
|
1533
|
+
```js
|
|
1534
|
+
const allowed = keycloakInstance.hasScopesFromRequest(
|
|
1535
|
+
req,
|
|
1536
|
+
['openid', 'profile'],
|
|
1537
|
+
'all'
|
|
1538
|
+
);
|
|
1539
|
+
```
|
|
1540
|
+
|
|
1541
|
+
#### API - requireScopes(requiredScopes, mode)
|
|
1542
|
+
|
|
1543
|
+
**Signature**
|
|
1544
|
+
|
|
1545
|
+
```js
|
|
1546
|
+
requireScopes(requiredScopes, mode = 'all')
|
|
1547
|
+
```
|
|
1548
|
+
|
|
1549
|
+
Express middleware that enforces scopes and responds `403` if missing.
|
|
1550
|
+
|
|
1551
|
+
**Returns**
|
|
1552
|
+
|
|
1553
|
+
- Middleware return.
|
|
1554
|
+
|
|
1555
|
+
**Example**
|
|
1556
|
+
|
|
1557
|
+
```js
|
|
1558
|
+
app.get(
|
|
1559
|
+
'/profile-email',
|
|
1560
|
+
keycloakInstance.protectMiddleware(),
|
|
1561
|
+
keycloakInstance.requireScopes(['email'], 'all'),
|
|
1562
|
+
(req, res) => res.send('Scope check passed')
|
|
1563
|
+
);
|
|
1564
|
+
```
|
|
1565
|
+
|
|
1331
1566
|
---
|
|
1332
1567
|
## Handling Unauthorized Access (401/403) Gracefully
|
|
1333
1568
|
|
|
@@ -7,6 +7,14 @@ This guide explains how to integrate the OIDC authentication methods (`generateA
|
|
|
7
7
|
- **`oidc-methods.js`** - Ready-to-use OIDC methods (no external dependencies)
|
|
8
8
|
- **`test/oidc-methods.test.js`** - Complete test suite (run with `npm test`)
|
|
9
9
|
|
|
10
|
+
## Client Prerequisites (Non-Browser Flows)
|
|
11
|
+
|
|
12
|
+
- `loginWithCredentials(credentials)` is a generic token endpoint helper.
|
|
13
|
+
- If you use it with `grant_type=password`, the Keycloak client must have `Direct Access Grants` enabled.
|
|
14
|
+
- This means the client exchanges user username/password directly with Keycloak token endpoint.
|
|
15
|
+
- In OAuth2 specification terms, this enables `Resource Owner Password Credentials Grant` support for that client.
|
|
16
|
+
- `loginPKCE(credentials)` uses `authorization_code` + `code_verifier` and does not require `Direct Access Grants`.
|
|
17
|
+
|
|
10
18
|
## Integration Steps
|
|
11
19
|
|
|
12
20
|
### Step 1: Run Tests (Verify Methods Work)
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main Keycloak middleware adapter for Express applications
|
|
5
|
+
*/
|
|
6
|
+
declare module 'keycloak-express-middleware' {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Token claims structure (decoded JWT)
|
|
10
|
+
*/
|
|
11
|
+
interface TokenClaims {
|
|
12
|
+
sub?: string;
|
|
13
|
+
aud?: string[];
|
|
14
|
+
iss?: string;
|
|
15
|
+
exp?: number;
|
|
16
|
+
iat?: number;
|
|
17
|
+
scope?: string;
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Keycloak adapter instance
|
|
23
|
+
*/
|
|
24
|
+
interface KeycloakAdapter {
|
|
25
|
+
config?: any;
|
|
26
|
+
grant?: any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Scope validation modes
|
|
31
|
+
*/
|
|
32
|
+
type ScopeMode = 'all' | 'any';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Main Keycloak class constructor options
|
|
36
|
+
*/
|
|
37
|
+
interface KeycloakOptions {
|
|
38
|
+
realm?: string;
|
|
39
|
+
bearer_only?: boolean;
|
|
40
|
+
ssl_required?: string;
|
|
41
|
+
resource?: string;
|
|
42
|
+
credentials?: {
|
|
43
|
+
secret?: string;
|
|
44
|
+
};
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Main Keycloak middleware class
|
|
50
|
+
*/
|
|
51
|
+
class Keycloak {
|
|
52
|
+
constructor(options?: KeycloakOptions);
|
|
53
|
+
|
|
54
|
+
// Middleware factory methods
|
|
55
|
+
middleware(): (req: Request, res: Response, next: NextFunction) => void;
|
|
56
|
+
protect(spec?: string | boolean | ((req: Request, res: Response) => boolean | string)): (req: Request, res: Response, next: NextFunction) => void;
|
|
57
|
+
enforcer(spec?: string | ((req: Request, res: Response) => boolean)): (req: Request, res: Response, next: NextFunction) => void;
|
|
58
|
+
|
|
59
|
+
// Login/Logout methods
|
|
60
|
+
login(): (req: Request, res: Response, next: NextFunction) => void;
|
|
61
|
+
loginMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
|
|
62
|
+
logout(): (req: Request, res: Response, next: NextFunction) => void;
|
|
63
|
+
logoutMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
|
|
64
|
+
redirectToUserAccountConsole(): (req: Request, res: Response, next: NextFunction) => void;
|
|
65
|
+
|
|
66
|
+
// OIDC methods
|
|
67
|
+
generateAuthorizationUrl(options: {
|
|
68
|
+
redirect_uri: string;
|
|
69
|
+
redirectUri?: string;
|
|
70
|
+
scope?: string;
|
|
71
|
+
state?: string;
|
|
72
|
+
}): { authorization_url: string; code_verifier: string; code_challenge: string };
|
|
73
|
+
|
|
74
|
+
loginWithCredentials(credentials: {
|
|
75
|
+
username?: string;
|
|
76
|
+
password?: string;
|
|
77
|
+
clientId?: string;
|
|
78
|
+
client_id?: string;
|
|
79
|
+
clientSecret?: string;
|
|
80
|
+
client_secret?: string;
|
|
81
|
+
}): Promise<any>;
|
|
82
|
+
|
|
83
|
+
loginPKCE(options: {
|
|
84
|
+
code: string;
|
|
85
|
+
redirect_uri: string;
|
|
86
|
+
redirectUri?: string;
|
|
87
|
+
code_verifier: string;
|
|
88
|
+
codeVerifier?: string;
|
|
89
|
+
}): Promise<any>;
|
|
90
|
+
|
|
91
|
+
// Scope helpers
|
|
92
|
+
hasScope(scopeInput: string | string[], requiredScope: string): boolean;
|
|
93
|
+
hasScopes(scopeInput: string | string[], requiredScopes: string[], mode?: ScopeMode): boolean;
|
|
94
|
+
getTokenClaims(req: Request): TokenClaims;
|
|
95
|
+
isAuthenticated(req: Request): boolean;
|
|
96
|
+
getScopes(scopeInputOrReq: string | string[] | Request): string[];
|
|
97
|
+
hasScopeFromRequest(req: Request, requiredScope: string): boolean;
|
|
98
|
+
hasScopesFromRequest(req: Request, requiredScopes: string[], mode?: ScopeMode): boolean;
|
|
99
|
+
requireScopes(requiredScopes: string[], mode?: ScopeMode): (req: Request, res: Response, next: NextFunction) => void;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default Keycloak;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Factory function to create Keycloak instance
|
|
106
|
+
*/
|
|
107
|
+
function keycloakExpress(options?: KeycloakOptions): Keycloak;
|
|
108
|
+
|
|
109
|
+
export { keycloakExpress };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Express Request extension for Keycloak properties
|
|
114
|
+
*/
|
|
115
|
+
declare global {
|
|
116
|
+
namespace Express {
|
|
117
|
+
interface Request {
|
|
118
|
+
kauth?: {
|
|
119
|
+
grant?: any;
|
|
120
|
+
};
|
|
121
|
+
encodedTokenRole?: string;
|
|
122
|
+
hasPermission?: (permission: string) => boolean;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
package/index.js
CHANGED
|
@@ -863,6 +863,12 @@ class keycloakExpressMiddleware {
|
|
|
863
863
|
* - client_credentials: Client Credentials Grant
|
|
864
864
|
* - authorization_code: Authorization Code Grant (without PKCE)
|
|
865
865
|
* - refresh_token: Refresh Token Grant
|
|
866
|
+
*
|
|
867
|
+
* Client prerequisite note:
|
|
868
|
+
* - When using `grant_type=password`, Keycloak client must have Direct Access Grants enabled.
|
|
869
|
+
* - This means the client exchanges user username/password directly with Keycloak token endpoint.
|
|
870
|
+
* - In OAuth2 terms, this is Resource Owner Password Credentials Grant support for that client.
|
|
871
|
+
* - This prerequisite does not apply to `client_credentials`, `refresh_token`, or `authorization_code` payloads.
|
|
866
872
|
*
|
|
867
873
|
* The method automatically appends clientId/clientSecret if configured and not overridden.
|
|
868
874
|
*
|
|
@@ -932,6 +938,10 @@ class keycloakExpressMiddleware {
|
|
|
932
938
|
*
|
|
933
939
|
* This method is specialized for the callback route after user login.
|
|
934
940
|
* It exchanges the authorization code (from redirect) + code_verifier for tokens.
|
|
941
|
+
*
|
|
942
|
+
* Client prerequisite note:
|
|
943
|
+
* - PKCE uses `authorization_code` + `code_verifier` and does not require Direct Access Grants.
|
|
944
|
+
* - Direct Access Grants is only required when using `grant_type=password` (ROPC) in loginWithCredentials.
|
|
935
945
|
*
|
|
936
946
|
* @param {Object} credentials - Token exchange parameters
|
|
937
947
|
* @param {string} credentials.code - Authorization code (from Keycloak redirect) - REQUIRED
|
|
@@ -995,6 +1005,159 @@ class keycloakExpressMiddleware {
|
|
|
995
1005
|
res.redirect(redirectUrl);
|
|
996
1006
|
}
|
|
997
1007
|
|
|
1008
|
+
/**
|
|
1009
|
+
* Check whether a scope string (or scope array) contains a specific scope.
|
|
1010
|
+
*
|
|
1011
|
+
* Scope input can come from:
|
|
1012
|
+
* - `req.kauth.grant.access_token.content.scope` (middleware/browser flow)
|
|
1013
|
+
* - `tokenResponse.scope` (loginWithCredentials/loginPKCE response)
|
|
1014
|
+
*
|
|
1015
|
+
* @param {string|string[]} scopeInput - Scope string (space-separated) or scope array.
|
|
1016
|
+
* @param {string} requiredScope - Scope to verify.
|
|
1017
|
+
* @returns {boolean} True if required scope is present, false otherwise.
|
|
1018
|
+
*/
|
|
1019
|
+
hasScope(scopeInput, requiredScope){
|
|
1020
|
+
if (!requiredScope || typeof requiredScope !== 'string') return false;
|
|
1021
|
+
|
|
1022
|
+
const scopes = Array.isArray(scopeInput)
|
|
1023
|
+
? scopeInput
|
|
1024
|
+
: String(scopeInput || '').split(' ');
|
|
1025
|
+
|
|
1026
|
+
return scopes
|
|
1027
|
+
.map((s) => String(s || '').trim())
|
|
1028
|
+
.filter(Boolean)
|
|
1029
|
+
.includes(requiredScope);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
/**
|
|
1033
|
+
* Check whether a scope input contains all (default) or any required scopes.
|
|
1034
|
+
*
|
|
1035
|
+
* @param {string|string[]} scopeInput - Scope string (space-separated) or scope array.
|
|
1036
|
+
* @param {string|string[]} requiredScopes - One scope or list of scopes to verify.
|
|
1037
|
+
* @param {'all'|'any'} [mode='all'] - `all`: every scope required, `any`: at least one required.
|
|
1038
|
+
* @returns {boolean} Scope check result according to selected mode.
|
|
1039
|
+
*/
|
|
1040
|
+
hasScopes(scopeInput, requiredScopes, mode = 'all'){
|
|
1041
|
+
const targets = Array.isArray(requiredScopes) ? requiredScopes : [requiredScopes];
|
|
1042
|
+
const normalizedTargets = targets
|
|
1043
|
+
.map((s) => String(s || '').trim())
|
|
1044
|
+
.filter(Boolean);
|
|
1045
|
+
|
|
1046
|
+
if (normalizedTargets.length === 0) return false;
|
|
1047
|
+
|
|
1048
|
+
if (mode === 'any') {
|
|
1049
|
+
return normalizedTargets.some((scope) => this.hasScope(scopeInput, scope));
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return normalizedTargets.every((scope) => this.hasScope(scopeInput, scope));
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/**
|
|
1056
|
+
* Safely read decoded access token claims from an Express request.
|
|
1057
|
+
*
|
|
1058
|
+
* @param {Object} req - Express request object.
|
|
1059
|
+
* @returns {Object} Decoded access token claims, or empty object when unavailable.
|
|
1060
|
+
*/
|
|
1061
|
+
getTokenClaims(req){
|
|
1062
|
+
return req?.kauth?.grant?.access_token?.content || {};
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* Check whether request contains a Keycloak-authenticated access token.
|
|
1067
|
+
*
|
|
1068
|
+
* @param {Object} req - Express request object.
|
|
1069
|
+
* @returns {boolean} True when access token is present, false otherwise.
|
|
1070
|
+
*/
|
|
1071
|
+
isAuthenticated(req){
|
|
1072
|
+
return !!req?.kauth?.grant?.access_token;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
/**
|
|
1076
|
+
* Normalize scopes into an array.
|
|
1077
|
+
*
|
|
1078
|
+
* Input accepted:
|
|
1079
|
+
* - space-separated scope string
|
|
1080
|
+
* - scope array
|
|
1081
|
+
* - Express request object (reads `req.kauth.grant.access_token.content.scope`)
|
|
1082
|
+
*
|
|
1083
|
+
* @param {string|string[]|Object} scopeInputOrReq - Scope source string/array or Express request.
|
|
1084
|
+
* @returns {string[]} Normalized scope array.
|
|
1085
|
+
*/
|
|
1086
|
+
getScopes(scopeInputOrReq){
|
|
1087
|
+
const looksLikeReq = scopeInputOrReq && typeof scopeInputOrReq === 'object' && ('kauth' in scopeInputOrReq || 'headers' in scopeInputOrReq || 'method' in scopeInputOrReq);
|
|
1088
|
+
const rawScope = looksLikeReq
|
|
1089
|
+
? this.getTokenClaims(scopeInputOrReq).scope
|
|
1090
|
+
: scopeInputOrReq;
|
|
1091
|
+
|
|
1092
|
+
const scopes = Array.isArray(rawScope)
|
|
1093
|
+
? rawScope
|
|
1094
|
+
: String(rawScope || '').split(' ');
|
|
1095
|
+
|
|
1096
|
+
return scopes
|
|
1097
|
+
.map((s) => String(s || '').trim())
|
|
1098
|
+
.filter(Boolean);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Check scope directly from Express request token claims.
|
|
1103
|
+
*
|
|
1104
|
+
* @param {Object} req - Express request.
|
|
1105
|
+
* @param {string} requiredScope - Scope to verify.
|
|
1106
|
+
* @returns {boolean} True when scope is present.
|
|
1107
|
+
*/
|
|
1108
|
+
hasScopeFromRequest(req, requiredScope){
|
|
1109
|
+
return this.hasScope(this.getScopes(req), requiredScope);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* Check multiple scopes directly from Express request token claims.
|
|
1114
|
+
*
|
|
1115
|
+
* @param {Object} req - Express request.
|
|
1116
|
+
* @param {string|string[]} requiredScopes - Required scope(s).
|
|
1117
|
+
* @param {'all'|'any'} [mode='all'] - Matching mode.
|
|
1118
|
+
* @returns {boolean} Scope check result.
|
|
1119
|
+
*/
|
|
1120
|
+
hasScopesFromRequest(req, requiredScopes, mode = 'all'){
|
|
1121
|
+
return this.hasScopes(this.getScopes(req), requiredScopes, mode);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Express middleware that enforces required scopes.
|
|
1126
|
+
*
|
|
1127
|
+
* - Calls `next()` when scope check passes.
|
|
1128
|
+
* - Returns 403 response when scope check fails.
|
|
1129
|
+
*
|
|
1130
|
+
* @param {string|string[]} requiredScopes - Scope(s) to enforce.
|
|
1131
|
+
* @param {'all'|'any'} [mode='all'] - Matching mode.
|
|
1132
|
+
* @returns {Function} Express middleware.
|
|
1133
|
+
*/
|
|
1134
|
+
requireScopes(requiredScopes, mode = 'all'){
|
|
1135
|
+
return (req, res, next) => {
|
|
1136
|
+
if (this.hasScopesFromRequest(req, requiredScopes, mode)) {
|
|
1137
|
+
return next();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
const payload = {
|
|
1141
|
+
error: 'forbidden',
|
|
1142
|
+
message: 'Missing required scope(s)',
|
|
1143
|
+
requiredScopes: Array.isArray(requiredScopes) ? requiredScopes : [requiredScopes],
|
|
1144
|
+
mode
|
|
1145
|
+
};
|
|
1146
|
+
|
|
1147
|
+
if (typeof res.status === 'function') {
|
|
1148
|
+
res.status(403);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
if (typeof res.json === 'function') {
|
|
1152
|
+
return res.json(payload);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
if (typeof res.send === 'function') {
|
|
1156
|
+
return res.send(payload.message);
|
|
1157
|
+
}
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
|
|
998
1161
|
|
|
999
1162
|
|
|
1000
1163
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keycloak-express-middleware",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
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
|
+
"typings": "index.d.ts",
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
8
9
|
"require": "./index.js",
|
|
@@ -11,24 +12,12 @@
|
|
|
11
12
|
}
|
|
12
13
|
},
|
|
13
14
|
"scripts": {
|
|
14
|
-
"test": "
|
|
15
|
+
"test": "npm --prefix test install && npm --prefix test run setup-keycloak && NODE_PATH=./test/node_modules npm --prefix test test",
|
|
15
16
|
"setup-keycloak": "eval \"$(ssh-agent -s)\" && ssh-add ~/.ssh/id_ed25519 && NODE_ENV=test node test/docker-keycloak/setup-keycloak.js"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"body-parser": "^2.2.0",
|
|
21
|
-
"cookie-parser": "^1.4.7",
|
|
22
|
-
"debug": "^4.4.1",
|
|
23
|
-
"express": "^5.1.0",
|
|
24
|
-
"express-session": "^1.18.1",
|
|
25
|
-
"jwt-simple": "^0.5.6",
|
|
26
|
-
"keycloak-connect": "^26.1.1",
|
|
27
|
-
"moment": "^2.30.1",
|
|
28
|
-
"morgan": "^1.10.0",
|
|
29
|
-
"responseinterceptor": "^2.0.2",
|
|
30
|
-
"serve-favicon": "^2.5.0",
|
|
31
|
-
"underscore": "^1.13.7"
|
|
19
|
+
"express-session": "^1.19.0",
|
|
20
|
+
"keycloak-connect": "^26.1.1"
|
|
32
21
|
},
|
|
33
22
|
"keywords": [
|
|
34
23
|
"keycloak",
|