@adatechnology/keycloak-admin 0.1.15 → 1.0.0-rc.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/README.md +59 -0
- package/dist/index.d.ts +137 -84
- package/dist/index.js +284 -662
- package/package.json +28 -19
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# @adatechnology/keycloak-admin
|
|
2
|
+
|
|
3
|
+
Cliente do Keycloak Admin API autenticado como **service account** (`client_credentials`).
|
|
4
|
+
Agnóstico de framework e de runtime: só depende de `fetch` e de `zod`.
|
|
5
|
+
|
|
6
|
+
Diferença deliberada para o `@adatechnology/nestjs-keycloak-admin`: aquele obtém token com
|
|
7
|
+
`grant_type=password` usando o usuário administrador do realm `master`. Este **nunca** envia senha
|
|
8
|
+
nem usuário — a identidade é do client confidencial, com `manage-users` do `realm-management`.
|
|
9
|
+
|
|
10
|
+
## Uso
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { createKeycloakAdminClient } from '@adatechnology/keycloak-admin'
|
|
14
|
+
|
|
15
|
+
const keycloak = createKeycloakAdminClient({
|
|
16
|
+
config: {
|
|
17
|
+
baseUrl: process.env.KEYCLOAK_URL,
|
|
18
|
+
clientId: process.env.KEYCLOAK_ADMIN_CLIENT_ID,
|
|
19
|
+
clientSecret: process.env.KEYCLOAK_ADMIN_CLIENT_SECRET,
|
|
20
|
+
realm: process.env.KEYCLOAK_REALM,
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const { id } = await keycloak.createUser({
|
|
25
|
+
attributes: { company_id: companyId },
|
|
26
|
+
email: 'admin@transportadora.example',
|
|
27
|
+
enabled: true,
|
|
28
|
+
firstName: 'Ada',
|
|
29
|
+
lastName: 'Lovelace',
|
|
30
|
+
password: { temporary: false, value: chosenPassword },
|
|
31
|
+
username: 'admin@transportadora.example',
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Operações: `createUser`, `findUserByEmail`, `updateUser`, `setEnabled`, `updateAttributes`,
|
|
36
|
+
`deleteUser`, `setPassword`, `setTemporaryPassword`.
|
|
37
|
+
|
|
38
|
+
## Token
|
|
39
|
+
|
|
40
|
+
Obtido sob demanda, guardado em memória e renovado 30s antes de expirar
|
|
41
|
+
(`KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS`). Chamadas concorrentes compartilham a mesma requisição em
|
|
42
|
+
voo — o Keycloak recebe uma, não N.
|
|
43
|
+
|
|
44
|
+
## Injeção
|
|
45
|
+
|
|
46
|
+
`fetch` e `now` são injetáveis, o que torna rede e relógio observáveis em teste:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
createKeycloakAdminClient({ config, fetch: stubFetch, now: clock.now })
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Erros
|
|
53
|
+
|
|
54
|
+
Toda falha vira `KeycloakAdminError` com `code` estável (`KEYCLOAK_ADMIN_ERROR_CODE`), `status` e
|
|
55
|
+
`context`. O contexto é montado por allowlist e passa por um redator que substitui `clientSecret`,
|
|
56
|
+
access token e senha por `[REDACTED]` — nem a mensagem nem o erro serializado carregam segredo.
|
|
57
|
+
|
|
58
|
+
Configuração inválida falha na construção do cliente, com o **caminho** do campo no contexto e nunca
|
|
59
|
+
o valor.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,91 +1,144 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
3
|
+
declare const KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS = 30000;
|
|
4
|
+
declare const KEYCLOAK_ADMIN_ERROR_CODE: {
|
|
5
|
+
readonly CONFIGURATION_INVALID: "KEYCLOAK_ADMIN_CONFIGURATION_INVALID";
|
|
6
|
+
readonly REQUEST_FAILED: "KEYCLOAK_ADMIN_REQUEST_FAILED";
|
|
7
|
+
readonly TOKEN_REQUEST_FAILED: "KEYCLOAK_ADMIN_TOKEN_REQUEST_FAILED";
|
|
8
|
+
readonly TOKEN_RESPONSE_INVALID: "KEYCLOAK_ADMIN_TOKEN_RESPONSE_INVALID";
|
|
9
|
+
readonly USER_ALREADY_EXISTS: "KEYCLOAK_ADMIN_USER_ALREADY_EXISTS";
|
|
10
|
+
readonly USER_ID_MISSING: "KEYCLOAK_ADMIN_USER_ID_MISSING";
|
|
11
|
+
readonly USER_NOT_FOUND: "KEYCLOAK_ADMIN_USER_NOT_FOUND";
|
|
12
|
+
};
|
|
13
|
+
type KeycloakAdminErrorCode = (typeof KEYCLOAK_ADMIN_ERROR_CODE)[keyof typeof KEYCLOAK_ADMIN_ERROR_CODE];
|
|
14
|
+
type BuildKeycloakAdminEndpointsParams = {
|
|
15
|
+
readonly baseUrl: string;
|
|
16
|
+
readonly realm: string;
|
|
17
|
+
};
|
|
18
|
+
declare function buildKeycloakAdminEndpoints({ baseUrl, realm }: BuildKeycloakAdminEndpointsParams): {
|
|
19
|
+
readonly token: `${string}/realms/${string}/protocol/openid-connect/token`;
|
|
20
|
+
readonly user: (userId: string) => string;
|
|
21
|
+
readonly userPassword: (userId: string) => string;
|
|
22
|
+
readonly users: string;
|
|
23
|
+
};
|
|
24
|
+
type KeycloakAdminEndpoints = ReturnType<typeof buildKeycloakAdminEndpoints>;
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
type KeycloakAdminErrorContext = Readonly<Record<string, unknown>>;
|
|
27
|
+
type KeycloakAdminErrorParams = {
|
|
28
|
+
readonly code: KeycloakAdminErrorCode;
|
|
29
|
+
readonly context?: KeycloakAdminErrorContext;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly status?: number;
|
|
32
|
+
};
|
|
33
|
+
type SerializedKeycloakAdminError = {
|
|
34
|
+
readonly code: KeycloakAdminErrorCode;
|
|
35
|
+
readonly context: KeycloakAdminErrorContext;
|
|
36
|
+
readonly message: string;
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly status: number | undefined;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Falha de qualquer operação do Admin API, com código estável para o consumidor decidir.
|
|
42
|
+
* O contexto é montado por allowlist e passa pelo redator — segredo, token e senha nunca entram.
|
|
43
|
+
*/
|
|
44
|
+
declare class KeycloakAdminError extends Error {
|
|
45
|
+
readonly code: KeycloakAdminErrorCode;
|
|
46
|
+
readonly context: KeycloakAdminErrorContext;
|
|
47
|
+
readonly status: number | undefined;
|
|
48
|
+
constructor({ code, context, message, status }: KeycloakAdminErrorParams);
|
|
49
|
+
toJSON(): SerializedKeycloakAdminError;
|
|
27
50
|
}
|
|
51
|
+
declare function isKeycloakAdminError(value: unknown): value is KeycloakAdminError;
|
|
28
52
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
53
|
+
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
54
|
+
type KeycloakAdminConfig = {
|
|
55
|
+
readonly baseUrl: string;
|
|
56
|
+
readonly clientId: string;
|
|
57
|
+
readonly clientSecret: string;
|
|
58
|
+
readonly realm: string;
|
|
59
|
+
};
|
|
60
|
+
type KeycloakUserAttributes = Readonly<Record<string, string | readonly string[]>>;
|
|
61
|
+
type KeycloakUser = {
|
|
62
|
+
readonly attributes?: KeycloakUserAttributes;
|
|
63
|
+
readonly email?: string;
|
|
64
|
+
readonly emailVerified?: boolean;
|
|
65
|
+
readonly enabled?: boolean;
|
|
66
|
+
readonly firstName?: string;
|
|
67
|
+
readonly id: string;
|
|
68
|
+
readonly lastName?: string;
|
|
69
|
+
readonly username?: string;
|
|
70
|
+
};
|
|
71
|
+
type KeycloakPassword = {
|
|
72
|
+
readonly temporary: boolean;
|
|
73
|
+
readonly value: string;
|
|
74
|
+
};
|
|
75
|
+
type CreateUserParams = {
|
|
76
|
+
readonly attributes?: KeycloakUserAttributes;
|
|
77
|
+
readonly email: string;
|
|
78
|
+
readonly emailVerified?: boolean;
|
|
79
|
+
readonly enabled?: boolean;
|
|
80
|
+
readonly firstName?: string;
|
|
81
|
+
readonly lastName?: string;
|
|
82
|
+
readonly password?: KeycloakPassword;
|
|
83
|
+
readonly username: string;
|
|
84
|
+
};
|
|
85
|
+
type CreateUserResult = {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
};
|
|
88
|
+
type FindUserByEmailParams = {
|
|
89
|
+
readonly email: string;
|
|
90
|
+
};
|
|
91
|
+
type UpdateUserParams = {
|
|
92
|
+
readonly user: Readonly<Partial<Pick<KeycloakUser, 'email' | 'emailVerified' | 'firstName' | 'lastName' | 'username'>>>;
|
|
93
|
+
readonly userId: string;
|
|
94
|
+
};
|
|
95
|
+
type SetEnabledParams = {
|
|
96
|
+
readonly enabled: boolean;
|
|
97
|
+
readonly userId: string;
|
|
98
|
+
};
|
|
99
|
+
type UpdateAttributesParams = {
|
|
100
|
+
readonly attributes: KeycloakUserAttributes;
|
|
101
|
+
readonly userId: string;
|
|
102
|
+
};
|
|
103
|
+
type DeleteUserParams = {
|
|
104
|
+
readonly userId: string;
|
|
105
|
+
};
|
|
106
|
+
type SetPasswordParams = {
|
|
107
|
+
readonly password: string;
|
|
108
|
+
readonly temporary: boolean;
|
|
109
|
+
readonly userId: string;
|
|
110
|
+
};
|
|
111
|
+
type SetTemporaryPasswordParams = {
|
|
112
|
+
readonly password: string;
|
|
113
|
+
readonly userId: string;
|
|
114
|
+
};
|
|
115
|
+
type KeycloakAdminClient = {
|
|
116
|
+
createUser(params: CreateUserParams): Promise<CreateUserResult>;
|
|
117
|
+
deleteUser(params: DeleteUserParams): Promise<void>;
|
|
118
|
+
findUserByEmail(params: FindUserByEmailParams): Promise<KeycloakUser | undefined>;
|
|
119
|
+
setEnabled(params: SetEnabledParams): Promise<void>;
|
|
120
|
+
setPassword(params: SetPasswordParams): Promise<void>;
|
|
121
|
+
setTemporaryPassword(params: SetTemporaryPasswordParams): Promise<void>;
|
|
122
|
+
updateAttributes(params: UpdateAttributesParams): Promise<void>;
|
|
123
|
+
updateUser(params: UpdateUserParams): Promise<void>;
|
|
124
|
+
};
|
|
125
|
+
type CreateKeycloakAdminClientParams = {
|
|
126
|
+
readonly config: KeycloakAdminConfig;
|
|
127
|
+
readonly fetch?: FetchLike;
|
|
128
|
+
readonly now?: () => number;
|
|
129
|
+
};
|
|
45
130
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
deleteUser(params: DeleteUserParams): Promise<void>;
|
|
57
|
-
updateUserAttributes(params: UpdateUserAttributesParams): Promise<void>;
|
|
58
|
-
sendVerifyEmail(params: SendVerifyEmailParams): Promise<void>;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export class KeycloakAdminError extends Error {
|
|
62
|
-
readonly statusCode?: number;
|
|
63
|
-
readonly code?: string;
|
|
64
|
-
readonly context?: Record<string, unknown>;
|
|
65
|
-
constructor(params: {
|
|
66
|
-
message: string;
|
|
67
|
-
statusCode?: number;
|
|
68
|
-
code?: string;
|
|
69
|
-
context?: Record<string, unknown>;
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export class KeycloakAdminClient implements KeycloakAdminClientInterface {
|
|
74
|
-
getAdminToken(): Promise<GetAdminTokenResult>;
|
|
75
|
-
updateUser(params: UpdateUserParams): Promise<void>;
|
|
76
|
-
resetPassword(params: ResetPasswordParams): Promise<void>;
|
|
77
|
-
toggleUserEnabled(params: ToggleUserEnabledParams): Promise<void>;
|
|
78
|
-
deleteUser(params: DeleteUserParams): Promise<void>;
|
|
79
|
-
updateUserAttributes(params: UpdateUserAttributesParams): Promise<void>;
|
|
80
|
-
sendVerifyEmail(params: SendVerifyEmailParams): Promise<void>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export class KeycloakAdminModule {
|
|
84
|
-
static forRoot(config: KeycloakAdminConfig): DynamicModule;
|
|
85
|
-
}
|
|
131
|
+
declare const keycloakAdminConfigSchema: z.ZodObject<{
|
|
132
|
+
baseUrl: z.ZodString;
|
|
133
|
+
clientId: z.ZodString;
|
|
134
|
+
clientSecret: z.ZodString;
|
|
135
|
+
realm: z.ZodString;
|
|
136
|
+
}, z.core.$strip>;
|
|
137
|
+
/**
|
|
138
|
+
* A falha carrega só o caminho do campo inválido. O valor nunca entra — seria o `clientSecret`.
|
|
139
|
+
*/
|
|
140
|
+
declare function parseKeycloakAdminConfig(value: unknown): KeycloakAdminConfig;
|
|
86
141
|
|
|
87
|
-
|
|
142
|
+
declare function createKeycloakAdminClient({ config: rawConfig, fetch: injectedFetch, now, }: CreateKeycloakAdminClientParams): KeycloakAdminClient;
|
|
88
143
|
|
|
89
|
-
export
|
|
90
|
-
export const KEYCLOAK_ADMIN_CONFIG = "KEYCLOAK_ADMIN_CONFIG";
|
|
91
|
-
export const KEYCLOAK_ADMIN_PROVIDER = "KEYCLOAK_ADMIN_PROVIDER";
|
|
144
|
+
export { type CreateKeycloakAdminClientParams, type CreateUserParams, type CreateUserResult, type DeleteUserParams, type FetchLike, type FindUserByEmailParams, KEYCLOAK_ADMIN_ERROR_CODE, KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS, type KeycloakAdminClient, type KeycloakAdminConfig, type KeycloakAdminEndpoints, KeycloakAdminError, type KeycloakAdminErrorCode, type KeycloakAdminErrorContext, type KeycloakAdminErrorParams, type KeycloakPassword, type KeycloakUser, type KeycloakUserAttributes, type SerializedKeycloakAdminError, type SetEnabledParams, type SetPasswordParams, type SetTemporaryPasswordParams, type UpdateAttributesParams, type UpdateUserParams, buildKeycloakAdminEndpoints, createKeycloakAdminClient, isKeycloakAdminError, keycloakAdminConfigSchema, parseKeycloakAdminConfig };
|
package/dist/index.js
CHANGED
|
@@ -1,702 +1,324 @@
|
|
|
1
|
-
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
var
|
|
6
|
-
var
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
// src/keycloak-admin.constant.ts
|
|
2
|
+
var KEYCLOAK_ADMIN_GRANT_TYPE_CLIENT_CREDENTIALS = "client_credentials";
|
|
3
|
+
var KEYCLOAK_ADMIN_FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
|
|
4
|
+
var KEYCLOAK_ADMIN_JSON_CONTENT_TYPE = "application/json";
|
|
5
|
+
var KEYCLOAK_ADMIN_REDACTED = "[REDACTED]";
|
|
6
|
+
var KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS = 3e4;
|
|
7
|
+
var KEYCLOAK_ADMIN_ERROR_CODE = {
|
|
8
|
+
CONFIGURATION_INVALID: "KEYCLOAK_ADMIN_CONFIGURATION_INVALID",
|
|
9
|
+
REQUEST_FAILED: "KEYCLOAK_ADMIN_REQUEST_FAILED",
|
|
10
|
+
TOKEN_REQUEST_FAILED: "KEYCLOAK_ADMIN_TOKEN_REQUEST_FAILED",
|
|
11
|
+
TOKEN_RESPONSE_INVALID: "KEYCLOAK_ADMIN_TOKEN_RESPONSE_INVALID",
|
|
12
|
+
USER_ALREADY_EXISTS: "KEYCLOAK_ADMIN_USER_ALREADY_EXISTS",
|
|
13
|
+
USER_ID_MISSING: "KEYCLOAK_ADMIN_USER_ID_MISSING",
|
|
14
|
+
USER_NOT_FOUND: "KEYCLOAK_ADMIN_USER_NOT_FOUND"
|
|
9
15
|
};
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
var index_exports = {};
|
|
22
|
-
__export(index_exports, {
|
|
23
|
-
KEYCLOAK_ADMIN_CLIENT: () => KEYCLOAK_ADMIN_CLIENT,
|
|
24
|
-
KEYCLOAK_ADMIN_CONFIG: () => KEYCLOAK_ADMIN_CONFIG,
|
|
25
|
-
KEYCLOAK_ADMIN_PROVIDER: () => KEYCLOAK_ADMIN_PROVIDER,
|
|
26
|
-
KeycloakAdminClient: () => KeycloakAdminClient,
|
|
27
|
-
KeycloakAdminError: () => KeycloakAdminError,
|
|
28
|
-
KeycloakAdminModule: () => KeycloakAdminModule,
|
|
29
|
-
validateKeycloakAdminConfig: () => validateKeycloakAdminConfig
|
|
30
|
-
});
|
|
31
|
-
module.exports = __toCommonJS(index_exports);
|
|
32
|
-
|
|
33
|
-
// src/keycloak-admin.module.ts
|
|
34
|
-
var import_common2 = require("@nestjs/common");
|
|
35
|
-
var import_http_client2 = require("@adatechnology/http-client");
|
|
36
|
-
|
|
37
|
-
// src/keycloak-admin.token.ts
|
|
38
|
-
var KEYCLOAK_ADMIN_CLIENT = "KEYCLOAK_ADMIN_CLIENT";
|
|
39
|
-
var KEYCLOAK_ADMIN_CONFIG = "KEYCLOAK_ADMIN_CONFIG";
|
|
40
|
-
var KEYCLOAK_ADMIN_PROVIDER = "KEYCLOAK_ADMIN_PROVIDER";
|
|
41
|
-
|
|
42
|
-
// src/keycloak-admin.client.ts
|
|
43
|
-
var import_common = require("@nestjs/common");
|
|
44
|
-
var import_http_client = require("@adatechnology/http-client");
|
|
45
|
-
var import_logger = require("@adatechnology/logger");
|
|
16
|
+
function buildKeycloakAdminEndpoints({ baseUrl, realm }) {
|
|
17
|
+
const origin = baseUrl.replace(/\/+$/, "");
|
|
18
|
+
const encodedRealm = encodeURIComponent(realm);
|
|
19
|
+
const users = `${origin}/admin/realms/${encodedRealm}/users`;
|
|
20
|
+
return {
|
|
21
|
+
token: `${origin}/realms/${encodedRealm}/protocol/openid-connect/token`,
|
|
22
|
+
user: (userId) => `${users}/${encodeURIComponent(userId)}`,
|
|
23
|
+
userPassword: (userId) => `${users}/${encodeURIComponent(userId)}/reset-password`,
|
|
24
|
+
users
|
|
25
|
+
};
|
|
26
|
+
}
|
|
46
27
|
|
|
47
|
-
// src/
|
|
48
|
-
var
|
|
49
|
-
statusCode;
|
|
28
|
+
// src/keycloak-admin.error.ts
|
|
29
|
+
var KeycloakAdminError = class _KeycloakAdminError extends Error {
|
|
50
30
|
code;
|
|
51
31
|
context;
|
|
52
|
-
|
|
53
|
-
|
|
32
|
+
status;
|
|
33
|
+
constructor({ code, context = {}, message, status }) {
|
|
34
|
+
super(message);
|
|
54
35
|
this.name = "KeycloakAdminError";
|
|
55
|
-
this.
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.context = context;
|
|
38
|
+
this.status = status;
|
|
58
39
|
Object.setPrototypeOf(this, _KeycloakAdminError.prototype);
|
|
59
40
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
publishConfig: {
|
|
69
|
-
access: "public"
|
|
70
|
-
},
|
|
71
|
-
main: "dist/index.js",
|
|
72
|
-
module: "dist/index.mjs",
|
|
73
|
-
types: "dist/index.d.ts",
|
|
74
|
-
files: [
|
|
75
|
-
"dist"
|
|
76
|
-
],
|
|
77
|
-
scripts: {
|
|
78
|
-
build: "rm -rf dist && tsup && cp index.d.ts dist/index.d.ts",
|
|
79
|
-
"build:watch": "tsup --watch",
|
|
80
|
-
check: "tsc -p tsconfig.json --noEmit"
|
|
81
|
-
},
|
|
82
|
-
dependencies: {
|
|
83
|
-
"@adatechnology/http-client": "workspace:*",
|
|
84
|
-
"@adatechnology/logger": "workspace:*"
|
|
85
|
-
},
|
|
86
|
-
peerDependencies: {
|
|
87
|
-
"@nestjs/common": "^11.0.16",
|
|
88
|
-
"@nestjs/core": "^11"
|
|
89
|
-
},
|
|
90
|
-
devDependencies: {
|
|
91
|
-
"@adatechnology/shared": "workspace:*",
|
|
92
|
-
"@esbuild-plugins/tsconfig-paths": "^0.1.2",
|
|
93
|
-
"@swc/core": "^1.15.24",
|
|
94
|
-
tsup: "^8.5.1",
|
|
95
|
-
typescript: "^5.2.0"
|
|
41
|
+
toJSON() {
|
|
42
|
+
return {
|
|
43
|
+
code: this.code,
|
|
44
|
+
context: this.context,
|
|
45
|
+
message: this.message,
|
|
46
|
+
name: this.name,
|
|
47
|
+
status: this.status
|
|
48
|
+
};
|
|
96
49
|
}
|
|
97
50
|
};
|
|
51
|
+
function isKeycloakAdminError(value) {
|
|
52
|
+
return value instanceof KeycloakAdminError;
|
|
53
|
+
}
|
|
98
54
|
|
|
99
|
-
// src/
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
var KEYCLOAK_ADMIN_BEARER_PREFIX = "Bearer ";
|
|
108
|
-
var KEYCLOAK_ADMIN_ENDPOINTS = {
|
|
109
|
-
MASTER_TOKEN: "/realms/master/protocol/openid-connect/token",
|
|
110
|
-
ADMIN_USERS: /* @__PURE__ */ __name((baseUrl, realm, userId) => userId ? `${baseUrl}/admin/realms/${realm}/users/${userId}` : `${baseUrl}/admin/realms/${realm}/users`, "ADMIN_USERS"),
|
|
111
|
-
ADMIN_RESET_PASSWORD: /* @__PURE__ */ __name((baseUrl, realm, userId) => `${baseUrl}/admin/realms/${realm}/users/${userId}/reset-password`, "ADMIN_RESET_PASSWORD"),
|
|
112
|
-
ADMIN_SEND_VERIFY_EMAIL: /* @__PURE__ */ __name((baseUrl, realm, userId) => `${baseUrl}/admin/realms/${realm}/users/${userId}/send-verify-email`, "ADMIN_SEND_VERIFY_EMAIL")
|
|
113
|
-
};
|
|
114
|
-
var KEYCLOAK_ADMIN_ERROR_CODES = {
|
|
115
|
-
ADMIN_TOKEN_ERROR: "KEYCLOAK_ADMIN_TOKEN_ERROR",
|
|
116
|
-
CREATE_USER_ERROR: "KEYCLOAK_CREATE_USER_ERROR",
|
|
117
|
-
UPDATE_USER_ERROR: "KEYCLOAK_UPDATE_USER_ERROR",
|
|
118
|
-
RESET_PASSWORD_ERROR: "KEYCLOAK_RESET_PASSWORD_ERROR",
|
|
119
|
-
TOGGLE_ENABLED_ERROR: "KEYCLOAK_TOGGLE_ENABLED_ERROR",
|
|
120
|
-
DELETE_USER_ERROR: "KEYCLOAK_DELETE_USER_ERROR",
|
|
121
|
-
UPDATE_ATTRIBUTES_ERROR: "KEYCLOAK_UPDATE_ATTRIBUTES_ERROR",
|
|
122
|
-
SEND_VERIFY_EMAIL_ERROR: "KEYCLOAK_SEND_VERIFY_EMAIL_ERROR"
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
// src/utils/extract-http-error.ts
|
|
126
|
-
function extractHttpError(err) {
|
|
127
|
-
const unknownErr = err;
|
|
128
|
-
const response = unknownErr == null ? void 0 : unknownErr.response;
|
|
129
|
-
const responseData = response == null ? void 0 : response.data;
|
|
130
|
-
let statusCode = void 0;
|
|
131
|
-
if (typeof (unknownErr == null ? void 0 : unknownErr.status) === "number") {
|
|
132
|
-
statusCode = unknownErr.status;
|
|
133
|
-
} else if (typeof (response == null ? void 0 : response.status) === "number") {
|
|
134
|
-
statusCode = response.status;
|
|
55
|
+
// src/keycloak-admin.schema.ts
|
|
56
|
+
import { z } from "zod";
|
|
57
|
+
function isAbsoluteHttpUrl(value) {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(value);
|
|
60
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
135
63
|
}
|
|
136
|
-
const details = responseData ?? (unknownErr == null ? void 0 : unknownErr.message);
|
|
137
|
-
const rawError = (responseData == null ? void 0 : responseData.error) ?? (responseData == null ? void 0 : responseData.errorMessage);
|
|
138
|
-
const errorCode = typeof rawError === "string" ? rawError : typeof (unknownErr == null ? void 0 : unknownErr.code) === "string" ? unknownErr.code : void 0;
|
|
139
|
-
return {
|
|
140
|
-
statusCode,
|
|
141
|
-
details,
|
|
142
|
-
errorCode
|
|
143
|
-
};
|
|
144
64
|
}
|
|
145
|
-
|
|
65
|
+
var keycloakAdminConfigSchema = z.object({
|
|
66
|
+
baseUrl: z.string().min(1).refine(isAbsoluteHttpUrl, { message: "must be an absolute http(s) URL" }),
|
|
67
|
+
clientId: z.string().min(1),
|
|
68
|
+
clientSecret: z.string().min(1),
|
|
69
|
+
realm: z.string().min(1)
|
|
70
|
+
});
|
|
71
|
+
function parseKeycloakAdminConfig(value) {
|
|
72
|
+
const parsed = keycloakAdminConfigSchema.safeParse(value);
|
|
73
|
+
if (parsed.success) return parsed.data;
|
|
74
|
+
throw new KeycloakAdminError({
|
|
75
|
+
code: KEYCLOAK_ADMIN_ERROR_CODE.CONFIGURATION_INVALID,
|
|
76
|
+
context: { fields: parsed.error.issues.map((issue) => issue.path.join(".")).sort() },
|
|
77
|
+
message: "Invalid Keycloak admin configuration"
|
|
78
|
+
});
|
|
79
|
+
}
|
|
146
80
|
|
|
147
|
-
// src/keycloak-admin.
|
|
148
|
-
function
|
|
149
|
-
|
|
150
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
151
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
152
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
81
|
+
// src/keycloak-admin.redaction.ts
|
|
82
|
+
function replaceAll({ secrets, text }) {
|
|
83
|
+
return secrets.reduce((current, secret) => current.split(secret).join(KEYCLOAK_ADMIN_REDACTED), text);
|
|
153
84
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (
|
|
85
|
+
function redactDeep({ secrets, value }) {
|
|
86
|
+
if (typeof value === "string") return replaceAll({ secrets, text: value });
|
|
87
|
+
if (Array.isArray(value)) return value.map((item) => redactDeep({ secrets, value: item }));
|
|
88
|
+
if (value !== null && typeof value === "object") {
|
|
89
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactDeep({ secrets, value: item })]));
|
|
90
|
+
}
|
|
91
|
+
return value;
|
|
157
92
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return
|
|
161
|
-
|
|
93
|
+
function createSecretRedactor(...secrets) {
|
|
94
|
+
const known = secrets.filter((secret) => typeof secret === "string" && secret !== "");
|
|
95
|
+
return {
|
|
96
|
+
text(value) {
|
|
97
|
+
return replaceAll({ secrets: known, text: value });
|
|
98
|
+
},
|
|
99
|
+
value(value) {
|
|
100
|
+
return redactDeep({ secrets: known, value });
|
|
101
|
+
},
|
|
102
|
+
with(...additional) {
|
|
103
|
+
return createSecretRedactor(...known, ...additional);
|
|
104
|
+
}
|
|
162
105
|
};
|
|
163
106
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
107
|
+
|
|
108
|
+
// src/keycloak-admin.response.ts
|
|
109
|
+
var MAX_DETAIL_LENGTH = 200;
|
|
110
|
+
async function readKeycloakDetail(response) {
|
|
111
|
+
const text = await response.text().catch(() => "");
|
|
112
|
+
if (text === "") return void 0;
|
|
113
|
+
try {
|
|
114
|
+
const parsed = JSON.parse(text);
|
|
115
|
+
if (parsed === null || typeof parsed !== "object") return text.slice(0, MAX_DETAIL_LENGTH);
|
|
116
|
+
const payload = parsed;
|
|
117
|
+
const detail = payload.errorMessage ?? payload.error_description ?? payload.error;
|
|
118
|
+
return typeof detail === "string" ? detail.slice(0, MAX_DETAIL_LENGTH) : void 0;
|
|
119
|
+
} catch {
|
|
120
|
+
return text.slice(0, MAX_DETAIL_LENGTH);
|
|
174
121
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/keycloak-admin.token.ts
|
|
125
|
+
function createKeycloakTokenProvider({
|
|
126
|
+
config,
|
|
127
|
+
endpoint,
|
|
128
|
+
fetch: fetchImpl,
|
|
129
|
+
now,
|
|
130
|
+
redactor
|
|
131
|
+
}) {
|
|
132
|
+
let cached;
|
|
133
|
+
let inFlight;
|
|
134
|
+
function isFresh(state) {
|
|
135
|
+
return state.expiresAtMs - KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS > now();
|
|
188
136
|
}
|
|
189
|
-
async
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
137
|
+
async function requestToken() {
|
|
138
|
+
const requestedAtMs = now();
|
|
139
|
+
const response = await fetchImpl(endpoint, {
|
|
140
|
+
body: new URLSearchParams({
|
|
141
|
+
client_id: config.clientId,
|
|
142
|
+
client_secret: config.clientSecret,
|
|
143
|
+
grant_type: KEYCLOAK_ADMIN_GRANT_TYPE_CLIENT_CREDENTIALS
|
|
144
|
+
}),
|
|
145
|
+
headers: { "content-type": KEYCLOAK_ADMIN_FORM_CONTENT_TYPE },
|
|
146
|
+
method: "POST"
|
|
193
147
|
});
|
|
194
|
-
|
|
195
|
-
const body = new URLSearchParams();
|
|
196
|
-
body.append("grant_type", KEYCLOAK_ADMIN_GRANT_TYPE_PASSWORD);
|
|
197
|
-
body.append("client_id", KEYCLOAK_ADMIN_CLIENT_ID_ADMIN_CLI);
|
|
198
|
-
body.append("username", this.config.adminUser);
|
|
199
|
-
body.append("password", this.config.adminPassword);
|
|
200
|
-
try {
|
|
201
|
-
const response = await this.httpProvider.post({
|
|
202
|
-
url,
|
|
203
|
-
data: body,
|
|
204
|
-
config: {
|
|
205
|
-
headers: {
|
|
206
|
-
"Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_FORM
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
const result = {
|
|
211
|
-
accessToken: response.data.access_token,
|
|
212
|
-
expiresIn: response.data.expires_in,
|
|
213
|
-
tokenType: response.data.token_type
|
|
214
|
-
};
|
|
215
|
-
this.log("info", `${method} - Success`, method);
|
|
216
|
-
return result;
|
|
217
|
-
} catch (err) {
|
|
218
|
-
const { statusCode, details, errorCode } = extractHttpError(err);
|
|
219
|
-
this.log("error", `${method} - Failed`, method, {
|
|
220
|
-
statusCode,
|
|
221
|
-
errorCode
|
|
222
|
-
});
|
|
148
|
+
if (!response.ok) {
|
|
223
149
|
throw new KeycloakAdminError({
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
url,
|
|
229
|
-
method: "POST",
|
|
230
|
-
details,
|
|
231
|
-
errorCode
|
|
232
|
-
}
|
|
150
|
+
code: KEYCLOAK_ADMIN_ERROR_CODE.TOKEN_REQUEST_FAILED,
|
|
151
|
+
context: redactor.value({ detail: await readKeycloakDetail(response), realm: config.realm }),
|
|
152
|
+
message: "Keycloak refused the service account token request",
|
|
153
|
+
status: response.status
|
|
233
154
|
});
|
|
234
155
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
var _a, _b;
|
|
238
|
-
const method = "createUser";
|
|
239
|
-
const { username, email, firstName, lastName, enabled, emailVerified, credentials, attributes, adminToken } = params;
|
|
240
|
-
this.log("info", `${method} - Start`, method, {
|
|
241
|
-
username,
|
|
242
|
-
email
|
|
243
|
-
});
|
|
244
|
-
const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm);
|
|
245
|
-
const userData = {
|
|
246
|
-
username,
|
|
247
|
-
email,
|
|
248
|
-
firstName,
|
|
249
|
-
lastName,
|
|
250
|
-
enabled,
|
|
251
|
-
emailVerified,
|
|
252
|
-
...credentials ? {
|
|
253
|
-
credentials
|
|
254
|
-
} : {},
|
|
255
|
-
...attributes ? {
|
|
256
|
-
attributes
|
|
257
|
-
} : {}
|
|
258
|
-
};
|
|
259
|
-
try {
|
|
260
|
-
const response = await this.httpProvider.post({
|
|
261
|
-
url,
|
|
262
|
-
data: userData,
|
|
263
|
-
config: {
|
|
264
|
-
headers: {
|
|
265
|
-
[KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
|
|
266
|
-
"Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
});
|
|
270
|
-
const locationHeader = ((_a = response.headers) == null ? void 0 : _a["location"]) ?? ((_b = response.headers) == null ? void 0 : _b["Location"]);
|
|
271
|
-
const userId = typeof locationHeader === "string" ? locationHeader.split("/").pop() ?? "" : "";
|
|
272
|
-
this.log("info", `${method} - Success`, method, {
|
|
273
|
-
username,
|
|
274
|
-
email,
|
|
275
|
-
userId
|
|
276
|
-
});
|
|
277
|
-
return userId;
|
|
278
|
-
} catch (err) {
|
|
279
|
-
const { statusCode, details, errorCode } = extractHttpError(err);
|
|
280
|
-
this.log("error", `${method} - Failed`, method, {
|
|
281
|
-
username,
|
|
282
|
-
email,
|
|
283
|
-
statusCode,
|
|
284
|
-
errorCode
|
|
285
|
-
});
|
|
156
|
+
const payload = await response.json();
|
|
157
|
+
if (typeof payload.access_token !== "string" || typeof payload.expires_in !== "number") {
|
|
286
158
|
throw new KeycloakAdminError({
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
url,
|
|
292
|
-
method: "POST",
|
|
293
|
-
username,
|
|
294
|
-
email,
|
|
295
|
-
details,
|
|
296
|
-
errorCode
|
|
297
|
-
}
|
|
159
|
+
code: KEYCLOAK_ADMIN_ERROR_CODE.TOKEN_RESPONSE_INVALID,
|
|
160
|
+
context: { realm: config.realm },
|
|
161
|
+
message: "Keycloak returned a token response without access_token or expires_in",
|
|
162
|
+
status: response.status
|
|
298
163
|
});
|
|
299
164
|
}
|
|
165
|
+
return {
|
|
166
|
+
accessToken: payload.access_token,
|
|
167
|
+
expiresAtMs: requestedAtMs + payload.expires_in * 1e3
|
|
168
|
+
};
|
|
300
169
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
config: {
|
|
313
|
-
headers: {
|
|
314
|
-
[KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
|
|
315
|
-
"Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
this.log("info", `${method} - Success`, method, {
|
|
320
|
-
userId
|
|
321
|
-
});
|
|
322
|
-
} catch (err) {
|
|
323
|
-
const { statusCode, details, errorCode } = extractHttpError(err);
|
|
324
|
-
this.log("error", `${method} - Failed`, method, {
|
|
325
|
-
userId,
|
|
326
|
-
statusCode,
|
|
327
|
-
errorCode
|
|
328
|
-
});
|
|
329
|
-
throw new KeycloakAdminError({
|
|
330
|
-
message: "Keycloak update user failed",
|
|
331
|
-
statusCode: statusCode ?? 502,
|
|
332
|
-
code: KEYCLOAK_ADMIN_ERROR_CODES.UPDATE_USER_ERROR,
|
|
333
|
-
context: {
|
|
334
|
-
url,
|
|
335
|
-
method: "PUT",
|
|
336
|
-
userId,
|
|
337
|
-
details,
|
|
338
|
-
errorCode
|
|
339
|
-
}
|
|
340
|
-
});
|
|
170
|
+
return {
|
|
171
|
+
async getAccessToken() {
|
|
172
|
+
if (cached && isFresh(cached)) return cached.accessToken;
|
|
173
|
+
inFlight ??= requestToken();
|
|
174
|
+
try {
|
|
175
|
+
const state = await inFlight;
|
|
176
|
+
cached = state;
|
|
177
|
+
return state.accessToken;
|
|
178
|
+
} finally {
|
|
179
|
+
inFlight = void 0;
|
|
180
|
+
}
|
|
341
181
|
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// src/keycloak-admin.client.ts
|
|
186
|
+
var HTTP_CONFLICT = 409;
|
|
187
|
+
var HTTP_NOT_FOUND = 404;
|
|
188
|
+
function errorCodeOf(status) {
|
|
189
|
+
if (status === HTTP_NOT_FOUND) return KEYCLOAK_ADMIN_ERROR_CODE.USER_NOT_FOUND;
|
|
190
|
+
if (status === HTTP_CONFLICT) return KEYCLOAK_ADMIN_ERROR_CODE.USER_ALREADY_EXISTS;
|
|
191
|
+
return KEYCLOAK_ADMIN_ERROR_CODE.REQUEST_FAILED;
|
|
192
|
+
}
|
|
193
|
+
function normalizeAttributes(attributes) {
|
|
194
|
+
return Object.fromEntries(
|
|
195
|
+
Object.entries(attributes).map(([key, value]) => [key, typeof value === "string" ? [value] : [...value]])
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
function userIdFromLocation(response) {
|
|
199
|
+
const location = response.headers.get("location");
|
|
200
|
+
const id = location?.split("/").filter(Boolean).at(-1);
|
|
201
|
+
if (id === void 0 || id === "") {
|
|
202
|
+
throw new KeycloakAdminError({
|
|
203
|
+
code: KEYCLOAK_ADMIN_ERROR_CODE.USER_ID_MISSING,
|
|
204
|
+
message: "Keycloak created the user without a usable Location header",
|
|
205
|
+
status: response.status
|
|
349
206
|
});
|
|
350
|
-
const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_RESET_PASSWORD(this.config.baseUrl, this.config.realm, userId);
|
|
351
|
-
try {
|
|
352
|
-
await this.httpProvider.put({
|
|
353
|
-
url,
|
|
354
|
-
data: {
|
|
355
|
-
type: "password",
|
|
356
|
-
value: password,
|
|
357
|
-
temporary
|
|
358
|
-
},
|
|
359
|
-
config: {
|
|
360
|
-
headers: {
|
|
361
|
-
[KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
|
|
362
|
-
"Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
this.log("info", `${method} - Success`, method, {
|
|
367
|
-
userId
|
|
368
|
-
});
|
|
369
|
-
} catch (err) {
|
|
370
|
-
const { statusCode, details, errorCode } = extractHttpError(err);
|
|
371
|
-
this.log("error", `${method} - Failed`, method, {
|
|
372
|
-
userId,
|
|
373
|
-
statusCode,
|
|
374
|
-
errorCode
|
|
375
|
-
});
|
|
376
|
-
throw new KeycloakAdminError({
|
|
377
|
-
message: "Keycloak reset password failed",
|
|
378
|
-
statusCode: statusCode ?? 502,
|
|
379
|
-
code: KEYCLOAK_ADMIN_ERROR_CODES.RESET_PASSWORD_ERROR,
|
|
380
|
-
context: {
|
|
381
|
-
url,
|
|
382
|
-
method: "PUT",
|
|
383
|
-
userId,
|
|
384
|
-
details,
|
|
385
|
-
errorCode
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
207
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
208
|
+
return id;
|
|
209
|
+
}
|
|
210
|
+
function createKeycloakAdminClient({
|
|
211
|
+
config: rawConfig,
|
|
212
|
+
fetch: injectedFetch,
|
|
213
|
+
now = Date.now
|
|
214
|
+
}) {
|
|
215
|
+
const config = parseKeycloakAdminConfig(rawConfig);
|
|
216
|
+
const endpoints = buildKeycloakAdminEndpoints(config);
|
|
217
|
+
const redactor = createSecretRedactor(config.clientSecret);
|
|
218
|
+
const fetchImpl = injectedFetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
219
|
+
const tokenProvider = createKeycloakTokenProvider({
|
|
220
|
+
config,
|
|
221
|
+
endpoint: endpoints.token,
|
|
222
|
+
fetch: fetchImpl,
|
|
223
|
+
now,
|
|
224
|
+
redactor
|
|
225
|
+
});
|
|
226
|
+
async function adminRequest({ body, method, secrets = [], url }) {
|
|
227
|
+
const accessToken = await tokenProvider.getAccessToken();
|
|
228
|
+
const response = await fetchImpl(url, {
|
|
229
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
230
|
+
headers: {
|
|
231
|
+
authorization: `Bearer ${accessToken}`,
|
|
232
|
+
...body === void 0 ? {} : { "content-type": KEYCLOAK_ADMIN_JSON_CONTENT_TYPE }
|
|
233
|
+
},
|
|
234
|
+
method
|
|
396
235
|
});
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
this.log("info", `${method} - Success`, method, {
|
|
412
|
-
userId,
|
|
413
|
-
enabled
|
|
414
|
-
});
|
|
415
|
-
} catch (err) {
|
|
416
|
-
const { statusCode, details, errorCode } = extractHttpError(err);
|
|
417
|
-
this.log("error", `${method} - Failed`, method, {
|
|
418
|
-
userId,
|
|
419
|
-
enabled,
|
|
420
|
-
statusCode,
|
|
421
|
-
errorCode
|
|
422
|
-
});
|
|
423
|
-
throw new KeycloakAdminError({
|
|
424
|
-
message: "Keycloak toggle user enabled failed",
|
|
425
|
-
statusCode: statusCode ?? 502,
|
|
426
|
-
code: KEYCLOAK_ADMIN_ERROR_CODES.TOGGLE_ENABLED_ERROR,
|
|
427
|
-
context: {
|
|
428
|
-
url,
|
|
429
|
-
method: "PUT",
|
|
430
|
-
userId,
|
|
431
|
-
enabled,
|
|
432
|
-
details,
|
|
433
|
-
errorCode
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
async deleteUser(params) {
|
|
439
|
-
const method = "deleteUser";
|
|
440
|
-
const { userId, adminToken } = params;
|
|
441
|
-
this.log("info", `${method} - Start`, method, {
|
|
442
|
-
userId
|
|
236
|
+
if (response.ok) return response;
|
|
237
|
+
const callRedactor = redactor.with(accessToken, ...secrets);
|
|
238
|
+
throw new KeycloakAdminError({
|
|
239
|
+
code: errorCodeOf(response.status),
|
|
240
|
+
context: callRedactor.value({
|
|
241
|
+
detail: await readKeycloakDetail(response),
|
|
242
|
+
method,
|
|
243
|
+
realm: config.realm
|
|
244
|
+
}),
|
|
245
|
+
message: callRedactor.text(`Keycloak admin request failed with status ${response.status}`),
|
|
246
|
+
status: response.status
|
|
443
247
|
});
|
|
444
|
-
const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm, userId);
|
|
445
|
-
try {
|
|
446
|
-
await this.httpProvider.delete({
|
|
447
|
-
url,
|
|
448
|
-
config: {
|
|
449
|
-
headers: {
|
|
450
|
-
[KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
this.log("info", `${method} - Success`, method, {
|
|
455
|
-
userId
|
|
456
|
-
});
|
|
457
|
-
} catch (err) {
|
|
458
|
-
const { statusCode, details, errorCode } = extractHttpError(err);
|
|
459
|
-
this.log("error", `${method} - Failed`, method, {
|
|
460
|
-
userId,
|
|
461
|
-
statusCode,
|
|
462
|
-
errorCode
|
|
463
|
-
});
|
|
464
|
-
throw new KeycloakAdminError({
|
|
465
|
-
message: "Keycloak delete user failed",
|
|
466
|
-
statusCode: statusCode ?? 502,
|
|
467
|
-
code: KEYCLOAK_ADMIN_ERROR_CODES.DELETE_USER_ERROR,
|
|
468
|
-
context: {
|
|
469
|
-
url,
|
|
470
|
-
method: "DELETE",
|
|
471
|
-
userId,
|
|
472
|
-
details,
|
|
473
|
-
errorCode
|
|
474
|
-
}
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
248
|
}
|
|
478
|
-
async
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
249
|
+
async function setPassword({ password, temporary, userId }) {
|
|
250
|
+
await adminRequest({
|
|
251
|
+
body: { temporary, type: "password", value: password },
|
|
252
|
+
method: "PUT",
|
|
253
|
+
secrets: [password],
|
|
254
|
+
url: endpoints.userPassword(userId)
|
|
484
255
|
});
|
|
485
|
-
const url = KEYCLOAK_ADMIN_ENDPOINTS.ADMIN_USERS(this.config.baseUrl, this.config.realm, userId);
|
|
486
|
-
try {
|
|
487
|
-
await this.httpProvider.put({
|
|
488
|
-
url,
|
|
489
|
-
data: {
|
|
490
|
-
attributes
|
|
491
|
-
},
|
|
492
|
-
config: {
|
|
493
|
-
headers: {
|
|
494
|
-
[KEYCLOAK_ADMIN_AUTHORIZATION_HEADER]: `${KEYCLOAK_ADMIN_BEARER_PREFIX}${adminToken}`,
|
|
495
|
-
"Content-Type": KEYCLOAK_ADMIN_CONTENT_TYPE_JSON
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
this.log("info", `${method} - Success`, method, {
|
|
500
|
-
userId
|
|
501
|
-
});
|
|
502
|
-
} catch (err) {
|
|
503
|
-
const { statusCode, details, errorCode } = extractHttpError(err);
|
|
504
|
-
this.log("error", `${method} - Failed`, method, {
|
|
505
|
-
userId,
|
|
506
|
-
statusCode,
|
|
507
|
-
errorCode
|
|
508
|
-
});
|
|
509
|
-
throw new KeycloakAdminError({
|
|
510
|
-
message: "Keycloak update user attributes failed",
|
|
511
|
-
statusCode: statusCode ?? 502,
|
|
512
|
-
code: KEYCLOAK_ADMIN_ERROR_CODES.UPDATE_ATTRIBUTES_ERROR,
|
|
513
|
-
context: {
|
|
514
|
-
url,
|
|
515
|
-
method: "PUT",
|
|
516
|
-
userId,
|
|
517
|
-
details,
|
|
518
|
-
errorCode
|
|
519
|
-
}
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
256
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
257
|
+
return {
|
|
258
|
+
async createUser({
|
|
259
|
+
attributes,
|
|
260
|
+
email,
|
|
261
|
+
emailVerified,
|
|
262
|
+
enabled,
|
|
263
|
+
firstName,
|
|
264
|
+
lastName,
|
|
265
|
+
password,
|
|
266
|
+
username
|
|
267
|
+
}) {
|
|
268
|
+
const response = await adminRequest({
|
|
269
|
+
body: {
|
|
270
|
+
...attributes === void 0 ? {} : { attributes: normalizeAttributes(attributes) },
|
|
271
|
+
...password === void 0 ? {} : {
|
|
272
|
+
credentials: [{ temporary: password.temporary, type: "password", value: password.value }]
|
|
273
|
+
},
|
|
274
|
+
email,
|
|
275
|
+
emailVerified: emailVerified ?? false,
|
|
276
|
+
enabled: enabled ?? true,
|
|
277
|
+
firstName,
|
|
278
|
+
lastName,
|
|
279
|
+
username
|
|
280
|
+
},
|
|
281
|
+
method: "POST",
|
|
282
|
+
secrets: [password?.value],
|
|
283
|
+
url: endpoints.users
|
|
550
284
|
});
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
285
|
+
return { id: userIdFromLocation(response) };
|
|
286
|
+
},
|
|
287
|
+
async deleteUser({ userId }) {
|
|
288
|
+
await adminRequest({ method: "DELETE", url: endpoints.user(userId) });
|
|
289
|
+
},
|
|
290
|
+
async findUserByEmail({ email }) {
|
|
291
|
+
const query = new URLSearchParams({ email, exact: "true" });
|
|
292
|
+
const response = await adminRequest({ method: "GET", url: `${endpoints.users}?${query}` });
|
|
293
|
+
const found = await response.json();
|
|
294
|
+
return found.at(0);
|
|
295
|
+
},
|
|
296
|
+
async setEnabled({ enabled, userId }) {
|
|
297
|
+
await adminRequest({ body: { enabled }, method: "PUT", url: endpoints.user(userId) });
|
|
298
|
+
},
|
|
299
|
+
setPassword,
|
|
300
|
+
async setTemporaryPassword({ password, userId }) {
|
|
301
|
+
await setPassword({ password, temporary: true, userId });
|
|
302
|
+
},
|
|
303
|
+
async updateAttributes({ attributes, userId }) {
|
|
304
|
+
await adminRequest({
|
|
305
|
+
body: { attributes: normalizeAttributes(attributes) },
|
|
306
|
+
method: "PUT",
|
|
307
|
+
url: endpoints.user(userId)
|
|
562
308
|
});
|
|
309
|
+
},
|
|
310
|
+
async updateUser({ user, userId }) {
|
|
311
|
+
await adminRequest({ body: user, method: "PUT", url: endpoints.user(userId) });
|
|
563
312
|
}
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
__name(_KeycloakAdminClient, "KeycloakAdminClient");
|
|
567
|
-
var KeycloakAdminClient = _KeycloakAdminClient;
|
|
568
|
-
KeycloakAdminClient = _ts_decorate([
|
|
569
|
-
(0, import_common.Injectable)(),
|
|
570
|
-
_ts_param(0, (0, import_common.Inject)(KEYCLOAK_ADMIN_CONFIG)),
|
|
571
|
-
_ts_param(1, (0, import_common.Inject)(import_http_client.HTTP_PROVIDER)),
|
|
572
|
-
_ts_param(2, (0, import_common.Optional)()),
|
|
573
|
-
_ts_param(2, (0, import_common.Inject)(import_logger.LOGGER_PROVIDER)),
|
|
574
|
-
_ts_metadata("design:type", Function),
|
|
575
|
-
_ts_metadata("design:paramtypes", [
|
|
576
|
-
typeof KeycloakAdminConfig === "undefined" ? Object : KeycloakAdminConfig,
|
|
577
|
-
typeof HttpProviderInterface === "undefined" ? Object : HttpProviderInterface,
|
|
578
|
-
typeof LoggerProviderInterface === "undefined" ? Object : LoggerProviderInterface
|
|
579
|
-
])
|
|
580
|
-
], KeycloakAdminClient);
|
|
581
|
-
|
|
582
|
-
// src/utils/validate-config.ts
|
|
583
|
-
function validateKeycloakAdminConfig(config) {
|
|
584
|
-
const issues = [];
|
|
585
|
-
if (!config) {
|
|
586
|
-
throw new KeycloakAdminError({
|
|
587
|
-
message: "KeycloakAdminConfig is required. Provide an object with baseUrl, realm, adminUser and adminPassword.",
|
|
588
|
-
statusCode: 400,
|
|
589
|
-
code: "KEYCLOAK_ADMIN_CONFIG_MISSING"
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
if (typeof config.baseUrl !== "string" || config.baseUrl.trim() === "") {
|
|
593
|
-
issues.push({
|
|
594
|
-
field: "baseUrl",
|
|
595
|
-
message: "baseUrl is required and must be a non-empty string (e.g. 'http://localhost:8081')"
|
|
596
|
-
});
|
|
597
|
-
} else if (!config.baseUrl.startsWith("http://") && !config.baseUrl.startsWith("https://")) {
|
|
598
|
-
issues.push({
|
|
599
|
-
field: "baseUrl",
|
|
600
|
-
message: `baseUrl must start with 'http://' or 'https://'. Received: '${config.baseUrl}'`
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
if (typeof config.realm !== "string" || config.realm.trim() === "") {
|
|
604
|
-
issues.push({
|
|
605
|
-
field: "realm",
|
|
606
|
-
message: "realm is required and must be a non-empty string (e.g. 'BACKEND')"
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
if (typeof config.adminUser !== "string" || config.adminUser.trim() === "") {
|
|
610
|
-
issues.push({
|
|
611
|
-
field: "adminUser",
|
|
612
|
-
message: "adminUser is required and must be a non-empty string"
|
|
613
|
-
});
|
|
614
|
-
}
|
|
615
|
-
if (typeof config.adminPassword !== "string" || config.adminPassword.trim() === "") {
|
|
616
|
-
issues.push({
|
|
617
|
-
field: "adminPassword",
|
|
618
|
-
message: "adminPassword is required and must be a non-empty string"
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
if (issues.length > 0) {
|
|
622
|
-
const summary = issues.map((i) => ` - ${i.field}: ${i.message}`).join("\n");
|
|
623
|
-
throw new KeycloakAdminError({
|
|
624
|
-
message: `KeycloakAdminConfig validation failed:
|
|
625
|
-
${summary}`,
|
|
626
|
-
statusCode: 400,
|
|
627
|
-
code: "KEYCLOAK_ADMIN_CONFIG_INVALID",
|
|
628
|
-
context: {
|
|
629
|
-
issues
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
__name(validateKeycloakAdminConfig, "validateKeycloakAdminConfig");
|
|
635
|
-
|
|
636
|
-
// src/keycloak-admin.module.ts
|
|
637
|
-
function _ts_decorate2(decorators, target, key, desc) {
|
|
638
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
639
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
640
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
641
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
313
|
+
};
|
|
642
314
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
validateKeycloakAdminConfig(config);
|
|
647
|
-
return {
|
|
648
|
-
module: _KeycloakAdminModule,
|
|
649
|
-
global: true,
|
|
650
|
-
imports: [
|
|
651
|
-
import_http_client2.HttpModule.forRoot({
|
|
652
|
-
baseURL: config.baseUrl,
|
|
653
|
-
timeout: 5e3
|
|
654
|
-
}, {
|
|
655
|
-
logging: {
|
|
656
|
-
enabled: true,
|
|
657
|
-
includeBody: true,
|
|
658
|
-
context: "KeycloakAdminHttpClient",
|
|
659
|
-
environments: [
|
|
660
|
-
"development",
|
|
661
|
-
"test"
|
|
662
|
-
]
|
|
663
|
-
}
|
|
664
|
-
})
|
|
665
|
-
],
|
|
666
|
-
providers: [
|
|
667
|
-
{
|
|
668
|
-
provide: KEYCLOAK_ADMIN_CONFIG,
|
|
669
|
-
useValue: config
|
|
670
|
-
},
|
|
671
|
-
{
|
|
672
|
-
provide: KEYCLOAK_ADMIN_CLIENT,
|
|
673
|
-
useClass: KeycloakAdminClient
|
|
674
|
-
},
|
|
675
|
-
{
|
|
676
|
-
provide: KEYCLOAK_ADMIN_PROVIDER,
|
|
677
|
-
useExisting: KEYCLOAK_ADMIN_CLIENT
|
|
678
|
-
}
|
|
679
|
-
],
|
|
680
|
-
exports: [
|
|
681
|
-
KEYCLOAK_ADMIN_CLIENT,
|
|
682
|
-
KEYCLOAK_ADMIN_PROVIDER,
|
|
683
|
-
KEYCLOAK_ADMIN_CONFIG
|
|
684
|
-
]
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
};
|
|
688
|
-
__name(_KeycloakAdminModule, "KeycloakAdminModule");
|
|
689
|
-
var KeycloakAdminModule = _KeycloakAdminModule;
|
|
690
|
-
KeycloakAdminModule = _ts_decorate2([
|
|
691
|
-
(0, import_common2.Module)({})
|
|
692
|
-
], KeycloakAdminModule);
|
|
693
|
-
// Annotate the CommonJS export names for ESM import in node:
|
|
694
|
-
0 && (module.exports = {
|
|
695
|
-
KEYCLOAK_ADMIN_CLIENT,
|
|
696
|
-
KEYCLOAK_ADMIN_CONFIG,
|
|
697
|
-
KEYCLOAK_ADMIN_PROVIDER,
|
|
698
|
-
KeycloakAdminClient,
|
|
315
|
+
export {
|
|
316
|
+
KEYCLOAK_ADMIN_ERROR_CODE,
|
|
317
|
+
KEYCLOAK_ADMIN_TOKEN_RENEWAL_SKEW_MS,
|
|
699
318
|
KeycloakAdminError,
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
319
|
+
buildKeycloakAdminEndpoints,
|
|
320
|
+
createKeycloakAdminClient,
|
|
321
|
+
isKeycloakAdminError,
|
|
322
|
+
keycloakAdminConfigSchema,
|
|
323
|
+
parseKeycloakAdminConfig
|
|
324
|
+
};
|
package/package.json
CHANGED
|
@@ -1,33 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adatechnology/keycloak-admin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
},
|
|
3
|
+
"version": "1.0.0-rc.0",
|
|
4
|
+
"description": "Agnostic Keycloak Admin API client authenticated as a service account (client_credentials)",
|
|
5
|
+
"type": "module",
|
|
7
6
|
"main": "dist/index.js",
|
|
8
|
-
"module": "dist/index.mjs",
|
|
9
7
|
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
10
14
|
"files": [
|
|
11
15
|
"dist"
|
|
12
16
|
],
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
+
"keywords": [
|
|
18
|
+
"keycloak",
|
|
19
|
+
"admin",
|
|
20
|
+
"client-credentials",
|
|
21
|
+
"service-account",
|
|
22
|
+
"identity"
|
|
23
|
+
],
|
|
17
24
|
"peerDependencies": {
|
|
18
|
-
"
|
|
19
|
-
"@nestjs/core": "^11"
|
|
25
|
+
"zod": "^3.24.1 || ^4.0.0"
|
|
20
26
|
},
|
|
21
27
|
"devDependencies": {
|
|
22
|
-
"@
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
|
|
28
|
+
"@types/bun": "latest",
|
|
29
|
+
"tsup": "^8.0.0",
|
|
30
|
+
"typescript": "^5.0.0",
|
|
31
|
+
"zod": "^4.4.3"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
27
36
|
},
|
|
28
37
|
"scripts": {
|
|
29
|
-
"build": "
|
|
30
|
-
"
|
|
31
|
-
"
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"check": "tsc --noEmit",
|
|
40
|
+
"test": "bun test"
|
|
32
41
|
}
|
|
33
42
|
}
|