@keycloak/keycloak-admin-client 26.5.2 → 26.5.3

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 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,4 @@
1
+ export interface DecodedToken {
2
+ exp?: number;
3
+ }
4
+ export declare function decodeToken(token: string): DecodedToken;
@@ -0,0 +1,49 @@
1
+ export function decodeToken(token) {
2
+ const [, payload] = token.split(".");
3
+ if (typeof payload !== "string") {
4
+ throw new Error("Unable to decode token, payload not found.");
5
+ }
6
+ let decoded;
7
+ try {
8
+ decoded = base64UrlDecode(payload);
9
+ }
10
+ catch (error) {
11
+ throw new Error("Unable to decode token, payload is not a valid Base64URL value.", { cause: error });
12
+ }
13
+ try {
14
+ return JSON.parse(decoded);
15
+ }
16
+ catch (error) {
17
+ throw new Error("Unable to decode token, payload is not a valid JSON value.", { cause: error });
18
+ }
19
+ }
20
+ function base64UrlDecode(input) {
21
+ let output = input.replaceAll("-", "+").replaceAll("_", "/");
22
+ switch (output.length % 4) {
23
+ case 0:
24
+ break;
25
+ case 2:
26
+ output += "==";
27
+ break;
28
+ case 3:
29
+ output += "=";
30
+ break;
31
+ default:
32
+ throw new Error("Input is not of the correct length.");
33
+ }
34
+ try {
35
+ return b64DecodeUnicode(output);
36
+ }
37
+ catch {
38
+ return atob(output);
39
+ }
40
+ }
41
+ function b64DecodeUnicode(input) {
42
+ return decodeURIComponent(atob(input).replace(/(.)/g, (m, p) => {
43
+ let code = p.charCodeAt(0).toString(16).toUpperCase();
44
+ if (code.length < 2) {
45
+ code = "0" + code;
46
+ }
47
+ return "%" + code;
48
+ }));
49
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@keycloak/keycloak-admin-client",
3
- "version": "26.5.2",
3
+ "version": "26.5.3",
4
4
  "description": "A client to interact with Keycloak's Administration API",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",