mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
added ability to refresh token when within time (#45789) (#45813)
Some checks are pending
Keycloak CI / Version Compatibility Matrix (push) Waiting to run
Keycloak CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak CI / Build (push) Blocked by required conditions
Keycloak CI / Base UT (push) Blocked by required conditions
Keycloak CI / Base IT (push) Blocked by required conditions
Keycloak CI / Adapter IT (push) Blocked by required conditions
Keycloak CI / Adapter IT Strict Cookies (push) Blocked by required conditions
Keycloak CI / Quarkus UT (push) Blocked by required conditions
Keycloak CI / Quarkus IT (push) Blocked by required conditions
Keycloak CI / Java Distribution IT/UT (push) Blocked by required conditions
Keycloak CI / Login Theme v1 tests (push) Blocked by required conditions
Keycloak CI / Volatile Sessions IT (push) Blocked by required conditions
Keycloak CI / External Infinispan IT (push) Blocked by required conditions
Keycloak CI / AuroraDB IT (push) Blocked by required conditions
Keycloak CI / AzureDB IT (push) Blocked by required conditions
Keycloak CI / Store IT (push) Blocked by required conditions
Keycloak CI / Store IT (additional) (push) Blocked by required conditions
Keycloak CI / Store Model Tests (push) Blocked by required conditions
Keycloak CI / Clustering IT (push) Blocked by required conditions
Keycloak CI / FIPS UT (push) Blocked by required conditions
Keycloak CI / FIPS IT (push) Blocked by required conditions
Keycloak CI / Forms IT (push) Blocked by required conditions
Keycloak CI / WebAuthn IT (push) Blocked by required conditions
Keycloak CI / SSSD (push) Blocked by required conditions
Keycloak CI / Migration Tests (push) Blocked by required conditions
Keycloak CI / Test Framework (push) Blocked by required conditions
Keycloak CI / Base IT (new) (push) Blocked by required conditions
Keycloak CI / Admin v2 (push) Blocked by required conditions
Keycloak CI / Cluster Compatibility Tests (push) Blocked by required conditions
Keycloak CI / Status Check - Keycloak CI (push) Blocked by required conditions
CodeQL / Check conditional workflows and jobs (push) Waiting to run
CodeQL / CodeQL Java (push) Blocked by required conditions
CodeQL / CodeQL JavaScript (push) Blocked by required conditions
CodeQL / CodeQL TypeScript (push) Blocked by required conditions
CodeQL / CodeQL GitHub Actions (push) Blocked by required conditions
CodeQL / Status Check - CodeQL (push) Blocked by required conditions
Keycloak Documentation / Check conditional workflows and jobs (push) Waiting to run
Keycloak Documentation / Build (push) Blocked by required conditions
Keycloak Documentation / External links check (push) Blocked by required conditions
Keycloak Documentation / Status Check - Keycloak Documentation (push) Blocked by required conditions
Keycloak Guides / Check conditional workflows and jobs (push) Waiting to run
Keycloak Guides / Build (push) Blocked by required conditions
Keycloak Guides / Status Check - Keycloak Guides (push) Blocked by required conditions
Keycloak JavaScript CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak JavaScript CI / Build Keycloak (push) Blocked by required conditions
Keycloak JavaScript CI / Admin Client (push) Blocked by required conditions
Keycloak JavaScript CI / UI Shared (push) Blocked by required conditions
Keycloak JavaScript CI / Account UI (push) Blocked by required conditions
Keycloak JavaScript CI / Admin UI (push) Blocked by required conditions
Keycloak JavaScript CI / Account UI E2E (push) Blocked by required conditions
Keycloak JavaScript CI / Admin UI E2E (push) Blocked by required conditions
Keycloak JavaScript CI / Keycloak Admin Client (push) Blocked by required conditions
Keycloak JavaScript CI / Status Check - Keycloak JavaScript CI (push) Blocked by required conditions
Keycloak Operator CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak Operator CI / Build distribution (push) Blocked by required conditions
Keycloak Operator CI / Test local apiserver (push) Blocked by required conditions
Keycloak Operator CI / Test remote (push) Blocked by required conditions
Keycloak Operator CI / Test OLM installation (push) Blocked by required conditions
Keycloak Operator CI / Status Check - Keycloak Operator CI (push) Blocked by required conditions
Some checks are pending
Keycloak CI / Version Compatibility Matrix (push) Waiting to run
Keycloak CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak CI / Build (push) Blocked by required conditions
Keycloak CI / Base UT (push) Blocked by required conditions
Keycloak CI / Base IT (push) Blocked by required conditions
Keycloak CI / Adapter IT (push) Blocked by required conditions
Keycloak CI / Adapter IT Strict Cookies (push) Blocked by required conditions
Keycloak CI / Quarkus UT (push) Blocked by required conditions
Keycloak CI / Quarkus IT (push) Blocked by required conditions
Keycloak CI / Java Distribution IT/UT (push) Blocked by required conditions
Keycloak CI / Login Theme v1 tests (push) Blocked by required conditions
Keycloak CI / Volatile Sessions IT (push) Blocked by required conditions
Keycloak CI / External Infinispan IT (push) Blocked by required conditions
Keycloak CI / AuroraDB IT (push) Blocked by required conditions
Keycloak CI / AzureDB IT (push) Blocked by required conditions
Keycloak CI / Store IT (push) Blocked by required conditions
Keycloak CI / Store IT (additional) (push) Blocked by required conditions
Keycloak CI / Store Model Tests (push) Blocked by required conditions
Keycloak CI / Clustering IT (push) Blocked by required conditions
Keycloak CI / FIPS UT (push) Blocked by required conditions
Keycloak CI / FIPS IT (push) Blocked by required conditions
Keycloak CI / Forms IT (push) Blocked by required conditions
Keycloak CI / WebAuthn IT (push) Blocked by required conditions
Keycloak CI / SSSD (push) Blocked by required conditions
Keycloak CI / Migration Tests (push) Blocked by required conditions
Keycloak CI / Test Framework (push) Blocked by required conditions
Keycloak CI / Base IT (new) (push) Blocked by required conditions
Keycloak CI / Admin v2 (push) Blocked by required conditions
Keycloak CI / Cluster Compatibility Tests (push) Blocked by required conditions
Keycloak CI / Status Check - Keycloak CI (push) Blocked by required conditions
CodeQL / Check conditional workflows and jobs (push) Waiting to run
CodeQL / CodeQL Java (push) Blocked by required conditions
CodeQL / CodeQL JavaScript (push) Blocked by required conditions
CodeQL / CodeQL TypeScript (push) Blocked by required conditions
CodeQL / CodeQL GitHub Actions (push) Blocked by required conditions
CodeQL / Status Check - CodeQL (push) Blocked by required conditions
Keycloak Documentation / Check conditional workflows and jobs (push) Waiting to run
Keycloak Documentation / Build (push) Blocked by required conditions
Keycloak Documentation / External links check (push) Blocked by required conditions
Keycloak Documentation / Status Check - Keycloak Documentation (push) Blocked by required conditions
Keycloak Guides / Check conditional workflows and jobs (push) Waiting to run
Keycloak Guides / Build (push) Blocked by required conditions
Keycloak Guides / Status Check - Keycloak Guides (push) Blocked by required conditions
Keycloak JavaScript CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak JavaScript CI / Build Keycloak (push) Blocked by required conditions
Keycloak JavaScript CI / Admin Client (push) Blocked by required conditions
Keycloak JavaScript CI / UI Shared (push) Blocked by required conditions
Keycloak JavaScript CI / Account UI (push) Blocked by required conditions
Keycloak JavaScript CI / Admin UI (push) Blocked by required conditions
Keycloak JavaScript CI / Account UI E2E (push) Blocked by required conditions
Keycloak JavaScript CI / Admin UI E2E (push) Blocked by required conditions
Keycloak JavaScript CI / Keycloak Admin Client (push) Blocked by required conditions
Keycloak JavaScript CI / Status Check - Keycloak JavaScript CI (push) Blocked by required conditions
Keycloak Operator CI / Check conditional workflows and jobs (push) Waiting to run
Keycloak Operator CI / Build distribution (push) Blocked by required conditions
Keycloak Operator CI / Test local apiserver (push) Blocked by required conditions
Keycloak Operator CI / Test remote (push) Blocked by required conditions
Keycloak Operator CI / Test OLM installation (push) Blocked by required conditions
Keycloak Operator CI / Status Check - Keycloak Operator CI (push) Blocked by required conditions
fixes: #44379
(cherry picked from commit ffc19d997e)
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
9d0b171b3d
commit
05b7a79efa
2 changed files with 137 additions and 5 deletions
|
|
@ -16,8 +16,9 @@ import { ServerInfo } from "./resources/serverInfo.js";
|
|||
import { Users } from "./resources/users.js";
|
||||
import { UserStorageProvider } from "./resources/userStorageProvider.js";
|
||||
import { WhoAmI } from "./resources/whoAmI.js";
|
||||
import { Credentials, getToken } from "./utils/auth.js";
|
||||
import { Credentials, getToken, Settings } from "./utils/auth.js";
|
||||
import { defaultBaseUrl, defaultRealm } from "./utils/constants.js";
|
||||
import { DecodedToken, decodeToken } from "./utils/decode.js";
|
||||
|
||||
export type RequestOptions = Omit<RequestInit, "signal">;
|
||||
|
||||
|
|
@ -33,6 +34,8 @@ export interface ConnectionConfig {
|
|||
timeout?: number;
|
||||
}
|
||||
|
||||
const MIN_VALIDITY = 5; // in seconds
|
||||
|
||||
export class KeycloakAdminClient {
|
||||
// Resources
|
||||
public users: Users;
|
||||
|
|
@ -64,6 +67,9 @@ export class KeycloakAdminClient {
|
|||
#requestOptions?: RequestOptions;
|
||||
#globalRequestArgOptions?: Pick<RequestArgs, "catchNotFound">;
|
||||
#tokenProvider?: TokenProvider;
|
||||
#accessTokenDecoded?: DecodedToken;
|
||||
#refreshTokenDecoded?: DecodedToken;
|
||||
#credentials?: Credentials;
|
||||
|
||||
constructor(connectionConfig?: ConnectionConfig) {
|
||||
this.baseUrl = connectionConfig?.baseUrl || defaultBaseUrl;
|
||||
|
|
@ -93,7 +99,16 @@ export class KeycloakAdminClient {
|
|||
}
|
||||
|
||||
public async auth(credentials: Credentials) {
|
||||
const { accessToken, refreshToken } = await getToken({
|
||||
const { accessToken, refreshToken } = await getToken(
|
||||
this.#getTokenSettings(credentials),
|
||||
);
|
||||
this.#credentials = credentials;
|
||||
this.setAccessToken(accessToken);
|
||||
this.setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
#getTokenSettings(credentials: Credentials): Settings {
|
||||
return {
|
||||
baseUrl: this.baseUrl,
|
||||
realmName: this.realmName,
|
||||
scope: this.scope,
|
||||
|
|
@ -102,9 +117,7 @@ export class KeycloakAdminClient {
|
|||
...this.#requestOptions,
|
||||
...(this.timeout ? { signal: AbortSignal.timeout(this.timeout) } : {}),
|
||||
},
|
||||
});
|
||||
this.accessToken = accessToken;
|
||||
this.refreshToken = refreshToken;
|
||||
};
|
||||
}
|
||||
|
||||
public registerTokenProvider(provider: TokenProvider) {
|
||||
|
|
@ -117,6 +130,12 @@ export class KeycloakAdminClient {
|
|||
|
||||
public setAccessToken(token: string) {
|
||||
this.accessToken = token;
|
||||
this.#accessTokenDecoded = decodeToken(token);
|
||||
}
|
||||
|
||||
public setRefreshToken(token: string) {
|
||||
this.refreshToken = token;
|
||||
this.#refreshTokenDecoded = decodeToken(token);
|
||||
}
|
||||
|
||||
public async getAccessToken() {
|
||||
|
|
@ -124,9 +143,54 @@ export class KeycloakAdminClient {
|
|||
return this.#tokenProvider.getAccessToken();
|
||||
}
|
||||
|
||||
if (this.isTokenExpired()) {
|
||||
await this.#refreshAccessToken();
|
||||
}
|
||||
|
||||
return this.accessToken;
|
||||
}
|
||||
|
||||
async #refreshAccessToken() {
|
||||
if (!this.refreshToken || !this.#credentials) {
|
||||
throw new Error(
|
||||
"Cannot refresh token: missing refresh token or credentials",
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isRefreshTokenExpired()) {
|
||||
throw new Error("Cannot refresh token: refresh token has expired");
|
||||
}
|
||||
|
||||
const { accessToken, refreshToken } = await getToken(
|
||||
this.#getTokenSettings({
|
||||
grantType: "refresh_token",
|
||||
clientId: this.#credentials.clientId,
|
||||
clientSecret: this.#credentials.clientSecret,
|
||||
refreshToken: this.refreshToken,
|
||||
}),
|
||||
);
|
||||
|
||||
this.setAccessToken(accessToken);
|
||||
this.setRefreshToken(refreshToken);
|
||||
}
|
||||
|
||||
public isTokenExpired(): boolean {
|
||||
return this.#isExpired(this.#accessTokenDecoded);
|
||||
}
|
||||
|
||||
public isRefreshTokenExpired(): boolean {
|
||||
return this.#isExpired(this.#refreshTokenDecoded);
|
||||
}
|
||||
|
||||
#isExpired(token?: DecodedToken): boolean {
|
||||
if (typeof token?.exp !== "number") {
|
||||
return false;
|
||||
}
|
||||
const expiresIn =
|
||||
token.exp - Math.ceil(new Date().getTime() / 1000) - MIN_VALIDITY;
|
||||
return expiresIn < 0;
|
||||
}
|
||||
|
||||
public getRequestOptions() {
|
||||
return this.#requestOptions;
|
||||
}
|
||||
|
|
|
|||
68
js/libs/keycloak-admin-client/src/utils/decode.ts
Normal file
68
js/libs/keycloak-admin-client/src/utils/decode.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
export interface DecodedToken {
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
export function decodeToken(token: string): DecodedToken {
|
||||
const [, payload] = token.split(".");
|
||||
|
||||
if (typeof payload !== "string") {
|
||||
throw new Error("Unable to decode token, payload not found.");
|
||||
}
|
||||
|
||||
let decoded;
|
||||
|
||||
try {
|
||||
decoded = base64UrlDecode(payload);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"Unable to decode token, payload is not a valid Base64URL value.",
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(decoded);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
"Unable to decode token, payload is not a valid JSON value.",
|
||||
{ cause: error },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function base64UrlDecode(input: string): string {
|
||||
let output = input.replaceAll("-", "+").replaceAll("_", "/");
|
||||
|
||||
switch (output.length % 4) {
|
||||
case 0:
|
||||
break;
|
||||
case 2:
|
||||
output += "==";
|
||||
break;
|
||||
case 3:
|
||||
output += "=";
|
||||
break;
|
||||
default:
|
||||
throw new Error("Input is not of the correct length.");
|
||||
}
|
||||
|
||||
try {
|
||||
return b64DecodeUnicode(output);
|
||||
} catch {
|
||||
return atob(output);
|
||||
}
|
||||
}
|
||||
|
||||
function b64DecodeUnicode(input: string): string {
|
||||
return decodeURIComponent(
|
||||
atob(input).replace(/(.)/g, (m, p) => {
|
||||
let code = p.charCodeAt(0).toString(16).toUpperCase();
|
||||
|
||||
if (code.length < 2) {
|
||||
code = "0" + code;
|
||||
}
|
||||
|
||||
return "%" + code;
|
||||
}),
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue