keycloak-api-manager 4.0.0 → 5.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/Handlers/attackDetectionHandler.js +64 -0
- package/Handlers/clientPoliciesHandler.js +120 -0
- package/Handlers/groupsHandler.js +32 -0
- package/Handlers/organizationsHandler.js +243 -0
- package/Handlers/serverInfoHandler.js +36 -0
- package/Handlers/userProfileHandler.js +121 -0
- package/README.md +83 -7157
- package/docs/architecture.md +47 -0
- package/docs/deployment.md +32 -0
- package/docs/keycloak-setup.md +47 -0
- package/docs/test-configuration.md +43 -0
- package/docs/testing.md +60 -0
- package/index.js +156 -240
- package/package.json +28 -15
- package/test/.mocharc.json +2 -2
- package/test/config/secrets.json +12 -0
- package/test/docker-keycloak/certs/keycloak.crt +58 -0
- package/test/docker-keycloak/certs/keycloak.key +28 -0
- package/test/docker-keycloak/docker-compose-https.yml +2 -0
- package/test/docker-keycloak/docker-compose.yml +4 -4
- package/test/helpers/matrix.js +16 -0
- package/test/matrix/auth.json +27 -0
- package/test/matrix/clients.json +45 -0
- package/test/matrix/realms-components-idp.json +37 -0
- package/test/matrix/users-roles-groups.json +26 -0
- package/test/package-lock.json +3032 -0
- package/test/specs/attackDetection.test.js +102 -0
- package/test/specs/clientCredentials.test.js +79 -0
- package/test/specs/clientPolicies.test.js +162 -0
- package/test/specs/{debugClientLibrary.test.js → diagnostics/debugClientLibrary.test.js} +2 -2
- package/test/specs/groupPermissions.test.js +87 -0
- package/test/specs/matrix/matrix-auth.test.js +112 -0
- package/test/specs/matrix/matrix-clients.test.js +59 -0
- package/test/specs/matrix/matrix-realms-components-idp.test.js +111 -0
- package/test/specs/matrix/matrix-users-roles-groups.test.js +68 -0
- package/test/specs/organizations.test.js +183 -0
- package/test/specs/serverInfo.test.js +140 -0
- package/test/specs/userProfile.test.js +135 -0
- package/test/{enableServerFeatures.js → support/enableServerFeatures.js} +43 -26
- package/test/{setup.js → support/setup.js} +3 -3
- package/test/support/testConfig.js +69 -0
- package/test/testConfig.js +1 -69
- package/test-output.log +72 -0
- package/test/TESTING.md +0 -327
- package/test/config/CONFIGURATION.md +0 -170
- package/test/diagnostic-protocol-mappers.js +0 -189
- package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +0 -262
- package/test/helpers/setup.js +0 -186
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Architecture and Runtime
|
|
2
|
+
|
|
3
|
+
This package exposes a single runtime (`index.js`) that initializes one Keycloak admin client instance and wires all handler modules.
|
|
4
|
+
|
|
5
|
+
## Runtime Lifecycle
|
|
6
|
+
|
|
7
|
+
1. `configure(credentials)`
|
|
8
|
+
- Validates and normalizes runtime configuration.
|
|
9
|
+
- Authenticates against Keycloak.
|
|
10
|
+
- Starts automatic token refresh.
|
|
11
|
+
- Injects the configured client into all handlers.
|
|
12
|
+
|
|
13
|
+
2. `setConfig(overrides)`
|
|
14
|
+
- Updates realm/baseUrl/request options at runtime.
|
|
15
|
+
- Keeps active session/token management in place.
|
|
16
|
+
|
|
17
|
+
3. `auth(credentials)`
|
|
18
|
+
- Direct token endpoint call for explicit auth flows.
|
|
19
|
+
|
|
20
|
+
4. `stop()`
|
|
21
|
+
- Stops refresh timer for clean process termination.
|
|
22
|
+
|
|
23
|
+
## Handler Design
|
|
24
|
+
|
|
25
|
+
Each file in `Handlers/` receives the configured Keycloak client through `setKcAdminClient`.
|
|
26
|
+
|
|
27
|
+
Pattern:
|
|
28
|
+
|
|
29
|
+
- Keep wrapper methods thin and explicit.
|
|
30
|
+
- Use official client methods whenever possible.
|
|
31
|
+
- Use direct REST calls only for endpoints not fully covered by `@keycloak/keycloak-admin-client`.
|
|
32
|
+
|
|
33
|
+
## Direct API Wrappers
|
|
34
|
+
|
|
35
|
+
Some handlers (for example Organizations update behavior) use direct `fetch` calls to match real Keycloak endpoint requirements.
|
|
36
|
+
|
|
37
|
+
Guideline:
|
|
38
|
+
|
|
39
|
+
- Prefer wrapper parity with official endpoints.
|
|
40
|
+
- Keep payload shaping close to Keycloak server expectations.
|
|
41
|
+
- Avoid test-specific logic in production handlers.
|
|
42
|
+
|
|
43
|
+
## Error Handling Principles
|
|
44
|
+
|
|
45
|
+
- Fail fast on missing configuration.
|
|
46
|
+
- Preserve Keycloak error context in thrown messages.
|
|
47
|
+
- Keep behavior deterministic between local/remote Keycloak instances.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Deployment Guide (Local and Remote)
|
|
2
|
+
|
|
3
|
+
This guide covers test/development Keycloak deployment options used by this project.
|
|
4
|
+
|
|
5
|
+
## Local Deployment
|
|
6
|
+
|
|
7
|
+
### HTTP (fast dev)
|
|
8
|
+
|
|
9
|
+
- Start compose from `test/docker-keycloak/`.
|
|
10
|
+
- Use `http://localhost:8080` as base URL.
|
|
11
|
+
|
|
12
|
+
### HTTPS (production-like)
|
|
13
|
+
|
|
14
|
+
- Provide certificate and key (`keycloak.crt`, `keycloak.key`).
|
|
15
|
+
- Expose `8443` and set hostname consistently.
|
|
16
|
+
|
|
17
|
+
## Remote Deployment (SSH)
|
|
18
|
+
|
|
19
|
+
- Copy compose and optional cert files to remote host.
|
|
20
|
+
- Start container remotely.
|
|
21
|
+
- Verify readiness and endpoint reachability.
|
|
22
|
+
|
|
23
|
+
## Recommended Verification Checklist
|
|
24
|
+
|
|
25
|
+
- Container healthy
|
|
26
|
+
- Token endpoint reachable
|
|
27
|
+
- Admin endpoint reachable
|
|
28
|
+
- Feature flags active (`admin-fine-grained-authz:v1,organization,client-policies`)
|
|
29
|
+
|
|
30
|
+
## Operational Tip
|
|
31
|
+
|
|
32
|
+
For automated test runs against remote hosts, keep test `baseUrl` aligned with reachable hostname/certificate pair to avoid TLS and normalization errors.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Keycloak Setup and Feature Flags
|
|
2
|
+
|
|
3
|
+
This guide describes the server-side prerequisites required for full package functionality.
|
|
4
|
+
|
|
5
|
+
## Minimum Recommended Setup
|
|
6
|
+
|
|
7
|
+
- Keycloak 25+ (26.x recommended)
|
|
8
|
+
- Admin user in `master` realm (or equivalent privileged client)
|
|
9
|
+
- HTTPS strongly recommended outside local development
|
|
10
|
+
|
|
11
|
+
## Required Feature Flags
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
--features=admin-fine-grained-authz:v1,organization,client-policies
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
|
|
19
|
+
- `admin-fine-grained-authz:v1`: required for management-permissions APIs used by group/user permission flows in this package.
|
|
20
|
+
- `organization`: required for Organizations endpoints.
|
|
21
|
+
- `client-policies`: required for client policy/profile endpoints.
|
|
22
|
+
|
|
23
|
+
## Docker Example
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
docker run -d --name keycloak \
|
|
27
|
+
-p 8080:8080 -p 8443:8443 \
|
|
28
|
+
-e KEYCLOAK_ADMIN=admin \
|
|
29
|
+
-e KEYCLOAK_ADMIN_PASSWORD=admin \
|
|
30
|
+
-e KC_FEATURES=admin-fine-grained-authz:v1,organization,client-policies \
|
|
31
|
+
keycloak/keycloak:latest start-dev
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Compose Example
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
environment:
|
|
38
|
+
KEYCLOAK_ADMIN: admin
|
|
39
|
+
KEYCLOAK_ADMIN_PASSWORD: admin
|
|
40
|
+
KC_FEATURES: 'admin-fine-grained-authz:v1,organization,client-policies'
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Verify Server Readiness
|
|
44
|
+
|
|
45
|
+
- Health endpoint should be reachable.
|
|
46
|
+
- Token endpoint should issue admin tokens.
|
|
47
|
+
- Admin endpoints should not return `Feature not enabled` for enabled features.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Test Configuration
|
|
2
|
+
|
|
3
|
+
Test configuration is managed through `propertiesmanager` with layered files.
|
|
4
|
+
|
|
5
|
+
## Files and Priority
|
|
6
|
+
|
|
7
|
+
1. `test/config/default.json` (committed defaults)
|
|
8
|
+
2. `test/config/secrets.json` (gitignored sensitive values)
|
|
9
|
+
3. `test/config/local.json` (gitignored machine-specific overrides)
|
|
10
|
+
|
|
11
|
+
The active section is selected by `NODE_ENV` (defaults to `test` in suite bootstrap).
|
|
12
|
+
|
|
13
|
+
## Required Keys
|
|
14
|
+
|
|
15
|
+
- `test.keycloak.baseUrl`
|
|
16
|
+
- `test.keycloak.realmName`
|
|
17
|
+
- `test.keycloak.clientId`
|
|
18
|
+
- `test.keycloak.username`
|
|
19
|
+
- `test.keycloak.password` (typically in `secrets.json`)
|
|
20
|
+
- `test.keycloak.grantType`
|
|
21
|
+
|
|
22
|
+
## Example `secrets.json`
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"test": {
|
|
27
|
+
"keycloak": {
|
|
28
|
+
"password": "admin"
|
|
29
|
+
},
|
|
30
|
+
"realm": {
|
|
31
|
+
"user": {
|
|
32
|
+
"password": "test-password"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Security Rules
|
|
40
|
+
|
|
41
|
+
- Never commit `secrets.json`.
|
|
42
|
+
- Never commit production credentials.
|
|
43
|
+
- Keep `default.json` non-sensitive.
|
package/docs/testing.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Testing Guide
|
|
2
|
+
|
|
3
|
+
The test suite validates the package against a real Keycloak server.
|
|
4
|
+
|
|
5
|
+
## Test Architecture
|
|
6
|
+
|
|
7
|
+
The suite uses a shared realm strategy:
|
|
8
|
+
|
|
9
|
+
- One global setup provisions baseline resources.
|
|
10
|
+
- Test files create their own unique entities where needed.
|
|
11
|
+
- Global teardown removes the shared test realm.
|
|
12
|
+
|
|
13
|
+
This improves speed and keeps the environment deterministic.
|
|
14
|
+
|
|
15
|
+
## Current Test Layout
|
|
16
|
+
|
|
17
|
+
```text
|
|
18
|
+
test/
|
|
19
|
+
specs/
|
|
20
|
+
*.test.js # core suites
|
|
21
|
+
diagnostics/*.test.js # diagnostic-style suites
|
|
22
|
+
matrix/*.test.js # data-driven matrix suites
|
|
23
|
+
support/
|
|
24
|
+
setup.js
|
|
25
|
+
enableServerFeatures.js
|
|
26
|
+
testConfig.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Commands
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# full suite
|
|
33
|
+
npm test
|
|
34
|
+
|
|
35
|
+
# run only test workspace
|
|
36
|
+
npm --prefix test test
|
|
37
|
+
|
|
38
|
+
# grep a subset
|
|
39
|
+
npm --prefix test test -- --grep "Organizations Handler Tests"
|
|
40
|
+
npm --prefix test test -- --grep "Matrix -"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Setup Flow
|
|
44
|
+
|
|
45
|
+
`test/support/setup.js` runs before all suites and executes `test/support/enableServerFeatures.js` to provision:
|
|
46
|
+
|
|
47
|
+
- realm
|
|
48
|
+
- client
|
|
49
|
+
- user
|
|
50
|
+
- roles
|
|
51
|
+
- group
|
|
52
|
+
- client scope
|
|
53
|
+
- fine-grained permissions (when feature-enabled)
|
|
54
|
+
|
|
55
|
+
## Writing New Tests
|
|
56
|
+
|
|
57
|
+
- Import config from `test/testConfig.js`.
|
|
58
|
+
- Use unique names for resources (`generateUniqueName` or timestamp).
|
|
59
|
+
- Clean up created resources in `after` hooks.
|
|
60
|
+
- Avoid destructive realm-wide mutations unless test is explicitly scoped for it.
|
package/index.js
CHANGED
|
@@ -1,262 +1,178 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
const KeycloakAdminClient = require('@keycloak/keycloak-admin-client').default;
|
|
2
|
+
|
|
3
|
+
const handlerRegistry = {
|
|
4
|
+
realms: require('./Handlers/realmsHandler'),
|
|
5
|
+
users: require('./Handlers/usersHandler'),
|
|
6
|
+
clients: require('./Handlers/clientsHandler'),
|
|
7
|
+
clientScopes: require('./Handlers/clientScopesHandler'),
|
|
8
|
+
identityProviders: require('./Handlers/identityProvidersHandler'),
|
|
9
|
+
groups: require('./Handlers/groupsHandler'),
|
|
10
|
+
roles: require('./Handlers/rolesHandler'),
|
|
11
|
+
components: require('./Handlers/componentsHandler'),
|
|
12
|
+
authenticationManagement: require('./Handlers/authenticationManagementHandler'),
|
|
13
|
+
attackDetection: require('./Handlers/attackDetectionHandler'),
|
|
14
|
+
organizations: require('./Handlers/organizationsHandler'),
|
|
15
|
+
userProfile: require('./Handlers/userProfileHandler'),
|
|
16
|
+
clientPolicies: require('./Handlers/clientPoliciesHandler'),
|
|
17
|
+
serverInfo: require('./Handlers/serverInfoHandler')
|
|
18
|
+
};
|
|
14
19
|
|
|
15
|
-
let
|
|
16
|
-
let tokenRefreshInterval=null;
|
|
20
|
+
let kcAdminClient = null;
|
|
21
|
+
let tokenRefreshInterval = null;
|
|
22
|
+
let runtimeConfig = null;
|
|
23
|
+
let authPayload = null;
|
|
17
24
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
* @param {Object} adminClientCredentials - Configuration object for Keycloak Admin Client
|
|
25
|
-
* @param {string} adminClientCredentials.baseUrl - Keycloak server base URL (e.g., "http://localhost:8080")
|
|
26
|
-
* @param {string} adminClientCredentials.realmName - Realm to authenticate against (use "master" for admin operations)
|
|
27
|
-
* @param {string} adminClientCredentials.clientId - Client ID configured in Keycloak (e.g., "admin-cli")
|
|
28
|
-
* @param {string} adminClientCredentials.grantType - OAuth2 grant type ("password", "client_credentials", etc.)
|
|
29
|
-
* @param {number} adminClientCredentials.tokenLifeSpan - Access token lifetime in seconds (recommended: 60-120)
|
|
30
|
-
* @param {string} [adminClientCredentials.username] - Admin username (required for "password" grant type)
|
|
31
|
-
* @param {string} [adminClientCredentials.password] - Admin password (required for "password" grant type)
|
|
32
|
-
* @param {string} [adminClientCredentials.clientSecret] - Client secret (required for "client_credentials" or confidential clients)
|
|
33
|
-
* @param {string} [adminClientCredentials.scope] - OAuth2 scope (optional, e.g., "openid profile")
|
|
34
|
-
* @param {Object} [adminClientCredentials.requestOptions] - Custom HTTP options (headers, timeout, etc.) compatible with Fetch API
|
|
35
|
-
* @param {string} [adminClientCredentials.totp] - Time-based One-Time Password for MFA (if enabled)
|
|
36
|
-
* @param {boolean} [adminClientCredentials.offlineToken=false] - Request offline token for long-lived refresh tokens
|
|
37
|
-
* @param {string} [adminClientCredentials.refreshToken] - Existing refresh token (for "refresh_token" grant type)
|
|
38
|
-
*
|
|
39
|
-
* @returns {Promise<void>} Resolves when configuration is complete and authentication successful
|
|
40
|
-
*
|
|
41
|
-
* @throws {Error} If authentication fails or required parameters are missing
|
|
42
|
-
*
|
|
43
|
-
* @example
|
|
44
|
-
* // Basic configuration with password grant
|
|
45
|
-
* await KeycloakManager.configure({
|
|
46
|
-
* baseUrl: 'http://localhost:8080',
|
|
47
|
-
* realmName: 'master',
|
|
48
|
-
* clientId: 'admin-cli',
|
|
49
|
-
* username: 'admin',
|
|
50
|
-
* password: 'admin',
|
|
51
|
-
* grantType: 'password',
|
|
52
|
-
* tokenLifeSpan: 120
|
|
53
|
-
* });
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* // Configuration with client credentials
|
|
57
|
-
* await KeycloakManager.configure({
|
|
58
|
-
* baseUrl: 'https://auth.example.com',
|
|
59
|
-
* realmName: 'master',
|
|
60
|
-
* clientId: 'service-account',
|
|
61
|
-
* clientSecret: 'secret-key',
|
|
62
|
-
* grantType: 'client_credentials',
|
|
63
|
-
* tokenLifeSpan: 60
|
|
64
|
-
* });
|
|
65
|
-
*
|
|
66
|
-
* @note After successful configuration, all admin handlers are exposed:
|
|
67
|
-
* - KeycloakManager.realms - Realm management
|
|
68
|
-
* - KeycloakManager.users - User management
|
|
69
|
-
* - KeycloakManager.clients - Client management
|
|
70
|
-
* - KeycloakManager.clientScopes - Client scope management
|
|
71
|
-
* - KeycloakManager.identityProviders - Identity provider management
|
|
72
|
-
* - KeycloakManager.groups - Group management
|
|
73
|
-
* - KeycloakManager.roles - Role management
|
|
74
|
-
* - KeycloakManager.components - Component management
|
|
75
|
-
* - KeycloakManager.authenticationManagement - Authentication flow management
|
|
76
|
-
*
|
|
77
|
-
* @note Token Refresh: The client automatically refreshes the access token at intervals
|
|
78
|
-
* calculated as (tokenLifeSpan * 1000) / 2 milliseconds. Call KeycloakManager.stop()
|
|
79
|
-
* to clear the refresh interval and allow graceful process termination.
|
|
80
|
-
*/
|
|
81
|
-
exports.configure=async function(adminClientCredentials){
|
|
82
|
-
configAdminclient={
|
|
83
|
-
baseUrl:adminClientCredentials.baseUrl,
|
|
84
|
-
realmName:adminClientCredentials.realmName
|
|
85
|
-
}
|
|
25
|
+
function assertConfigured() {
|
|
26
|
+
if (!kcAdminClient || !runtimeConfig) {
|
|
27
|
+
throw new Error('Keycloak Admin Client is not configured. Call configure() first.');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
86
30
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
31
|
+
function toBaseUrl(baseUrl) {
|
|
32
|
+
if (!baseUrl || typeof baseUrl !== 'string') {
|
|
33
|
+
throw new Error('Invalid baseUrl. It must be a non-empty string.');
|
|
34
|
+
}
|
|
35
|
+
return baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
36
|
+
}
|
|
90
37
|
|
|
38
|
+
function bindHandlers() {
|
|
39
|
+
Object.entries(handlerRegistry).forEach(([name, handler]) => {
|
|
40
|
+
handler.setKcAdminClient(kcAdminClient);
|
|
41
|
+
exports[name] = handler;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
91
44
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
delete adminClientCredentials.tokenLifeSpan;
|
|
99
|
-
await kcAdminClient.auth(adminClientCredentials);
|
|
45
|
+
function clearRefreshTimer() {
|
|
46
|
+
if (tokenRefreshInterval) {
|
|
47
|
+
clearInterval(tokenRefreshInterval);
|
|
48
|
+
tokenRefreshInterval = null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
100
51
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
52
|
+
function startRefreshTimer(intervalMs) {
|
|
53
|
+
clearRefreshTimer();
|
|
104
54
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
55
|
+
tokenRefreshInterval = setInterval(async () => {
|
|
56
|
+
try {
|
|
57
|
+
await kcAdminClient.auth({ ...authPayload });
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.error('Token refresh failed:', err.message);
|
|
110
60
|
}
|
|
61
|
+
}, intervalMs);
|
|
111
62
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
exports.users=usersHandler;
|
|
117
|
-
|
|
118
|
-
clientsHandler.setKcAdminClient(kcAdminClient);
|
|
119
|
-
exports.clients=clientsHandler;
|
|
120
|
-
|
|
121
|
-
clientScopesHandler.setKcAdminClient(kcAdminClient);
|
|
122
|
-
exports.clientScopes=clientScopesHandler;
|
|
123
|
-
|
|
124
|
-
identityProvidersHandler.setKcAdminClient(kcAdminClient);
|
|
125
|
-
exports.identityProviders=identityProvidersHandler;
|
|
126
|
-
|
|
127
|
-
groupsHandler.setKcAdminClient(kcAdminClient);
|
|
128
|
-
exports.groups=groupsHandler;
|
|
129
|
-
|
|
130
|
-
rolesHandler.setKcAdminClient(kcAdminClient);
|
|
131
|
-
exports.roles=rolesHandler;
|
|
63
|
+
if (tokenRefreshInterval.unref) {
|
|
64
|
+
tokenRefreshInterval.unref();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
132
67
|
|
|
133
|
-
|
|
134
|
-
|
|
68
|
+
exports.configure = async function configure(adminClientCredentials = {}) {
|
|
69
|
+
const {
|
|
70
|
+
baseUrl,
|
|
71
|
+
realmName,
|
|
72
|
+
clientId,
|
|
73
|
+
clientSecret,
|
|
74
|
+
tokenLifeSpan,
|
|
75
|
+
...credentials
|
|
76
|
+
} = adminClientCredentials;
|
|
77
|
+
|
|
78
|
+
const normalizedBaseUrl = toBaseUrl(baseUrl);
|
|
79
|
+
|
|
80
|
+
runtimeConfig = {
|
|
81
|
+
baseUrl: normalizedBaseUrl,
|
|
82
|
+
realmName,
|
|
83
|
+
clientId,
|
|
84
|
+
clientSecret
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
kcAdminClient = new KeycloakAdminClient({
|
|
88
|
+
baseUrl: normalizedBaseUrl,
|
|
89
|
+
realmName
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const originalSetRefreshToken = kcAdminClient.setRefreshToken?.bind(kcAdminClient);
|
|
93
|
+
if (originalSetRefreshToken) {
|
|
94
|
+
kcAdminClient.setRefreshToken = (token) => {
|
|
95
|
+
if (!token) {
|
|
96
|
+
kcAdminClient.refreshToken = undefined;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
return originalSetRefreshToken(token);
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
authPayload = {
|
|
104
|
+
clientId,
|
|
105
|
+
...(clientSecret ? { clientSecret } : {}),
|
|
106
|
+
...credentials
|
|
107
|
+
};
|
|
108
|
+
await kcAdminClient.auth(authPayload);
|
|
109
|
+
|
|
110
|
+
const intervalMs = Number.isFinite(Number(tokenLifeSpan)) && Number(tokenLifeSpan) > 0
|
|
111
|
+
? (Number(tokenLifeSpan) * 1000) / 2
|
|
112
|
+
: 30000;
|
|
113
|
+
|
|
114
|
+
startRefreshTimer(intervalMs);
|
|
115
|
+
bindHandlers();
|
|
116
|
+
};
|
|
135
117
|
|
|
136
|
-
|
|
137
|
-
|
|
118
|
+
exports.setConfig = function setConfig(configToOverride = {}) {
|
|
119
|
+
assertConfigured();
|
|
120
|
+
kcAdminClient.setConfig(configToOverride);
|
|
138
121
|
|
|
122
|
+
runtimeConfig = {
|
|
123
|
+
...runtimeConfig,
|
|
124
|
+
...(configToOverride.baseUrl ? { baseUrl: toBaseUrl(configToOverride.baseUrl) } : {}),
|
|
125
|
+
...(configToOverride.realmName ? { realmName: configToOverride.realmName } : {})
|
|
126
|
+
};
|
|
127
|
+
};
|
|
139
128
|
|
|
140
|
-
|
|
129
|
+
exports.getToken = function getToken() {
|
|
130
|
+
assertConfigured();
|
|
131
|
+
return {
|
|
132
|
+
accessToken: kcAdminClient.accessToken,
|
|
133
|
+
refreshToken: kcAdminClient.refreshToken
|
|
134
|
+
};
|
|
141
135
|
};
|
|
142
136
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
* reinitializing the client or re-authenticating.
|
|
147
|
-
*
|
|
148
|
-
* @param {Object} configToOverride - Configuration object to update
|
|
149
|
-
* @param {string} [configToOverride.realmName] - The name of the target realm for subsequent API requests
|
|
150
|
-
* @param {string} [configToOverride.baseUrl] - The base URL of the Keycloak server (e.g., https://auth.example.com)
|
|
151
|
-
* @param {Object} [configToOverride.requestOptions] - Custom HTTP options (headers, timeout, etc.) applied to API calls
|
|
152
|
-
* @param {string} [configToOverride.realmPath] - A custom realm path if your Keycloak instance uses a non-standard realm route
|
|
153
|
-
* @returns {void}
|
|
154
|
-
*
|
|
155
|
-
* @note Calling setConfig does not perform authentication - it only changes configuration values in memory.
|
|
156
|
-
* The authentication token already stored in the admin client remains active until it expires.
|
|
157
|
-
* Only the properties explicitly passed in the config object are updated; all others remain unchanged.
|
|
158
|
-
*/
|
|
159
|
-
exports.setConfig=function(configToOverride){
|
|
160
|
-
return(kcAdminClient.setConfig(configToOverride));
|
|
161
|
-
}
|
|
137
|
+
exports.stop = function stop() {
|
|
138
|
+
clearRefreshTimer();
|
|
139
|
+
};
|
|
162
140
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
* Returns both the access token (used for API authorization) and the refresh token
|
|
166
|
-
* (used to renew the session when the access token expires).
|
|
167
|
-
*
|
|
168
|
-
* @returns {Object} Token object containing:
|
|
169
|
-
* @returns {string} accessToken - The active access token string currently held by the Keycloak Admin Client
|
|
170
|
-
* @returns {string} refreshToken - The corresponding refresh token string, if available
|
|
171
|
-
*
|
|
172
|
-
* @note The tokens are managed internally by the Keycloak Admin Client after successful authentication.
|
|
173
|
-
* The accessToken typically expires after a short period (e.g., 60 seconds by default).
|
|
174
|
-
* If the client is not authenticated or the session has expired, both values may be undefined.
|
|
175
|
-
*/
|
|
176
|
-
exports.getToken=function(){
|
|
177
|
-
return({
|
|
178
|
-
accessToken:kcAdminClient.accessToken,
|
|
179
|
-
refreshToken:kcAdminClient.refreshToken,
|
|
180
|
-
});
|
|
181
|
-
}
|
|
141
|
+
exports.auth = async function auth(credentials = {}) {
|
|
142
|
+
assertConfigured();
|
|
182
143
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
* Calling stop() clears this interval, allowing your Node.js process to exit gracefully.
|
|
188
|
-
*
|
|
189
|
-
* @returns {void}
|
|
190
|
-
*
|
|
191
|
-
* @note This method should be called when you're done using the Keycloak Admin Client
|
|
192
|
-
* and want to terminate your application. It's particularly important in test environments
|
|
193
|
-
* or CLI scripts where the process needs to exit cleanly. The method is safe to call
|
|
194
|
-
* multiple times; subsequent calls have no effect.
|
|
195
|
-
*
|
|
196
|
-
* @example
|
|
197
|
-
* // Configure and use the admin client
|
|
198
|
-
* await KeycloakManager.configure({ ... });
|
|
199
|
-
* const users = await KeycloakManager.users.find();
|
|
200
|
-
* // Clean up and allow process to exit
|
|
201
|
-
* KeycloakManager.stop();
|
|
202
|
-
*/
|
|
203
|
-
exports.stop=function(){
|
|
204
|
-
if (tokenRefreshInterval) {
|
|
205
|
-
clearInterval(tokenRefreshInterval);
|
|
206
|
-
tokenRefreshInterval=null;
|
|
144
|
+
const body = new URLSearchParams();
|
|
145
|
+
Object.entries(credentials).forEach(([key, value]) => {
|
|
146
|
+
if (value !== undefined && value !== null) {
|
|
147
|
+
body.append(key, String(value));
|
|
207
148
|
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
* });
|
|
226
|
-
* console.log("Access Token:", tokenResponse.access_token);
|
|
227
|
-
*/
|
|
228
|
-
exports.auth=async function(credentials){
|
|
229
|
-
credentials.client_id=configAdminclient.clientId;
|
|
230
|
-
credentials.client_secret=configAdminclient.clientSecret;
|
|
231
|
-
let options={
|
|
232
|
-
url: `${configAdminclient.baseUrl}realms/${configAdminclient.realmName}/protocol/openid-connect/token` ,
|
|
233
|
-
headers: {'content-type': 'application/www-form-urlencoded', 'Authorization': "Bearer " + kcAdminClient.accessToken },
|
|
234
|
-
form: credentials
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (runtimeConfig.clientId) {
|
|
152
|
+
body.append('client_id', runtimeConfig.clientId);
|
|
153
|
+
}
|
|
154
|
+
if (runtimeConfig.clientSecret) {
|
|
155
|
+
body.append('client_secret', runtimeConfig.clientSecret);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const response = await fetch(
|
|
159
|
+
`${runtimeConfig.baseUrl}/realms/${runtimeConfig.realmName}/protocol/openid-connect/token`,
|
|
160
|
+
{
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: {
|
|
163
|
+
'content-type': 'application/x-www-form-urlencoded'
|
|
164
|
+
},
|
|
165
|
+
body
|
|
235
166
|
}
|
|
236
|
-
|
|
237
|
-
request.post(options, function (error, response, body) {
|
|
238
|
-
if (error) {
|
|
239
|
-
console.error("Internal Server Error:", error); // internal error
|
|
240
|
-
reject(error);
|
|
241
|
-
} else {
|
|
242
|
-
resolve(JSON.parse(body)); // ✅ return auth token or error due to invalid credentials
|
|
243
|
-
}
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
/*
|
|
254
|
-
<table><tbody>
|
|
255
|
-
<tr><th align="left">Alessandro Romanino</th><td><a href="https://github.com/aromanino">GitHub/aromanino</a></td><td><a href="mailto:a.romanino@gmail.com">mailto:a.romanino@gmail.com</a></td></tr>
|
|
256
|
-
<tr><th align="left">Guido Porruvecchio</th><td><a href="https://github.com/gporruvecchio">GitHub/porruvecchio</a></td><td><a href="mailto:guido.porruvecchio@gmail.com">mailto:guido.porruvecchio@gmail.com</a></td></tr>
|
|
257
|
-
</tbody></table>
|
|
258
|
-
* */
|
|
259
|
-
|
|
167
|
+
);
|
|
260
168
|
|
|
169
|
+
const responseText = await response.text();
|
|
170
|
+
const payload = responseText ? JSON.parse(responseText) : {};
|
|
261
171
|
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const errorMessage = payload.error_description || payload.error || 'Authentication failed';
|
|
174
|
+
throw new Error(errorMessage);
|
|
175
|
+
}
|
|
262
176
|
|
|
177
|
+
return payload;
|
|
178
|
+
};
|