UNPKG

keycloak-js

Version:

A client-side JavaScript OpenID Connect library that can be used to secure web applications.

297 lines (247 loc) 9.28 kB
/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const KeycloakAuthorization = function (keycloak, options) { const _instance = this this.rpt = null // Only here for backwards compatibility, as the configuration is now loaded on demand. // See: // - https://github.com/keycloak/keycloak/pull/6619 // - https://issues.redhat.com/browse/KEYCLOAK-10894 // TODO: Remove both `ready` property and `init` method in a future version Object.defineProperty(this, 'ready', { get () { console.warn("The 'ready' property is deprecated and will be removed in a future version. Initialization now happens automatically, using this property is no longer required.") return Promise.resolve() } }) this.init = () => { console.warn("The 'init()' method is deprecated and will be removed in a future version. Initialization now happens automatically, calling this method is no longer required.") } /** @type {Promise<unknown> | undefined} */ let configPromise /** * Initializes the configuration or re-uses the existing one if present. * @returns {Promise<void>} A promise that resolves when the configuration is loaded. */ async function initializeConfigIfNeeded () { if (_instance.config) { return _instance.config } if (configPromise) { return await configPromise } if (!keycloak.didInitialize) { throw new Error('The Keycloak instance has not been initialized yet.') } configPromise = loadConfig(keycloak.authServerUrl, keycloak.realm) _instance.config = await configPromise } /** * This method enables client applications to better integrate with resource servers protected by a Keycloak * policy enforcer using UMA protocol. * * The authorization request must be provided with a ticket. */ this.authorize = function (authorizationRequest) { this.then = async function (onGrant, onDeny, onError) { try { await initializeConfigIfNeeded() } catch (error) { handleError(error, onError) return } if (authorizationRequest && authorizationRequest.ticket) { const request = new globalThis.XMLHttpRequest() request.open('POST', _instance.config.token_endpoint, true) request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token) request.onreadystatechange = function () { if (request.readyState === 4) { const status = request.status if (status >= 200 && status < 300) { const rpt = JSON.parse(request.responseText).access_token _instance.rpt = rpt onGrant(rpt) } else if (status === 403) { if (onDeny) { onDeny() } else { console.error('Authorization request was denied by the server.') } } else { if (onError) { onError() } else { console.error('Could not obtain authorization data from server.') } } } } let params = 'grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=' + keycloak.clientId + '&ticket=' + authorizationRequest.ticket if (authorizationRequest.submitRequest !== undefined) { params += '&submit_request=' + authorizationRequest.submitRequest } const metadata = authorizationRequest.metadata if (metadata) { if (metadata.responseIncludeResourceName) { params += '&response_include_resource_name=' + metadata.responseIncludeResourceName } if (metadata.responsePermissionsLimit) { params += '&response_permissions_limit=' + metadata.responsePermissionsLimit } } if (_instance.rpt && (authorizationRequest.incrementalAuthorization === undefined || authorizationRequest.incrementalAuthorization)) { params += '&rpt=' + _instance.rpt } request.send(params) } } return this } /** * Obtains all entitlements from a Keycloak Server based on a given resourceServerId. */ this.entitlement = function (resourceServerId, authorizationRequest) { this.then = async function (onGrant, onDeny, onError) { try { await initializeConfigIfNeeded() } catch (error) { handleError(error, onError) return } const request = new globalThis.XMLHttpRequest() request.open('POST', _instance.config.token_endpoint, true) request.setRequestHeader('Content-type', 'application/x-www-form-urlencoded') request.setRequestHeader('Authorization', 'Bearer ' + keycloak.token) request.onreadystatechange = function () { if (request.readyState === 4) { const status = request.status if (status >= 200 && status < 300) { const rpt = JSON.parse(request.responseText).access_token _instance.rpt = rpt onGrant(rpt) } else if (status === 403) { if (onDeny) { onDeny() } else { console.error('Authorization request was denied by the server.') } } else { if (onError) { onError() } else { console.error('Could not obtain authorization data from server.') } } } } if (!authorizationRequest) { authorizationRequest = {} } let params = 'grant_type=urn:ietf:params:oauth:grant-type:uma-ticket&client_id=' + keycloak.clientId if (authorizationRequest.claimToken) { params += '&claim_token=' + authorizationRequest.claimToken if (authorizationRequest.claimTokenFormat) { params += '&claim_token_format=' + authorizationRequest.claimTokenFormat } } params += '&audience=' + resourceServerId let permissions = authorizationRequest.permissions if (!permissions) { permissions = [] } for (let i = 0; i < permissions.length; i++) { const resource = permissions[i] let permission = resource.id if (resource.scopes && resource.scopes.length > 0) { permission += '#' for (let j = 0; j < resource.scopes.length; j++) { const scope = resource.scopes[j] if (permission.indexOf('#') !== permission.length - 1) { permission += ',' } permission += scope } } params += '&permission=' + permission } const metadata = authorizationRequest.metadata if (metadata) { if (metadata.responseIncludeResourceName) { params += '&response_include_resource_name=' + metadata.responseIncludeResourceName } if (metadata.responsePermissionsLimit) { params += '&response_permissions_limit=' + metadata.responsePermissionsLimit } } if (_instance.rpt) { params += '&rpt=' + _instance.rpt } request.send(params) } return this } return this } /** * Obtains the configuration from the server. * @param {string} serverUrl The URL of the Keycloak server. * @param {string} realm The realm name. * @returns {Promise<unknown>} A promise that resolves when the configuration is loaded. */ async function loadConfig (serverUrl, realm) { const url = `${serverUrl}/realms/${encodeURIComponent(realm)}/.well-known/uma2-configuration` try { return await fetchJSON(url) } catch (error) { throw new Error('Could not obtain configuration from server.', { cause: error }) } } /** * Fetches the JSON data from the given URL. * @param {string} url The URL to fetch the data from. * @returns {Promise<unknown>} A promise that resolves when the data is loaded. */ async function fetchJSON (url) { let response try { response = await fetch(url) } catch (error) { throw new Error('Server did not respond.', { cause: error }) } if (!response.ok) { throw new Error('Server responded with an invalid status.') } try { return await response.json() } catch (error) { throw new Error('Server responded with invalid JSON.', { cause: error }) } } /** * @param {unknown} error * @param {((error: unknown) => void) | undefined} handler */ function handleError (error, handler) { if (handler) { handler(error) } else { console.error(error) } } export default KeycloakAuthorization