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.
Files changed (48) hide show
  1. package/Handlers/attackDetectionHandler.js +64 -0
  2. package/Handlers/clientPoliciesHandler.js +120 -0
  3. package/Handlers/groupsHandler.js +32 -0
  4. package/Handlers/organizationsHandler.js +243 -0
  5. package/Handlers/serverInfoHandler.js +36 -0
  6. package/Handlers/userProfileHandler.js +121 -0
  7. package/README.md +83 -7157
  8. package/docs/architecture.md +47 -0
  9. package/docs/deployment.md +32 -0
  10. package/docs/keycloak-setup.md +47 -0
  11. package/docs/test-configuration.md +43 -0
  12. package/docs/testing.md +60 -0
  13. package/index.js +156 -240
  14. package/package.json +28 -15
  15. package/test/.mocharc.json +2 -2
  16. package/test/config/secrets.json +12 -0
  17. package/test/docker-keycloak/certs/keycloak.crt +58 -0
  18. package/test/docker-keycloak/certs/keycloak.key +28 -0
  19. package/test/docker-keycloak/docker-compose-https.yml +2 -0
  20. package/test/docker-keycloak/docker-compose.yml +4 -4
  21. package/test/helpers/matrix.js +16 -0
  22. package/test/matrix/auth.json +27 -0
  23. package/test/matrix/clients.json +45 -0
  24. package/test/matrix/realms-components-idp.json +37 -0
  25. package/test/matrix/users-roles-groups.json +26 -0
  26. package/test/package-lock.json +3032 -0
  27. package/test/specs/attackDetection.test.js +102 -0
  28. package/test/specs/clientCredentials.test.js +79 -0
  29. package/test/specs/clientPolicies.test.js +162 -0
  30. package/test/specs/{debugClientLibrary.test.js → diagnostics/debugClientLibrary.test.js} +2 -2
  31. package/test/specs/groupPermissions.test.js +87 -0
  32. package/test/specs/matrix/matrix-auth.test.js +112 -0
  33. package/test/specs/matrix/matrix-clients.test.js +59 -0
  34. package/test/specs/matrix/matrix-realms-components-idp.test.js +111 -0
  35. package/test/specs/matrix/matrix-users-roles-groups.test.js +68 -0
  36. package/test/specs/organizations.test.js +183 -0
  37. package/test/specs/serverInfo.test.js +140 -0
  38. package/test/specs/userProfile.test.js +135 -0
  39. package/test/{enableServerFeatures.js → support/enableServerFeatures.js} +43 -26
  40. package/test/{setup.js → support/setup.js} +3 -3
  41. package/test/support/testConfig.js +69 -0
  42. package/test/testConfig.js +1 -69
  43. package/test-output.log +72 -0
  44. package/test/TESTING.md +0 -327
  45. package/test/config/CONFIGURATION.md +0 -170
  46. package/test/diagnostic-protocol-mappers.js +0 -189
  47. package/test/docker-keycloak/DEPLOYMENT_GUIDE.md +0 -262
  48. 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.
@@ -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
- var express = require('express');
2
- var keycloakAdminClient=require('@keycloak/keycloak-admin-client').default;
3
- var kcAdminClient=null;
4
- var realmHandler=require('./Handlers/realmsHandler');
5
- var usersHandler=require('./Handlers/usersHandler');
6
- var clientsHandler=require('./Handlers/clientsHandler');
7
- var clientScopesHandler=require('./Handlers/clientScopesHandler');
8
- var identityProvidersHandler=require('./Handlers/identityProvidersHandler');
9
- var groupsHandler=require('./Handlers/groupsHandler');
10
- var rolesHandler=require('./Handlers/rolesHandler');
11
- var componentsHandler=require('./Handlers/componentsHandler');
12
- var authenticationManagementHandler=require('./Handlers/authenticationManagementHandler');
13
- var request=require('request');
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 configAdminclient=null;
16
- let tokenRefreshInterval=null;
20
+ let kcAdminClient = null;
21
+ let tokenRefreshInterval = null;
22
+ let runtimeConfig = null;
23
+ let authPayload = null;
17
24
 
18
- /**
19
- * Configure and initialize the Keycloak Admin Client for managing Keycloak resources.
20
- *
21
- * This function MUST be called before using any administrative functions exposed by this library.
22
- * It sets up the admin client with proper credentials and establishes automatic token refresh.
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
- kcAdminClient= new keycloakAdminClient(configAdminclient);
88
- configAdminclient.clientId=adminClientCredentials.clientId;
89
- configAdminclient.clientSecret=adminClientCredentials.clientSecret;
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
- let tokenLifeSpan= Number(adminClientCredentials.tokenLifeSpan);
93
- tokenLifeSpan = Number.isFinite(tokenLifeSpan) && tokenLifeSpan > 0
94
- ? (tokenLifeSpan * 1000) / 2
95
- : 30000;
96
- delete adminClientCredentials.baseUrl;
97
- delete adminClientCredentials.realmName;
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
- if (tokenRefreshInterval) {
102
- clearInterval(tokenRefreshInterval);
103
- }
52
+ function startRefreshTimer(intervalMs) {
53
+ clearRefreshTimer();
104
54
 
105
- tokenRefreshInterval = setInterval(async ()=>{
106
- await kcAdminClient.auth(adminClientCredentials);
107
- },tokenLifeSpan);
108
- if (tokenRefreshInterval.unref) {
109
- tokenRefreshInterval.unref();
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
- realmHandler.setKcAdminClient(kcAdminClient);
113
- exports.realms=realmHandler;
114
-
115
- usersHandler.setKcAdminClient(kcAdminClient);
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
- componentsHandler.setKcAdminClient(kcAdminClient);
134
- exports.components=componentsHandler;
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
- authenticationManagementHandler.setKcAdminClient(kcAdminClient);
137
- exports.authenticationManagement=authenticationManagementHandler;
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
- //exports = kcAdminClient;
129
+ exports.getToken = function getToken() {
130
+ assertConfigured();
131
+ return {
132
+ accessToken: kcAdminClient.accessToken,
133
+ refreshToken: kcAdminClient.refreshToken
134
+ };
141
135
  };
142
136
 
143
- /**
144
- * Updates the runtime configuration of the Keycloak Admin Client instance.
145
- * Allows switching the target realm, base URL, or HTTP request options without
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
- * Retrieves the current authentication tokens used by the Keycloak Admin Client.
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
- * Cleanly stops the Keycloak Admin Client by clearing the automatic token refresh interval.
185
- * When the admin client is configured with tokenLifeSpan, it automatically refreshes
186
- * the access token at regular intervals to maintain the session.
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
- * Allows a user or client to authenticate against a Keycloak realm and obtain an access token.
212
- * Sends a direct HTTP POST request to the Keycloak OpenID Connect token endpoint using the provided credentials.
213
- *
214
- * @param {Object} credentials - Authentication details
215
- * @param {string} [credentials.username] - Username of the user (required for password grant)
216
- * @param {string} [credentials.password] - Password of the user (required for password grant)
217
- * @param {string} credentials.grant_type - The OAuth2 grant type (e.g. "password", "client_credentials", "refresh_token")
218
- * @returns {Promise<Object>} Token response object from Keycloak
219
- *
220
- * @example
221
- * const tokenResponse = await KeycloakManager.auth({
222
- * username: "demo",
223
- * password: "demo123",
224
- * grant_type: "password",
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
- return new Promise((resolve, reject) => {
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
+ };