JavaScript adapter for Keycloak providing OpenID Connect authentication and UMA authorization for web applications.
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
User-Managed Access (UMA) and policy enforcement capabilities providing fine-grained authorization for applications protected by Keycloak policy enforcers.
import Keycloak from "keycloak-js";
import KeycloakAuthorization from "keycloak-js/keycloak-authz";
// Create Keycloak instance first
const keycloak = new Keycloak({
url: "http://keycloak-server",
realm: "my-realm",
clientId: "my-app"
});
// Create authorization instance
const authorization = new KeycloakAuthorization(keycloak);
// Initialize Keycloak before using authorization
await keycloak.init();Creates an authorization client instance for UMA and policy enforcement.
/**
* Creates an authorization client for UMA and policy enforcement
* @param keycloak - Initialized Keycloak instance
*/
constructor(keycloak: Keycloak);Initializes the authorization client. This method is deprecated but still available.
/**
* Initializes authorization client (deprecated)
* @returns Promise that resolves when initialization completes
* @deprecated This method is deprecated and may be removed in future versions
*/
init(): Promise<void>;Usage Example:
import Keycloak from "keycloak-js";
import KeycloakAuthorization from "keycloak-js/keycloak-authz";
const keycloak = new Keycloak({
url: "https://auth.mycompany.com",
realm: "my-realm",
clientId: "resource-server"
});
await keycloak.init();
const authorization = new KeycloakAuthorization(keycloak);Requests authorization based on permission ticket from UMA-protected resource server.
/**
* Requests authorization using permission ticket or resource permissions
* @param request - Authorization request with ticket or permissions
* @returns Promise resolving to Requesting Party Token (RPT)
*/
authorize(request: AuthorizationRequest): Promise<string>;Usage Examples:
// Handle 401 response with permission ticket
async function handleUnauthorizedResponse(response) {
const wwwAuthHeader = response.headers.get("WWW-Authenticate");
const ticket = extractTicketFromHeader(wwwAuthHeader);
try {
const rpt = await authorization.authorize({ ticket });
// Retry original request with RPT
const retryResponse = await fetch(originalUrl, {
headers: {
Authorization: `Bearer ${rpt}`
}
});
return retryResponse;
} catch (error) {
console.error("Authorization denied:", error);
showAccessDeniedMessage();
}
}
// Request specific resource permissions
const rpt = await authorization.authorize({
permissions: [
{
id: "photo-album",
scopes: ["view", "edit"]
},
{
name: "document-folder",
scopes: ["view"]
}
]
});
// Request with multiple tickets
const rpt2 = await authorization.authorize({
tickets: ["ticket1", "ticket2"],
incrementalAuthorization: true
});Obtains entitlements (RPT) for a specific resource server.
/**
* Obtains entitlements (RPT) for specified resource server
* @param resourceServerId - Client ID of the resource server
* @param request - Optional authorization request with specific permissions
* @returns Promise resolving to Requesting Party Token (RPT)
*/
entitlement(resourceServerId: string, request?: AuthorizationRequest): Promise<string>;Usage Examples:
// Get all permissions user can access for resource server
const rpt = await authorization.entitlement("photo-service");
// Get specific permissions for resource server
const rpt = await authorization.entitlement("document-service", {
permissions: [
{
name: "quarterly-reports",
scopes: ["view", "download"]
}
]
});
// Use RPT for subsequent API calls
const response = await fetch("/api/documents/quarterly-reports", {
headers: {
Authorization: `Bearer ${rpt}`
}
});Gets the current Requesting Party Token.
/**
* Current Requesting Party Token (RPT) with permissions
*/
rpt?: string;Usage Example:
// Check if we have a current RPT
if (authorization.rpt) {
console.log("Current RPT:", authorization.rpt);
// Use existing RPT for API calls
const response = await fetch("/api/protected-resource", {
headers: {
Authorization: `Bearer ${authorization.rpt}`
}
});
} else {
// Request new entitlement
const rpt = await authorization.entitlement("my-resource-server");
}Configuration object for authorization requests.
interface AuthorizationRequest {
/** Permission ticket from UMA-protected resource server */
ticket?: string;
/** Array of permission tickets */
tickets?: string[];
/** Array of specific resource permissions to request */
permissions?: ResourcePermission[];
/** Metadata controlling request processing */
metadata?: AuthorizationMetadata;
/** Whether to create permission requests for ticket resources */
submit_request?: boolean;
/** Enable incremental authorization */
incrementalAuthorization?: boolean;
/** Submitter identifier */
submitterId?: string;
/** Claim token for additional claims */
claimToken?: string;
/** Format of the claim token */
claimTokenFormat?: string;
}Represents a resource and its requested scopes.
interface ResourcePermission {
/** Resource identifier or name */
id?: string;
/** Resource name */
name?: string;
/** Array of scope names to request for this resource */
scopes?: string[];
}Controls how authorization requests are processed by the server.
interface AuthorizationMetadata {
/** Include resource names in RPT permissions */
response_include_resource_name?: boolean;
/** Limit number of permissions in RPT */
response_permissions_limit?: number;
}Complete example of handling UMA authorization flow:
class ResourceClient {
constructor(resourceServerUrl, keycloak, authorization) {
this.baseUrl = resourceServerUrl;
this.keycloak = keycloak;
this.authorization = authorization;
}
async accessResource(resourcePath) {
try {
// Try initial request
let response = await this.makeRequest(resourcePath);
if (response.status === 401) {
// Handle UMA authorization
response = await this.handleUMAAuth(response, resourcePath);
}
return response;
} catch (error) {
console.error("Resource access failed:", error);
throw error;
}
}
async makeRequest(resourcePath, rpt = null) {
const token = rpt || this.keycloak.token;
return fetch(`${this.baseUrl}${resourcePath}`, {
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json"
}
});
}
async handleUMAAuth(response, resourcePath) {
const wwwAuth = response.headers.get("WWW-Authenticate");
if (wwwAuth && wwwAuth.includes("UMA")) {
const ticket = this.extractTicket(wwwAuth);
try {
// Request authorization with ticket
const rpt = await this.authorization.authorize({
ticket,
submit_request: true
});
// Retry request with RPT
return await this.makeRequest(resourcePath, rpt);
} catch (authError) {
console.error("Authorization denied:", authError);
throw new Error("Access denied to resource");
}
}
throw new Error("Unexpected authentication challenge");
}
extractTicket(wwwAuthHeader) {
const ticketMatch = wwwAuthHeader.match(/ticket="([^"]+)"/);
return ticketMatch ? ticketMatch[1] : null;
}
}
// Usage
const resourceClient = new ResourceClient(
"https://api.mycompany.com",
keycloak,
authorization
);
const data = await resourceClient.accessResource("/protected/documents/123");Common authorization error scenarios:
// Authorization with error handling
async function requestPermissions() {
try {
const rpt = await authorization.authorize({
permissions: [
{ name: "document-123", scopes: ["view", "edit"] }
]
});
console.log("Authorization successful");
return rpt;
} catch (error) {
if (error.message.includes("access_denied")) {
console.log("Access denied by policy");
showPermissionDeniedDialog();
} else if (error.message.includes("invalid_ticket")) {
console.log("Invalid permission ticket");
// Request new ticket from resource server
} else {
console.error("Authorization error:", error);
showGenericErrorMessage();
}
throw error;
}
}
// Entitlement with fallback
async function getEntitlements(resourceServerId) {
try {
return await authorization.entitlement(resourceServerId);
} catch (error) {
console.warn(`No entitlements for ${resourceServerId}:`, error);
return null;
}
}The authorization functions support callback-style error handling:
// Using callback pattern (legacy style)
authorization.authorize({ ticket })
.then((rpt) => {
// onGrant callback - authorization successful
console.log("Access granted, RPT:", rpt);
retryRequestWithRPT(rpt);
})
.catch((error) => {
if (error.type === "access_denied") {
// onDeny callback - access denied
console.log("Access denied by policy");
showAccessDeniedUI();
} else {
// onError callback - unexpected error
console.error("Authorization error:", error);
showErrorUI();
}
});