keycloak/js/libs/keycloak-admin-client/src/client.ts
Michal Vavřík ec1ddc73d4
Added typescript based module for the client admin v2 (#46440)
* Added typescript based module for the client admin v2

Based on the new openapi client admin api this module can be generated
based on the defenition.

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* now uses openapitools to generate and moved it into the existing module for better adoption

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* changed back to use kiota as it offers a nicer fluent api

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* fixed build

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* better api

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* removed base representation filter

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* added flag to explicited enable v2

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* re-run generation

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>

* enable client-admin-api:v2 in PR CI tests

Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>

* fix JS OpenAPI generation on Windows

Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>

* remove unnecessary statement from generate.ts

Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>

* Fix Windows line endings in JS OpenAPI post-processing

Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>

---------

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
Signed-off-by: Michal Vavřík <michal.vavrik@aol.com>
Co-authored-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
2026-02-18 19:34:00 +01:00

229 lines
7.1 KiB
TypeScript

import type { RequestArgs } from "./resources/agent.js";
import { AttackDetection } from "./resources/attackDetection.js";
import { AuthenticationManagement } from "./resources/authenticationManagement.js";
import { Cache } from "./resources/cache.js";
import { ClientPolicies } from "./resources/clientPolicies.js";
import { Clients } from "./resources/clients.js";
import { ClientScopes } from "./resources/clientScopes.js";
import { Components } from "./resources/components.js";
import { Groups } from "./resources/groups.js";
import { IdentityProviders } from "./resources/identityProviders.js";
import { Realms } from "./resources/realms.js";
import { Organizations } from "./resources/organizations.js";
import { Workflows } from "./resources/workflows.js";
import { Roles } from "./resources/roles.js";
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, 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">;
export interface TokenProvider {
getAccessToken: () => Promise<string | undefined>;
}
export interface ConnectionConfig {
baseUrl?: string;
realmName?: string;
requestOptions?: RequestOptions;
requestArgOptions?: Pick<RequestArgs, "catchNotFound">;
timeout?: number;
/**
* Enable experimental APIs (e.g., v2 API).
* These APIs are not yet stable and may change without notice.
* @default false
*/
enableExperimentalApis?: boolean;
}
const MIN_VALIDITY = 5; // in seconds
export class KeycloakAdminClient {
// Resources
public users: Users;
public userStorageProvider: UserStorageProvider;
public groups: Groups;
public roles: Roles;
public organizations: Organizations;
public workflows: Workflows;
public clients: Clients;
public realms: Realms;
public clientScopes: ClientScopes;
public clientPolicies: ClientPolicies;
public identityProviders: IdentityProviders;
public components: Components;
public serverInfo: ServerInfo;
public whoAmI: WhoAmI;
public attackDetection: AttackDetection;
public authenticationManagement: AuthenticationManagement;
public cache: Cache;
// Members
public baseUrl: string;
public realmName: string;
public scope?: string;
public accessToken?: string;
public refreshToken?: string;
public timeout?: number;
public enableExperimentalApis: boolean;
#requestOptions?: RequestOptions;
#globalRequestArgOptions?: Pick<RequestArgs, "catchNotFound">;
#tokenProvider?: TokenProvider;
#accessTokenDecoded?: DecodedToken;
#refreshTokenDecoded?: DecodedToken;
#credentials?: Credentials;
constructor(connectionConfig?: ConnectionConfig) {
this.baseUrl = connectionConfig?.baseUrl || defaultBaseUrl;
this.realmName = connectionConfig?.realmName || defaultRealm;
this.timeout = connectionConfig?.timeout;
this.enableExperimentalApis =
connectionConfig?.enableExperimentalApis ?? false;
this.#requestOptions = connectionConfig?.requestOptions;
this.#globalRequestArgOptions = connectionConfig?.requestArgOptions;
// Initialize resources
this.users = new Users(this);
this.userStorageProvider = new UserStorageProvider(this);
this.groups = new Groups(this);
this.roles = new Roles(this);
this.organizations = new Organizations(this);
this.workflows = new Workflows(this);
this.clients = new Clients(this);
this.realms = new Realms(this);
this.clientScopes = new ClientScopes(this);
this.clientPolicies = new ClientPolicies(this);
this.identityProviders = new IdentityProviders(this);
this.components = new Components(this);
this.authenticationManagement = new AuthenticationManagement(this);
this.serverInfo = new ServerInfo(this);
this.whoAmI = new WhoAmI(this);
this.attackDetection = new AttackDetection(this);
this.cache = new Cache(this);
}
public async auth(credentials: Credentials) {
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,
credentials,
requestOptions: {
...this.#requestOptions,
...(this.timeout ? { signal: AbortSignal.timeout(this.timeout) } : {}),
},
};
}
public registerTokenProvider(provider: TokenProvider) {
if (this.#tokenProvider) {
throw new Error("An existing token provider was already registered.");
}
this.#tokenProvider = provider;
}
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() {
if (this.#tokenProvider) {
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;
}
public getGlobalRequestArgOptions():
| Pick<RequestArgs, "catchNotFound">
| undefined {
return this.#globalRequestArgOptions;
}
public setConfig(connectionConfig: ConnectionConfig) {
if (
typeof connectionConfig.baseUrl === "string" &&
connectionConfig.baseUrl
) {
this.baseUrl = connectionConfig.baseUrl;
}
if (
typeof connectionConfig.realmName === "string" &&
connectionConfig.realmName
) {
this.realmName = connectionConfig.realmName;
}
this.#requestOptions = connectionConfig.requestOptions;
}
}