@keycloak/keycloak-admin-client 26.5.2 → 26.5.4
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/lib/client.d.ts +3 -0
- package/lib/client.js +50 -4
- package/lib/utils/decode.d.ts +4 -0
- package/lib/utils/decode.js +50 -0
- package/package.json +1 -1
package/lib/client.d.ts
CHANGED
|
@@ -57,7 +57,10 @@ export declare class KeycloakAdminClient {
|
|
|
57
57
|
auth(credentials: Credentials): Promise<void>;
|
|
58
58
|
registerTokenProvider(provider: TokenProvider): void;
|
|
59
59
|
setAccessToken(token: string): void;
|
|
60
|
+
setRefreshToken(token: string): void;
|
|
60
61
|
getAccessToken(): Promise<string | undefined>;
|
|
62
|
+
isTokenExpired(): boolean;
|
|
63
|
+
isRefreshTokenExpired(): boolean;
|
|
61
64
|
getRequestOptions(): RequestOptions | undefined;
|
|
62
65
|
getGlobalRequestArgOptions(): Pick<RequestArgs, "catchNotFound"> | undefined;
|
|
63
66
|
setConfig(connectionConfig: ConnectionConfig): void;
|
package/lib/client.js
CHANGED
|
@@ -17,6 +17,8 @@ import { UserStorageProvider } from "./resources/userStorageProvider.js";
|
|
|
17
17
|
import { WhoAmI } from "./resources/whoAmI.js";
|
|
18
18
|
import { getToken } from "./utils/auth.js";
|
|
19
19
|
import { defaultBaseUrl, defaultRealm } from "./utils/constants.js";
|
|
20
|
+
import { decodeToken } from "./utils/decode.js";
|
|
21
|
+
const MIN_VALIDITY = 5; // in seconds
|
|
20
22
|
export class KeycloakAdminClient {
|
|
21
23
|
// Resources
|
|
22
24
|
users;
|
|
@@ -46,6 +48,9 @@ export class KeycloakAdminClient {
|
|
|
46
48
|
#requestOptions;
|
|
47
49
|
#globalRequestArgOptions;
|
|
48
50
|
#tokenProvider;
|
|
51
|
+
#accessTokenDecoded;
|
|
52
|
+
#refreshTokenDecoded;
|
|
53
|
+
#credentials;
|
|
49
54
|
constructor(connectionConfig) {
|
|
50
55
|
this.baseUrl = connectionConfig?.baseUrl || defaultBaseUrl;
|
|
51
56
|
this.realmName = connectionConfig?.realmName || defaultRealm;
|
|
@@ -72,7 +77,13 @@ export class KeycloakAdminClient {
|
|
|
72
77
|
this.cache = new Cache(this);
|
|
73
78
|
}
|
|
74
79
|
async auth(credentials) {
|
|
75
|
-
const { accessToken, refreshToken } = await getToken(
|
|
80
|
+
const { accessToken, refreshToken } = await getToken(this.#getTokenSettings(credentials));
|
|
81
|
+
this.#credentials = credentials;
|
|
82
|
+
this.setAccessToken(accessToken);
|
|
83
|
+
this.setRefreshToken(refreshToken);
|
|
84
|
+
}
|
|
85
|
+
#getTokenSettings(credentials) {
|
|
86
|
+
return {
|
|
76
87
|
baseUrl: this.baseUrl,
|
|
77
88
|
realmName: this.realmName,
|
|
78
89
|
scope: this.scope,
|
|
@@ -81,9 +92,7 @@ export class KeycloakAdminClient {
|
|
|
81
92
|
...this.#requestOptions,
|
|
82
93
|
...(this.timeout ? { signal: AbortSignal.timeout(this.timeout) } : {}),
|
|
83
94
|
},
|
|
84
|
-
}
|
|
85
|
-
this.accessToken = accessToken;
|
|
86
|
-
this.refreshToken = refreshToken;
|
|
95
|
+
};
|
|
87
96
|
}
|
|
88
97
|
registerTokenProvider(provider) {
|
|
89
98
|
if (this.#tokenProvider) {
|
|
@@ -93,13 +102,50 @@ export class KeycloakAdminClient {
|
|
|
93
102
|
}
|
|
94
103
|
setAccessToken(token) {
|
|
95
104
|
this.accessToken = token;
|
|
105
|
+
this.#accessTokenDecoded = decodeToken(token);
|
|
106
|
+
}
|
|
107
|
+
setRefreshToken(token) {
|
|
108
|
+
this.refreshToken = token;
|
|
109
|
+
this.#refreshTokenDecoded = decodeToken(token);
|
|
96
110
|
}
|
|
97
111
|
async getAccessToken() {
|
|
98
112
|
if (this.#tokenProvider) {
|
|
99
113
|
return this.#tokenProvider.getAccessToken();
|
|
100
114
|
}
|
|
115
|
+
if (this.isTokenExpired()) {
|
|
116
|
+
await this.#refreshAccessToken();
|
|
117
|
+
}
|
|
101
118
|
return this.accessToken;
|
|
102
119
|
}
|
|
120
|
+
async #refreshAccessToken() {
|
|
121
|
+
if (!this.refreshToken || !this.#credentials) {
|
|
122
|
+
throw new Error("Cannot refresh token: missing refresh token or credentials");
|
|
123
|
+
}
|
|
124
|
+
if (this.isRefreshTokenExpired()) {
|
|
125
|
+
throw new Error("Cannot refresh token: refresh token has expired");
|
|
126
|
+
}
|
|
127
|
+
const { accessToken, refreshToken } = await getToken(this.#getTokenSettings({
|
|
128
|
+
grantType: "refresh_token",
|
|
129
|
+
clientId: this.#credentials.clientId,
|
|
130
|
+
clientSecret: this.#credentials.clientSecret,
|
|
131
|
+
refreshToken: this.refreshToken,
|
|
132
|
+
}));
|
|
133
|
+
this.setAccessToken(accessToken);
|
|
134
|
+
this.setRefreshToken(refreshToken);
|
|
135
|
+
}
|
|
136
|
+
isTokenExpired() {
|
|
137
|
+
return this.#isExpired(this.#accessTokenDecoded);
|
|
138
|
+
}
|
|
139
|
+
isRefreshTokenExpired() {
|
|
140
|
+
return this.#isExpired(this.#refreshTokenDecoded);
|
|
141
|
+
}
|
|
142
|
+
#isExpired(token) {
|
|
143
|
+
if (typeof token?.exp !== "number") {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const expiresIn = token.exp - Math.ceil(new Date().getTime() / 1000) - MIN_VALIDITY;
|
|
147
|
+
return expiresIn < 0;
|
|
148
|
+
}
|
|
103
149
|
getRequestOptions() {
|
|
104
150
|
return this.#requestOptions;
|
|
105
151
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export function decodeToken(token) {
|
|
2
|
+
const [, payload] = token?.split(".") || [];
|
|
3
|
+
if (typeof payload !== "string") {
|
|
4
|
+
console.info("Unable to decode token, payload not found.");
|
|
5
|
+
return {};
|
|
6
|
+
}
|
|
7
|
+
let decoded;
|
|
8
|
+
try {
|
|
9
|
+
decoded = base64UrlDecode(payload);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
throw new Error("Unable to decode token, payload is not a valid Base64URL value.", { cause: error });
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(decoded);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
throw new Error("Unable to decode token, payload is not a valid JSON value.", { cause: error });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function base64UrlDecode(input) {
|
|
22
|
+
let output = input.replaceAll("-", "+").replaceAll("_", "/");
|
|
23
|
+
switch (output.length % 4) {
|
|
24
|
+
case 0:
|
|
25
|
+
break;
|
|
26
|
+
case 2:
|
|
27
|
+
output += "==";
|
|
28
|
+
break;
|
|
29
|
+
case 3:
|
|
30
|
+
output += "=";
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
throw new Error("Input is not of the correct length.");
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
return b64DecodeUnicode(output);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return atob(output);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function b64DecodeUnicode(input) {
|
|
43
|
+
return decodeURIComponent(atob(input).replace(/(.)/g, (m, p) => {
|
|
44
|
+
let code = p.charCodeAt(0).toString(16).toUpperCase();
|
|
45
|
+
if (code.length < 2) {
|
|
46
|
+
code = "0" + code;
|
|
47
|
+
}
|
|
48
|
+
return "%" + code;
|
|
49
|
+
}));
|
|
50
|
+
}
|