mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-06 06:49:53 -06:00
Remove Keycloak JS from repository (#37057)
Closes #36645 Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@@ -30,7 +30,6 @@
|
||||
# Core Clients (@keycloak/core-clients-maintainers)
|
||||
###################################################################################################
|
||||
|
||||
/js/libs/keycloak-js/ @keycloak/core-clients-maintainers
|
||||
|
||||
###################################################################################################
|
||||
# Cloud Native (@keycloak/cloud-native-maintainers)
|
||||
|
||||
1
.github/actions/conditional/conditions
vendored
1
.github/actions/conditional/conditions
vendored
@@ -37,7 +37,6 @@ rest/admin-ui-ext/ js
|
||||
services/ js
|
||||
js/apps/account-ui/ ci ci-webauthn
|
||||
js/libs/ui-shared/ ci ci-webauthn
|
||||
js/libs/keycloak-js/ ci ci-quarkus
|
||||
|
||||
# The sections below contain a sub-set of files existing in the project which are supported languages by CodeQL.
|
||||
# See: https://codeql.github.com/docs/codeql-overview/supported-languages-and-frameworks/
|
||||
|
||||
@@ -4,7 +4,6 @@ mvn:keycloak-api-docs-dist:keycloak-api-docs
|
||||
mvn:documentation/keycloak-documentation:keycloak-documentation
|
||||
|
||||
npm:js/libs/keycloak-admin-client/target/keycloak-keycloak-admin-client-$$VERSION$$.tgz:keycloak-admin-client-$$VERSION$$.tgz
|
||||
npm:js/libs/keycloak-js/target/keycloak-js-$$VERSION$$.tgz:keycloak-js-$$VERSION$$.tgz
|
||||
npm:js/libs/ui-shared/target/keycloak-keycloak-ui-shared-$$VERSION$$.tgz:keycloak-ui-shared-$$VERSION$$.tgz
|
||||
npm:js/apps/account-ui/target/keycloak-keycloak-account-ui-$$VERSION$$.tgz:keycloak-account-ui-$$VERSION$$.tgz
|
||||
npm:js/apps/admin-ui/target/keycloak-keycloak-admin-ui-$$VERSION$$.tgz:keycloak-admin-ui-$$VERSION$$.tgz
|
||||
|
||||
@@ -9,8 +9,7 @@ This directory contains the UIs and related libraries of the Keycloak project wr
|
||||
│ ├── admin-ui # Admin UI for handling login, registration, administration, and account management
|
||||
│ └── keycloak-server # Keycloak server for local development of UIs
|
||||
├── libs
|
||||
│ ├── keycloak-admin-client # Keycloak Admin Client library for Keycloak REST API
|
||||
│ └── keycloak-js # Keycloak JS library for securing HTML5/JavaScript applications
|
||||
│ └── keycloak-admin-client # Keycloak Admin Client library for Keycloak REST API
|
||||
├── ...
|
||||
|
||||
## Data processing
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@patternfly/react-table": "^5.4.14",
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"keycloak-js": "workspace:*",
|
||||
"keycloak-js": "^26.1.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
"i18next": "^24.2.2",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"jszip": "^3.10.1",
|
||||
"keycloak-js": "workspace:*",
|
||||
"keycloak-js": "^26.1.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"p-debounce": "^4.0.0",
|
||||
"react": "^18.3.1",
|
||||
|
||||
@@ -22,8 +22,6 @@ export default tseslint.config(
|
||||
"**/lib/",
|
||||
"**/target/",
|
||||
"./apps/keycloak-server/server/",
|
||||
// Keycloak JS follows a completely different and outdated style, so we'll exclude it for now.
|
||||
"./libs/keycloak-js/",
|
||||
],
|
||||
},
|
||||
eslint.configs.recommended,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# Keycloak JS
|
||||
|
||||
The documentation can be found in the [Keycloak documentation](https://www.keycloak.org/securing-apps/javascript-adapter).
|
||||
143
js/libs/keycloak-js/lib/keycloak-authz.d.ts
vendored
143
js/libs/keycloak-js/lib/keycloak-authz.d.ts
vendored
@@ -1,143 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
import Keycloak from './keycloak.js';
|
||||
|
||||
export interface KeycloakAuthorizationPromise {
|
||||
then(onGrant: (rpt: string) => void, onDeny: () => void, onError: () => void): void;
|
||||
}
|
||||
|
||||
export interface AuthorizationRequest {
|
||||
/**
|
||||
* An array of objects representing the resource and scopes.
|
||||
*/
|
||||
permissions?:ResourcePermission[],
|
||||
|
||||
/**
|
||||
* A permission ticket obtained from a resource server when using UMA authorization protocol.
|
||||
*/
|
||||
ticket?:string,
|
||||
|
||||
/**
|
||||
* A boolean value indicating whether the server should create permission requests to the resources
|
||||
* and scopes referenced by a permission ticket. This parameter will only take effect when used together
|
||||
* with the ticket parameter as part of a UMA authorization process.
|
||||
*/
|
||||
submitRequest?:boolean,
|
||||
|
||||
/**
|
||||
* Defines additional information about this authorization request in order to specify how it should be processed
|
||||
* by the server.
|
||||
*/
|
||||
metadata?:AuthorizationRequestMetadata,
|
||||
|
||||
/**
|
||||
* Defines whether or not this authorization request should include the current RPT. If set to true, the RPT will
|
||||
* be sent and permissions in the current RPT will be included in the new RPT. Otherwise, only the permissions referenced in this
|
||||
* authorization request will be granted in the new RPT.
|
||||
*/
|
||||
incrementalAuthorization?:boolean
|
||||
}
|
||||
|
||||
export interface AuthorizationRequestMetadata {
|
||||
/**
|
||||
* A boolean value indicating to the server if resource names should be included in the RPT’s permissions.
|
||||
* If false, only the resource identifier is included.
|
||||
*/
|
||||
responseIncludeResourceName?:any,
|
||||
|
||||
/**
|
||||
* An integer N that defines a limit for the amount of permissions an RPT can have. When used together with
|
||||
* rpt parameter, only the last N requested permissions will be kept in the RPT.
|
||||
*/
|
||||
response_permissions_limit?:number
|
||||
}
|
||||
|
||||
export interface ResourcePermission {
|
||||
/**
|
||||
* The id or name of a resource.
|
||||
*/
|
||||
id:string,
|
||||
|
||||
/**
|
||||
* An array of strings where each value is the name of a scope associated with the resource.
|
||||
*/
|
||||
scopes?:string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Instead of importing 'KeycloakAuthorizationInstance' you can import 'KeycloakAuthorization' directly as a type.
|
||||
*/
|
||||
export type KeycloakAuthorizationInstance = KeycloakAuthorization;
|
||||
|
||||
/**
|
||||
* @deprecated Construct a KeycloakAuthorization instance using the `new` keyword instead.
|
||||
*/
|
||||
declare function KeycloakAuthorization(keycloak: Keycloak): KeycloakAuthorization;
|
||||
|
||||
declare class KeycloakAuthorization {
|
||||
/**
|
||||
* Creates a new Keycloak client instance.
|
||||
* @param config Path to a JSON config file or a plain config object.
|
||||
*/
|
||||
constructor(keycloak: Keycloak)
|
||||
|
||||
rpt: any;
|
||||
config: { rpt_endpoint: string };
|
||||
|
||||
/**
|
||||
* Initializes the `KeycloakAuthorization` instance.
|
||||
* @deprecated Initialization now happens automatically, calling this method is no longer required.
|
||||
*/
|
||||
init(): void;
|
||||
|
||||
/**
|
||||
* A promise that resolves when the `KeycloakAuthorization` instance is initialized.
|
||||
* @deprecated Initialization now happens automatically, using this property is no longer required.
|
||||
*/
|
||||
ready: Promise<void>;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param authorizationRequest An AuthorizationRequest instance with a valid permission ticket set.
|
||||
* @returns A promise to set functions to be invoked on grant, deny or error.
|
||||
*/
|
||||
authorize(authorizationRequest: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||
|
||||
/**
|
||||
* Obtains all entitlements from a Keycloak server based on a given resourceServerId.
|
||||
*
|
||||
* @param resourceServerId The id (client id) of the resource server to obtain permissions from.
|
||||
* @param authorizationRequest An AuthorizationRequest instance.
|
||||
* @returns A promise to set functions to be invoked on grant, deny or error.
|
||||
*/
|
||||
entitlement(resourceServerId: string, authorizationRequest?: AuthorizationRequest): KeycloakAuthorizationPromise;
|
||||
}
|
||||
|
||||
export default KeycloakAuthorization;
|
||||
|
||||
/**
|
||||
* @deprecated The 'KeycloakAuthorization' namespace is deprecated, use named imports instead.
|
||||
*/
|
||||
export as namespace KeycloakAuthorization;
|
||||
@@ -1,296 +0,0 @@
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
var KeycloakAuthorization = function (keycloak, options) {
|
||||
var _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) {
|
||||
var request = new 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) {
|
||||
var status = request.status;
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
var 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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var 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;
|
||||
}
|
||||
|
||||
var 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;
|
||||
}
|
||||
|
||||
var request = new 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) {
|
||||
var status = request.status;
|
||||
|
||||
if (status >= 200 && status < 300) {
|
||||
var 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 = {};
|
||||
}
|
||||
|
||||
var 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;
|
||||
|
||||
var permissions = authorizationRequest.permissions;
|
||||
|
||||
if (!permissions) {
|
||||
permissions = [];
|
||||
}
|
||||
|
||||
for (var i = 0; i < permissions.length; i++) {
|
||||
var resource = permissions[i];
|
||||
var permission = resource.id;
|
||||
|
||||
if (resource.scopes && resource.scopes.length > 0) {
|
||||
permission += "#";
|
||||
for (var j = 0; j < resource.scopes.length; j++) {
|
||||
var scope = resource.scopes[j];
|
||||
if (permission.indexOf('#') != permission.length - 1) {
|
||||
permission += ",";
|
||||
}
|
||||
permission += scope;
|
||||
}
|
||||
}
|
||||
|
||||
params += "&permission=" + permission;
|
||||
}
|
||||
|
||||
var 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(message, error);
|
||||
}
|
||||
}
|
||||
|
||||
export default KeycloakAuthorization;
|
||||
660
js/libs/keycloak-js/lib/keycloak.d.ts
vendored
660
js/libs/keycloak-js/lib/keycloak.d.ts
vendored
@@ -1,660 +0,0 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright 2017 Brett Epps <https://github.com/eppsilon>
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||
* associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
* following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
* portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
|
||||
* LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
|
||||
* NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
export type KeycloakOnLoad = 'login-required'|'check-sso';
|
||||
export type KeycloakResponseMode = 'query'|'fragment';
|
||||
export type KeycloakResponseType = 'code'|'id_token token'|'code id_token token';
|
||||
export type KeycloakFlow = 'standard'|'implicit'|'hybrid';
|
||||
export type KeycloakPkceMethod = 'S256' | false;
|
||||
|
||||
export interface KeycloakConfig {
|
||||
/**
|
||||
* URL to the Keycloak server, for example: http://keycloak-server/auth
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* Name of the realm, for example: 'myrealm'
|
||||
*/
|
||||
realm: string;
|
||||
/**
|
||||
* Client identifier, example: 'myapp'
|
||||
*/
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export interface Acr {
|
||||
/**
|
||||
* Array of values, which will be used inside ID Token `acr` claim sent inside the `claims` parameter to Keycloak server during login.
|
||||
* Values should correspond to the ACR levels defined in the ACR to Loa mapping for realm or client or to the numbers (levels) inside defined
|
||||
* Keycloak authentication flow. See section 5.5.1 of OIDC 1.0 specification for the details.
|
||||
*/
|
||||
values: string[];
|
||||
/**
|
||||
* This parameter specifies if ACR claims is considered essential or not.
|
||||
*/
|
||||
essential: boolean;
|
||||
}
|
||||
|
||||
export interface KeycloakInitOptions {
|
||||
/**
|
||||
* Adds a [cryptographic nonce](https://en.wikipedia.org/wiki/Cryptographic_nonce)
|
||||
* to verify that the authentication response matches the request.
|
||||
* @default true
|
||||
*/
|
||||
useNonce?: boolean;
|
||||
|
||||
/**
|
||||
*
|
||||
* Allow usage of different types of adapters or a custom adapter to make Keycloak work in different environments.
|
||||
*
|
||||
* The following options are supported:
|
||||
* - `default` - Use default APIs that are available in browsers.
|
||||
* - `cordova` - Use a WebView in Cordova.
|
||||
* - `cordova-native` - Use Cordova native APIs, this is recommended over `cordova`.
|
||||
*
|
||||
* It's also possible to pass in a custom adapter for the environment you are running Keycloak in. In order to do so extend the `KeycloakAdapter` interface and implement the methods that are defined there.
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
* ```ts
|
||||
* // Implement the 'KeycloakAdapter' interface so that all required methods are guaranteed to be present.
|
||||
* const MyCustomAdapter: KeycloakAdapter = {
|
||||
* login(options) {
|
||||
* // Write your own implementation here.
|
||||
* }
|
||||
*
|
||||
* // The other methods go here...
|
||||
* };
|
||||
*
|
||||
* keycloak.init({
|
||||
* adapter: MyCustomAdapter,
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
adapter?: 'default' | 'cordova' | 'cordova-native' | KeycloakAdapter;
|
||||
|
||||
/**
|
||||
* Specifies an action to do on load.
|
||||
*/
|
||||
onLoad?: KeycloakOnLoad;
|
||||
|
||||
/**
|
||||
* Set an initial value for the token.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for the refresh token.
|
||||
*/
|
||||
refreshToken?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for the id token (only together with `token` or
|
||||
* `refreshToken`).
|
||||
*/
|
||||
idToken?: string;
|
||||
|
||||
/**
|
||||
* Set an initial value for skew between local time and Keycloak server in
|
||||
* seconds (only together with `token` or `refreshToken`).
|
||||
*/
|
||||
timeSkew?: number;
|
||||
|
||||
/**
|
||||
* Set to enable/disable monitoring login state.
|
||||
* @default true
|
||||
*/
|
||||
checkLoginIframe?: boolean;
|
||||
|
||||
/**
|
||||
* Set the interval to check login state (in seconds).
|
||||
* @default 5
|
||||
*/
|
||||
checkLoginIframeInterval?: number;
|
||||
|
||||
/**
|
||||
* Set the OpenID Connect response mode to send to Keycloak upon login.
|
||||
* @default fragment After successful authentication Keycloak will redirect
|
||||
* to JavaScript application with OpenID Connect parameters
|
||||
* added in URL fragment. This is generally safer and
|
||||
* recommended over query.
|
||||
*/
|
||||
responseMode?: KeycloakResponseMode;
|
||||
|
||||
/**
|
||||
* Specifies a default uri to redirect to after login or logout.
|
||||
* This is currently supported for adapter 'cordova-native' and 'default'
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* Specifies an uri to redirect to after silent check-sso.
|
||||
* Silent check-sso will only happen, when this redirect uri is given and
|
||||
* the specified uri is available within the application.
|
||||
*/
|
||||
silentCheckSsoRedirectUri?: string;
|
||||
|
||||
/**
|
||||
* Specifies whether the silent check-sso should fallback to "non-silent"
|
||||
* check-sso when 3rd party cookies are blocked by the browser. Defaults
|
||||
* to true.
|
||||
*/
|
||||
silentCheckSsoFallback?: boolean;
|
||||
|
||||
/**
|
||||
* Set the OpenID Connect flow.
|
||||
* @default standard
|
||||
*/
|
||||
flow?: KeycloakFlow;
|
||||
|
||||
/**
|
||||
* Configures the Proof Key for Code Exchange (PKCE) method to use. This will default to 'S256'.
|
||||
* Can be disabled by passing `false`.
|
||||
*/
|
||||
pkceMethod?: KeycloakPkceMethod;
|
||||
|
||||
/**
|
||||
* Configures the 'acr_values' query param in compliance with section 3.1.2.1
|
||||
* of the OIDC 1.0 specification.
|
||||
* Used to tell Keycloak what level of authentication the user needs.
|
||||
*/
|
||||
acrValues?: string;
|
||||
|
||||
/**
|
||||
* Enables logging messages from Keycloak to the console.
|
||||
* @default false
|
||||
*/
|
||||
enableLogging?: boolean
|
||||
|
||||
/**
|
||||
* Set the default scope parameter to the login endpoint. Use a space-delimited list of scopes.
|
||||
* Note that the scope 'openid' will be always be added to the list of scopes by the adapter.
|
||||
* Note that the default scope specified here is overwritten if the `login()` options specify scope explicitly.
|
||||
*/
|
||||
scope?: string
|
||||
|
||||
/**
|
||||
* Configures how long will Keycloak adapter wait for receiving messages from server in ms. This is used,
|
||||
* for example, when waiting for response of 3rd party cookies check.
|
||||
*
|
||||
* @default 10000
|
||||
*/
|
||||
messageReceiveTimeout?: number
|
||||
|
||||
/**
|
||||
* When onLoad is 'login-required', sets the 'ui_locales' query param in compliance with section 3.1.2.1
|
||||
* of the OIDC 1.0 specification.
|
||||
*/
|
||||
locale?: string;
|
||||
|
||||
/**
|
||||
* HTTP method for calling the end_session endpoint. Defaults to 'GET'.
|
||||
*/
|
||||
logoutMethod?: 'GET' | 'POST';
|
||||
}
|
||||
|
||||
export interface KeycloakLoginOptions {
|
||||
/**
|
||||
* Specifies the scope parameter for the login url
|
||||
* The scope 'openid' will be added to the scope if it is missing or undefined.
|
||||
*/
|
||||
scope?: string;
|
||||
|
||||
/**
|
||||
* Specifies the uri to redirect to after login.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* By default the login screen is displayed if the user is not logged into
|
||||
* Keycloak. To only authenticate to the application if the user is already
|
||||
* logged in and not display the login page if the user is not logged in, set
|
||||
* this option to `'none'`. To always require re-authentication and ignore
|
||||
* SSO, set this option to `'login'`. To always prompt the user for consent,
|
||||
* set this option to `'consent'`. This ensures that consent is requested,
|
||||
* even if it has been given previously.
|
||||
*/
|
||||
prompt?: 'none' | 'login' | 'consent';
|
||||
|
||||
/**
|
||||
* If value is `'register'` then user is redirected to registration page,
|
||||
* otherwise to login page.
|
||||
*/
|
||||
action?: string;
|
||||
|
||||
/**
|
||||
* Used just if user is already authenticated. Specifies maximum time since
|
||||
* the authentication of user happened. If user is already authenticated for
|
||||
* longer time than `'maxAge'`, the SSO is ignored and he will need to
|
||||
* authenticate again.
|
||||
*/
|
||||
maxAge?: number;
|
||||
|
||||
/**
|
||||
* Used to pre-fill the username/email field on the login form.
|
||||
*/
|
||||
loginHint?: string;
|
||||
|
||||
/**
|
||||
* Sets the `acr` claim of the ID token sent inside the `claims` parameter. See section 5.5.1 of the OIDC 1.0 specification.
|
||||
*/
|
||||
acr?: Acr;
|
||||
|
||||
/**
|
||||
* Configures the 'acr_values' query param in compliance with section 3.1.2.1
|
||||
* of the OIDC 1.0 specification.
|
||||
* Used to tell Keycloak what level of authentication the user needs.
|
||||
*/
|
||||
acrValues?: string;
|
||||
|
||||
/**
|
||||
* Used to tell Keycloak which IDP the user wants to authenticate with.
|
||||
*/
|
||||
idpHint?: string;
|
||||
|
||||
/**
|
||||
* Sets the 'ui_locales' query param in compliance with section 3.1.2.1
|
||||
* of the OIDC 1.0 specification.
|
||||
*/
|
||||
locale?: string;
|
||||
|
||||
/**
|
||||
* Specifies arguments that are passed to the Cordova in-app-browser (if applicable).
|
||||
* Options 'hidden' and 'location' are not affected by these arguments.
|
||||
* All available options are defined at https://cordova.apache.org/docs/en/latest/reference/cordova-plugin-inappbrowser/.
|
||||
* Example of use: { zoom: "no", hardwareback: "yes" }
|
||||
*/
|
||||
cordovaOptions?: { [optionName: string]: string };
|
||||
}
|
||||
|
||||
export interface KeycloakLogoutOptions {
|
||||
/**
|
||||
* Specifies the uri to redirect to after logout.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* HTTP method for calling the end_session endpoint. Defaults to 'GET'.
|
||||
*/
|
||||
logoutMethod?: 'GET' | 'POST';
|
||||
}
|
||||
|
||||
export interface KeycloakRegisterOptions extends Omit<KeycloakLoginOptions, 'action'> { }
|
||||
|
||||
export interface KeycloakAccountOptions {
|
||||
/**
|
||||
* Specifies the uri to redirect to when redirecting back to the application.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
}
|
||||
export interface KeycloakError {
|
||||
error: string;
|
||||
error_description: string;
|
||||
}
|
||||
|
||||
export interface KeycloakAdapter {
|
||||
login(options?: KeycloakLoginOptions): Promise<void>;
|
||||
logout(options?: KeycloakLogoutOptions): Promise<void>;
|
||||
register(options?: KeycloakRegisterOptions): Promise<void>;
|
||||
accountManagement(): Promise<void>;
|
||||
redirectUri(options: { redirectUri: string; }, encodeHash: boolean): string;
|
||||
}
|
||||
|
||||
export interface KeycloakProfile {
|
||||
id?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
enabled?: boolean;
|
||||
emailVerified?: boolean;
|
||||
totp?: boolean;
|
||||
createdTimestamp?: number;
|
||||
attributes?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface KeycloakTokenParsed {
|
||||
iss?: string;
|
||||
sub?: string;
|
||||
aud?: string;
|
||||
exp?: number;
|
||||
iat?: number;
|
||||
auth_time?: number;
|
||||
nonce?: string;
|
||||
acr?: string;
|
||||
amr?: string;
|
||||
azp?: string;
|
||||
session_state?: string;
|
||||
realm_access?: KeycloakRoles;
|
||||
resource_access?: KeycloakResourceAccess;
|
||||
[key: string]: any; // Add other attributes here.
|
||||
}
|
||||
|
||||
export interface KeycloakResourceAccess {
|
||||
[key: string]: KeycloakRoles
|
||||
}
|
||||
|
||||
export interface KeycloakRoles {
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Instead of importing 'KeycloakInstance' you can import 'Keycloak' directly as a type.
|
||||
*/
|
||||
export type KeycloakInstance = Keycloak;
|
||||
|
||||
/**
|
||||
* A client for the Keycloak authentication server.
|
||||
* @see {@link https://keycloak.gitbooks.io/securing-client-applications-guide/content/topics/oidc/javascript-adapter.html|Keycloak JS adapter documentation}
|
||||
*/
|
||||
declare class Keycloak {
|
||||
/**
|
||||
* Creates a new Keycloak client instance.
|
||||
* @param config A configuration object or path to a JSON config file.
|
||||
*/
|
||||
constructor(config: KeycloakConfig | string)
|
||||
|
||||
/**
|
||||
* Is true if the user is authenticated, false otherwise.
|
||||
*/
|
||||
authenticated?: boolean;
|
||||
|
||||
/**
|
||||
* The user id.
|
||||
*/
|
||||
subject?: string;
|
||||
|
||||
/**
|
||||
* Response mode passed in init (default value is `'fragment'`).
|
||||
*/
|
||||
responseMode?: KeycloakResponseMode;
|
||||
|
||||
/**
|
||||
* Response type sent to Keycloak with login requests. This is determined
|
||||
* based on the flow value used during initialization, but can be overridden
|
||||
* by setting this value.
|
||||
*/
|
||||
responseType?: KeycloakResponseType;
|
||||
|
||||
/**
|
||||
* Flow passed in init.
|
||||
*/
|
||||
flow?: KeycloakFlow;
|
||||
|
||||
/**
|
||||
* The realm roles associated with the token.
|
||||
*/
|
||||
realmAccess?: KeycloakRoles;
|
||||
|
||||
/**
|
||||
* The resource roles associated with the token.
|
||||
*/
|
||||
resourceAccess?: KeycloakResourceAccess;
|
||||
|
||||
/**
|
||||
* The base64 encoded token that can be sent in the Authorization header in
|
||||
* requests to services.
|
||||
*/
|
||||
token?: string;
|
||||
|
||||
/**
|
||||
* The parsed token as a JavaScript object.
|
||||
*/
|
||||
tokenParsed?: KeycloakTokenParsed;
|
||||
|
||||
/**
|
||||
* The base64 encoded refresh token that can be used to retrieve a new token.
|
||||
*/
|
||||
refreshToken?: string;
|
||||
|
||||
/**
|
||||
* The parsed refresh token as a JavaScript object.
|
||||
*/
|
||||
refreshTokenParsed?: KeycloakTokenParsed;
|
||||
|
||||
/**
|
||||
* The base64 encoded ID token.
|
||||
*/
|
||||
idToken?: string;
|
||||
|
||||
/**
|
||||
* The parsed id token as a JavaScript object.
|
||||
*/
|
||||
idTokenParsed?: KeycloakTokenParsed;
|
||||
|
||||
/**
|
||||
* The estimated time difference between the browser time and the Keycloak
|
||||
* server in seconds. This value is just an estimation, but is accurate
|
||||
* enough when determining if a token is expired or not.
|
||||
*/
|
||||
timeSkew?: number;
|
||||
|
||||
/**
|
||||
* Whether the instance has been initialized by calling `.init()`.
|
||||
*/
|
||||
didInitialize: boolean;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
loginRequired?: boolean;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
authServerUrl?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
realm?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
clientId?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
redirectUri?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
sessionId?: string;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
profile?: KeycloakProfile;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
userInfo?: {}; // KeycloakUserInfo;
|
||||
|
||||
/**
|
||||
* Called when the adapter is initialized.
|
||||
*/
|
||||
onReady?(authenticated?: boolean): void;
|
||||
|
||||
/**
|
||||
* Called when a user is successfully authenticated.
|
||||
*/
|
||||
onAuthSuccess?(): void;
|
||||
|
||||
/**
|
||||
* Called if there was an error during authentication.
|
||||
*/
|
||||
onAuthError?(errorData: KeycloakError): void;
|
||||
|
||||
/**
|
||||
* Called when the token is refreshed.
|
||||
*/
|
||||
onAuthRefreshSuccess?(): void;
|
||||
|
||||
/**
|
||||
* Called if there was an error while trying to refresh the token.
|
||||
*/
|
||||
onAuthRefreshError?(): void;
|
||||
|
||||
/**
|
||||
* Called if the user is logged out (will only be called if the session
|
||||
* status iframe is enabled, or in Cordova mode).
|
||||
*/
|
||||
onAuthLogout?(): void;
|
||||
|
||||
/**
|
||||
* Called when the access token is expired. If a refresh token is available
|
||||
* the token can be refreshed with Keycloak#updateToken, or in cases where
|
||||
* it's not (ie. with implicit flow) you can redirect to login screen to
|
||||
* obtain a new access token.
|
||||
*/
|
||||
onTokenExpired?(): void;
|
||||
|
||||
/**
|
||||
* Called when a AIA has been requested by the application.
|
||||
* @param status the outcome of the required action
|
||||
* @param action the alias name of the required action, e.g. UPDATE_PASSWORD, CONFIGURE_TOTP etc.
|
||||
*/
|
||||
onActionUpdate?(status: 'success'|'cancelled'|'error', action?: string): void;
|
||||
|
||||
/**
|
||||
* Called to initialize the adapter.
|
||||
* @param initOptions Initialization options.
|
||||
* @returns A promise to set functions to be invoked on success or error.
|
||||
*/
|
||||
init(initOptions?: KeycloakInitOptions): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Redirects to login form.
|
||||
* @param options Login options.
|
||||
*/
|
||||
login(options?: KeycloakLoginOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Redirects to logout.
|
||||
* @param options Logout options.
|
||||
*/
|
||||
logout(options?: KeycloakLogoutOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Redirects to registration form.
|
||||
* @param options The options used for the registration.
|
||||
*/
|
||||
register(options?: KeycloakRegisterOptions): Promise<void>;
|
||||
|
||||
/**
|
||||
* Redirects to the Account Management Console.
|
||||
*/
|
||||
accountManagement(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Returns the URL to login form.
|
||||
* @param options Supports same options as Keycloak#login.
|
||||
*/
|
||||
createLoginUrl(options?: KeycloakLoginOptions): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns the URL to logout the user.
|
||||
* @param options Logout options.
|
||||
*/
|
||||
createLogoutUrl(options?: KeycloakLogoutOptions): string;
|
||||
|
||||
/**
|
||||
* Returns the URL to registration page.
|
||||
* @param options The options used for creating the registration URL.
|
||||
*/
|
||||
createRegisterUrl(options?: KeycloakRegisterOptions): Promise<string>;
|
||||
|
||||
/**
|
||||
* Returns the URL to the Account Management Console.
|
||||
* @param options The options used for creating the account URL.
|
||||
*/
|
||||
createAccountUrl(options?: KeycloakAccountOptions): string;
|
||||
|
||||
/**
|
||||
* Returns true if the token has less than `minValidity` seconds left before
|
||||
* it expires.
|
||||
* @param minValidity If not specified, `0` is used.
|
||||
*/
|
||||
isTokenExpired(minValidity?: number): boolean;
|
||||
|
||||
/**
|
||||
* If the token expires within `minValidity` seconds, the token is refreshed.
|
||||
* If the session status iframe is enabled, the session status is also
|
||||
* checked.
|
||||
* @param minValidity If not specified, `5` is used.
|
||||
* @returns A promise to set functions that can be invoked if the token is
|
||||
* still valid, or if the token is no longer valid.
|
||||
* @example
|
||||
* ```js
|
||||
* keycloak.updateToken(5).then(function(refreshed) {
|
||||
* if (refreshed) {
|
||||
* alert('Token was successfully refreshed');
|
||||
* } else {
|
||||
* alert('Token is still valid');
|
||||
* }
|
||||
* }).catch(function() {
|
||||
* alert('Failed to refresh the token, or the session has expired');
|
||||
* });
|
||||
*/
|
||||
updateToken(minValidity?: number): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Clears authentication state, including tokens. This can be useful if
|
||||
* the application has detected the session was expired, for example if
|
||||
* updating token fails. Invoking this results in Keycloak#onAuthLogout
|
||||
* callback listener being invoked.
|
||||
*/
|
||||
clearToken(): void;
|
||||
|
||||
/**
|
||||
* Returns true if the token has the given realm role.
|
||||
* @param role A realm role name.
|
||||
*/
|
||||
hasRealmRole(role: string): boolean;
|
||||
|
||||
/**
|
||||
* Returns true if the token has the given role for the resource.
|
||||
* @param role A role name.
|
||||
* @param resource If not specified, `clientId` is used.
|
||||
*/
|
||||
hasResourceRole(role: string, resource?: string): boolean;
|
||||
|
||||
/**
|
||||
* Loads the user's profile.
|
||||
* @returns A promise to set functions to be invoked on success or error.
|
||||
*/
|
||||
loadUserProfile(): Promise<KeycloakProfile>;
|
||||
|
||||
/**
|
||||
* @private Undocumented.
|
||||
*/
|
||||
loadUserInfo(): Promise<{}>;
|
||||
}
|
||||
|
||||
export default Keycloak;
|
||||
|
||||
/**
|
||||
* @deprecated The 'Keycloak' namespace is deprecated, use named imports instead.
|
||||
*/
|
||||
export as namespace Keycloak;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "keycloak-js",
|
||||
"version": "999.0.0-SNAPSHOT",
|
||||
"type": "module",
|
||||
"description": "A client-side JavaScript OpenID Connect library that can be used to secure web applications.",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./lib/keycloak.d.ts",
|
||||
"default": "./lib/keycloak.js"
|
||||
},
|
||||
"./authz": {
|
||||
"types": "./lib/keycloak-authz.d.ts",
|
||||
"default": "./lib/keycloak-authz.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/keycloak/keycloak.git"
|
||||
},
|
||||
"author": "Keycloak",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://www.keycloak.org",
|
||||
"keywords": [
|
||||
"keycloak",
|
||||
"sso",
|
||||
"oauth",
|
||||
"oauth2",
|
||||
"authentication"
|
||||
]
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<artifactId>keycloak-js-parent</artifactId>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<version>999.0.0-SNAPSHOT</version>
|
||||
<relativePath>../../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>keycloak-js-adapter</artifactId>
|
||||
|
||||
<name>Keycloak JavaScript Adapter</name>
|
||||
<description>A client-side JavaScript OpenID Connect library that can be used to secure web applications.</description>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>jboss-release</id>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>create-target-dir</id>
|
||||
<phase>prepare-package</phase>
|
||||
<configuration>
|
||||
<target>
|
||||
<mkdir dir="./target" />
|
||||
</target>
|
||||
</configuration>
|
||||
<goals>
|
||||
<goal>run</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>com.github.eirslett</groupId>
|
||||
<artifactId>frontend-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>pnpm-pack</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>pnpm</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<arguments>pack --pack-destination=target</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>build-helper-maven-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>attach-artifacts</id>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>attach-artifact</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<artifacts>
|
||||
<artifact>
|
||||
<file>target/keycloak-js-${project.version.npm}.tgz</file>
|
||||
<type>tar.gz</type>
|
||||
</artifact>
|
||||
</artifacts>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</profile>
|
||||
</profiles>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-deploy-plugin</artifactId>
|
||||
<configuration>
|
||||
<skip>false</skip>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowSyntheticDefaultImports": true
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
"@patternfly/react-styles": "^5.4.1",
|
||||
"@patternfly/react-table": "^5.4.14",
|
||||
"i18next": "^24.2.2",
|
||||
"keycloak-js": "workspace:*",
|
||||
"keycloak-js": "^26.1.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
21
js/pnpm-lock.yaml
generated
21
js/pnpm-lock.yaml
generated
@@ -96,8 +96,8 @@ importers:
|
||||
specifier: ^3.0.2
|
||||
version: 3.0.2
|
||||
keycloak-js:
|
||||
specifier: workspace:*
|
||||
version: link:../../libs/keycloak-js
|
||||
specifier: ^26.1.2
|
||||
version: 26.1.2
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -196,8 +196,8 @@ importers:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
keycloak-js:
|
||||
specifier: workspace:*
|
||||
version: link:../../libs/keycloak-js
|
||||
specifier: ^26.1.2
|
||||
version: 26.1.2
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -369,8 +369,6 @@ importers:
|
||||
specifier: ^10.9.2
|
||||
version: 10.9.2(@swc/core@1.10.15)(@types/node@22.13.1)(typescript@5.7.3)
|
||||
|
||||
libs/keycloak-js: {}
|
||||
|
||||
libs/ui-shared:
|
||||
dependencies:
|
||||
'@keycloak/keycloak-admin-client':
|
||||
@@ -392,8 +390,8 @@ importers:
|
||||
specifier: ^24.2.2
|
||||
version: 24.2.2(typescript@5.7.3)
|
||||
keycloak-js:
|
||||
specifier: workspace:*
|
||||
version: link:../keycloak-js
|
||||
specifier: ^26.1.2
|
||||
version: 26.1.2
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
@@ -3747,6 +3745,9 @@ packages:
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
keycloak-js@26.1.2:
|
||||
resolution: {integrity: sha512-nZ26zNgZevVSo7bqeljOfFVCQ4HnPTeYIwdfIwg0uSuXgxD+zS0j1uqaypPlqU17Hu8qHlygj0u72TxPlCWmYw==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
@@ -6072,7 +6073,7 @@ snapshots:
|
||||
i18next: 24.2.2(typescript@5.7.3)
|
||||
i18next-http-backend: 3.0.2
|
||||
jszip: 3.10.1
|
||||
keycloak-js: link:libs/keycloak-js
|
||||
keycloak-js: 26.1.2
|
||||
lodash-es: 4.17.21
|
||||
p-debounce: 4.0.0
|
||||
react: 18.3.1
|
||||
@@ -9031,6 +9032,8 @@ snapshots:
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
keycloak-js@26.1.2: {}
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
<module>apps/admin-ui</module>
|
||||
<module>libs/keycloak-admin-client</module>
|
||||
<module>libs/ui-shared</module>
|
||||
<module>libs/keycloak-js</module>
|
||||
<module>themes-vendor</module>
|
||||
</modules>
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ sed -i 's/:project_versionDoc: .*/:project_versionDoc: '$NEW_VERSION'/' topics/t
|
||||
cd -
|
||||
|
||||
# NPM publish
|
||||
echo "$(jq '. += {"version": "'$NEW_NPM_VERSION'"}' js/libs/keycloak-js/package.json)" > js/libs/keycloak-js/package.json
|
||||
echo "$(jq '. += {"version": "'$NEW_NPM_VERSION'"}' js/libs/keycloak-admin-client/package.json)" > js/libs/keycloak-admin-client/package.json
|
||||
echo "$(jq '. += {"version": "'$NEW_NPM_VERSION'"}' js/libs/ui-shared/package.json)" > js/libs/ui-shared/package.json
|
||||
echo "$(jq '. += {"version": "'$NEW_NPM_VERSION'"}' js/apps/account-ui/package.json)" > js/apps/account-ui/package.json
|
||||
|
||||
@@ -30,11 +30,6 @@
|
||||
<artifactId>integration-arquillian-testsuite-providers</artifactId>
|
||||
<name>Auth Server Services - Testsuite Providers</name>
|
||||
|
||||
<properties>
|
||||
<js-adapter.dist.path>${project.basedir}/../../../../../../js/libs/keycloak-js/lib</js-adapter.dist.path>
|
||||
<js-adapter.target.path>${project.basedir}/target/classes/javascript</js-adapter.target.path>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!-- Keycloak deps for tests -->
|
||||
@@ -102,37 +97,5 @@
|
||||
</plugin>
|
||||
</plugins>
|
||||
</pluginManagement>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>src/main/resources</directory>
|
||||
<filtering>true</filtering>
|
||||
</resource>
|
||||
</resources>
|
||||
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>copy-keycloak-js</id>
|
||||
<phase>generate-resources</phase>
|
||||
<goals>
|
||||
<goal>copy-resources</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<outputDirectory>${js-adapter.target.path}</outputDirectory>
|
||||
<resources>
|
||||
<resource>
|
||||
<directory>${js-adapter.dist.path}</directory>
|
||||
<includes>
|
||||
<include>keycloak.js</include>
|
||||
</includes>
|
||||
</resource>
|
||||
</resources>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
||||
@@ -96,7 +96,6 @@ import org.keycloak.testsuite.forms.PassThroughClientAuthenticator;
|
||||
import org.keycloak.testsuite.model.infinispan.InfinispanTestUtil;
|
||||
import org.keycloak.testsuite.rest.representation.AuthenticatorState;
|
||||
import org.keycloak.testsuite.rest.resource.TestCacheResource;
|
||||
import org.keycloak.testsuite.rest.resource.TestJavascriptResource;
|
||||
import org.keycloak.testsuite.rest.resource.TestLDAPResource;
|
||||
import org.keycloak.testsuite.rest.resource.TestingExportImportResource;
|
||||
import org.keycloak.testsuite.runonserver.FetchOnServer;
|
||||
@@ -865,12 +864,6 @@ public class TestingResourceProvider implements RealmResourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Path("/javascript")
|
||||
public TestJavascriptResource getJavascriptResource() {
|
||||
return new TestJavascriptResource(session);
|
||||
}
|
||||
|
||||
private void setFeatureInProfileFile(File file, Profile.Feature featureProfile, String newState) {
|
||||
doWithProperties(file, props -> {
|
||||
props.setProperty(PropertiesProfileConfigResolver.getPropertyKey(featureProfile), newState);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package org.keycloak.testsuite.rest.resource;
|
||||
|
||||
import org.keycloak.headers.SecurityHeadersProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.testsuite.rest.TestingResourceProvider;
|
||||
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
import static org.keycloak.testsuite.util.ServerURLs.getAuthServerContextRoot;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class TestJavascriptResource {
|
||||
|
||||
private KeycloakSession session;
|
||||
|
||||
public TestJavascriptResource(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/js/keycloak.js")
|
||||
@Produces("application/javascript")
|
||||
public String getJavascriptAdapter() throws IOException {
|
||||
return resourceToString("/javascript/keycloak.js");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/index.html")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public String getJavascriptTestingEnvironment() throws IOException {
|
||||
session.getProvider(SecurityHeadersProvider.class).options().skipHeaders();
|
||||
return resourceToString("/javascript/index.html");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/init-in-head.html")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public String getJavascriptTestingEnvironmentWithInitInHead() throws IOException {
|
||||
session.getProvider(SecurityHeadersProvider.class).options().skipHeaders();
|
||||
return resourceToString("/javascript/init-in-head.html");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/silent-check-sso.html")
|
||||
@Produces(MediaType.TEXT_HTML)
|
||||
public String getJavascriptTestingEnvironmentSilentCheckSso() throws IOException {
|
||||
return resourceToString("/javascript/silent-check-sso.html");
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/keycloak.json")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public String getKeycloakJSON() throws IOException {
|
||||
return resourceToString("/javascript/keycloak.json");
|
||||
}
|
||||
|
||||
private String resourceToString(String path) throws IOException {
|
||||
try (InputStream is = TestingResourceProvider.class.getResourceAsStream(path);
|
||||
BufferedReader buf = new BufferedReader(new InputStreamReader(is))) {
|
||||
String line = buf.readLine();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
while (line != null) {
|
||||
sb.append(line).append("\n");
|
||||
line = buf.readLine();
|
||||
}
|
||||
|
||||
return sb.toString().replace("${js-adapter.auth-server-url}", getAuthServerContextRoot() + "/auth");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"keycloak-js": "./js/keycloak.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Result</h2>
|
||||
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="output"></pre>
|
||||
|
||||
<h2>Events</h2>
|
||||
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre>
|
||||
|
||||
|
||||
<script type="module">
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
function output(data) {
|
||||
if (typeof data === 'object') {
|
||||
data = JSON.stringify(data, null, ' ');
|
||||
}
|
||||
document.getElementById('output').innerHTML = data;
|
||||
}
|
||||
|
||||
function event(event) {
|
||||
var e = document.getElementById('events').innerHTML;
|
||||
document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
|
||||
}
|
||||
|
||||
function getParameterByName(name, url) {
|
||||
if (!url) url = window.location.href;
|
||||
name = name.replace(/[\[\]]/g, "\\$&");
|
||||
var regex = new RegExp("[?&#]" + name + "(=([^&#]*)|&|#|$)"),
|
||||
results = regex.exec(url);
|
||||
if (!results) return null;
|
||||
if (!results[2]) return '';
|
||||
return decodeURIComponent(results[2].replace(/\+/g, " "));
|
||||
}
|
||||
|
||||
// Expose globals for tests.
|
||||
globalThis.Keycloak = Keycloak;
|
||||
globalThis.output = output;
|
||||
globalThis.event = event;
|
||||
globalThis.getParameterByName = getParameterByName;
|
||||
|
||||
function showExpires() {
|
||||
if (!keycloak.tokenParsed) {
|
||||
output("Not authenticated");
|
||||
return;
|
||||
}
|
||||
|
||||
var o = 'Token Expires:\t\t' + new Date((keycloak.tokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
|
||||
o += 'Token Expires in:\t' + Math.round(keycloak.tokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds\n';
|
||||
|
||||
if (keycloak.refreshTokenParsed) {
|
||||
o += 'Refresh Token Expires:\t' + new Date((keycloak.refreshTokenParsed.exp + keycloak.timeSkew) * 1000).toLocaleString() + '\n';
|
||||
o += 'Refresh Expires in:\t' + Math.round(keycloak.refreshTokenParsed.exp + keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds';
|
||||
}
|
||||
|
||||
output(o);
|
||||
}
|
||||
|
||||
function showError() {
|
||||
output("Error: " + getParameterByName("error") + "\n" + "Error description: " + getParameterByName("error_description"));
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,77 +0,0 @@
|
||||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<html>
|
||||
<head>
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
"keycloak-js": "./js/keycloak.js"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<script type="module">
|
||||
import Keycloak from 'keycloak-js';
|
||||
|
||||
function output(data) {
|
||||
if (typeof data === 'object') {
|
||||
data = JSON.stringify(data, null, ' ');
|
||||
}
|
||||
document.getElementById('output').innerHTML = data;
|
||||
}
|
||||
|
||||
function event(event) {
|
||||
var e = document.getElementById('events').innerHTML;
|
||||
document.getElementById('events').innerHTML = new Date().toLocaleString() + "\t" + event + "\n" + e;
|
||||
}
|
||||
|
||||
const keycloak = new Keycloak({
|
||||
url: '${js-adapter.auth-server-url}',
|
||||
realm: 'test',
|
||||
clientId: 'js-console'
|
||||
});
|
||||
|
||||
// Expose globals for tests.
|
||||
globalThis.Keycloak = Keycloak;
|
||||
globalThis.keycloak = keycloak;
|
||||
globalThis.output = output;
|
||||
globalThis.event = event;
|
||||
|
||||
keycloak.init().then((authenticated) => {
|
||||
output('Init Success (' + (authenticated ? 'Authenticated' : 'Not Authenticated') + ')');
|
||||
}).catch(function() {
|
||||
output('Init Error');
|
||||
});
|
||||
|
||||
keycloak.onAuthSuccess = function () {event('Auth Success')};
|
||||
keycloak.onAuthError = function () {event('Auth Error')};
|
||||
keycloak.onAuthRefreshSuccess = function () {event('Auth Refresh Success')};
|
||||
keycloak.onAuthRefreshError = function () {event('Auth Refresh Error')};
|
||||
keycloak.onAuthLogout = function () {event('Auth Logout')};
|
||||
keycloak.onTokenExpired = function () {event('Access token expired.')};
|
||||
keycloak.onActionUpdate = function (status) {event('AIA status: ' + status)};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Result</h2>
|
||||
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="output"></pre>
|
||||
|
||||
<h2>Events</h2>
|
||||
<pre style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;" id="events"></pre>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"realm" : "test",
|
||||
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url" : "${js-adapter.auth-server-url}",
|
||||
"ssl-required" : "external",
|
||||
"resource" : "js-console",
|
||||
"public-client" : true
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
<html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>
|
||||
@@ -323,16 +323,6 @@ public interface TestingResource {
|
||||
String runModelTestOnServer(@QueryParam("testClassName") String testClassName,
|
||||
@QueryParam("testMethodName") String testMethodName);
|
||||
|
||||
@GET
|
||||
@Path("js/keycloak.js")
|
||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||
String getJavascriptAdapter();
|
||||
|
||||
@GET
|
||||
@Path("/get-javascript-testing-environment")
|
||||
@Produces(MediaType.TEXT_HTML_UTF_8)
|
||||
String getJavascriptTestingEnvironment();
|
||||
|
||||
@GET
|
||||
@Path("/list-disabled-features")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
package org.keycloak.testsuite.util.javascript;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class JSObjectBuilder {
|
||||
|
||||
private Map<String, Object> arguments;
|
||||
|
||||
|
||||
public static JSObjectBuilder create() {
|
||||
return new JSObjectBuilder();
|
||||
}
|
||||
|
||||
private JSObjectBuilder() {
|
||||
arguments = new HashMap<>();
|
||||
}
|
||||
|
||||
public JSObjectBuilder defaultSettings() {
|
||||
standardFlow();
|
||||
fragmentResponse();
|
||||
enableLogging();
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder standardFlow() {
|
||||
arguments.put("flow", "standard");
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder implicitFlow() {
|
||||
arguments.put("flow", "implicit");
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder fragmentResponse() {
|
||||
arguments.put("responseMode", "fragment");
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder queryResponse() {
|
||||
arguments.put("responseMode", "query");
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder checkSSOOnLoad() {
|
||||
arguments.put("onLoad", "check-sso");
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder disableSilentCheckSSOFallback() {
|
||||
arguments.put("silentCheckSsoFallback", false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder disableCheckLoginIframe() {
|
||||
arguments.put("checkLoginIframe", false);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder setCheckLoginIframeIntervalTo1() {
|
||||
arguments.put("checkLoginIframeInterval", 1);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder loginRequiredOnLoad() {
|
||||
arguments.put("onLoad", "login-required");
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder enableLogging() {
|
||||
arguments.put("enableLogging", true);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean contains(String key, Object value) {
|
||||
return arguments.containsKey(key) && arguments.get(key).equals(value);
|
||||
}
|
||||
|
||||
public JSObjectBuilder add(String key, Object value) {
|
||||
arguments.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isLoginRequired() {
|
||||
return arguments.get("onLoad").equals("login-required");
|
||||
}
|
||||
|
||||
|
||||
public JSObjectBuilder pkceS256() {
|
||||
return pkceMethod("S256");
|
||||
}
|
||||
|
||||
private JSObjectBuilder pkceMethod(String method) {
|
||||
arguments.put("pkceMethod", method);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JSObjectBuilder acrValues(String value) {
|
||||
Objects.requireNonNull(value, "value");
|
||||
arguments.put("acrValues", value);
|
||||
return this;
|
||||
}
|
||||
|
||||
private boolean skipQuotes(Object o) {
|
||||
return (o instanceof Integer || o instanceof Boolean || o instanceof JSObjectBuilder);
|
||||
}
|
||||
|
||||
public String build() {
|
||||
StringBuilder argument = new StringBuilder("{");
|
||||
String comma = "";
|
||||
for (Map.Entry<String, Object> option : arguments.entrySet()) {
|
||||
argument.append(comma)
|
||||
.append(option.getKey())
|
||||
.append(" : ");
|
||||
|
||||
if (option.getValue().getClass().isArray()) {
|
||||
try {
|
||||
argument.append(JsonSerialization.writeValueAsString(option.getValue()));
|
||||
} catch (IOException ioe) {
|
||||
throw new IllegalArgumentException("Not possible to serialize value of the option " + option.getKey(), ioe);
|
||||
}
|
||||
} else {
|
||||
if (!skipQuotes(option.getValue())) argument.append("\"");
|
||||
|
||||
argument.append(option.getValue());
|
||||
|
||||
if (!skipQuotes(option.getValue())) argument.append("\"");
|
||||
}
|
||||
comma = ",";
|
||||
}
|
||||
|
||||
argument.append("}");
|
||||
|
||||
return argument.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return build();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package org.keycloak.testsuite.util.javascript;
|
||||
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public interface JavascriptStateValidator extends Serializable {
|
||||
|
||||
void validate(WebDriver driver, Object output, WebElement events);
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
package org.keycloak.testsuite.util.javascript;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
|
||||
import org.keycloak.testsuite.pages.LogoutConfirmPage;
|
||||
import org.keycloak.testsuite.util.WaitUtils;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebDriverException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.pause;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
||||
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class JavascriptTestExecutor {
|
||||
protected WebDriver jsDriver;
|
||||
protected JavascriptExecutor jsExecutor;
|
||||
private WebElement output;
|
||||
protected WebElement events;
|
||||
private OIDCLogin loginPage;
|
||||
protected boolean configured;
|
||||
|
||||
private static final Logger logger = Logger.getLogger(JavascriptTestExecutor.class);
|
||||
|
||||
public static JavascriptTestExecutor create(WebDriver driver, OIDCLogin loginPage) {
|
||||
return new JavascriptTestExecutor(driver, loginPage);
|
||||
}
|
||||
|
||||
protected JavascriptTestExecutor(WebDriver driver, OIDCLogin loginPage) {
|
||||
this.jsDriver = driver;
|
||||
driver.manage().timeouts().setScriptTimeout(WaitUtils.PAGELOAD_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
|
||||
jsExecutor = (JavascriptExecutor) driver;
|
||||
events = driver.findElement(By.id("events"));
|
||||
output = driver.findElement(By.id("output"));
|
||||
this.loginPage = loginPage;
|
||||
configured = false;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor login() {
|
||||
return login((String)null, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor login(JavascriptStateValidator validator) {
|
||||
return login((String)null, validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches a MutationObserver that sends a message from iframe to main window with incorrect data when the iframe is loaded
|
||||
*/
|
||||
public JavascriptTestExecutor attachCheck3pCookiesIframeMutationObserver() {
|
||||
jsExecutor.executeScript("// Select the node that will be observed for mutations\n" +
|
||||
" const targetNode = document.body;" +
|
||||
"" +
|
||||
" // Options for the observer (which mutations to observe)\n" +
|
||||
" const config = {attributes: true, childList: true, subtree: true};" +
|
||||
"" +
|
||||
" // Callback function to execute when mutations are observed\n" +
|
||||
" const callback = function (mutationsList, observer) {" +
|
||||
" console.log(\"Mutation found\");" +
|
||||
" var iframeNode = mutationsList[0].addedNodes[0];" +
|
||||
" if (iframeNode && iframeNode.localName === 'iframe') {" +
|
||||
" var s = document.createElement('script');" +
|
||||
" s.type = 'text/javascript';" +
|
||||
" var code = \"window.parent.postMessage('Evil Message', '*');\";" +
|
||||
" s.appendChild(document.createTextNode(code));" +
|
||||
" iframeNode.contentDocument.body.appendChild(s);" +
|
||||
" }" +
|
||||
" }\n" +
|
||||
"" +
|
||||
" // Create an observer instance linked to the callback function\n" +
|
||||
" const observer = new MutationObserver(callback);" +
|
||||
"" +
|
||||
" // Start observing the target node for configured mutations\n" +
|
||||
" observer.observe(targetNode, config);");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor login(JSObjectBuilder optionsBuilder, JavascriptStateValidator validator) {
|
||||
return login(optionsBuilder.build(), validator);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor login(String options, JavascriptStateValidator validator) {
|
||||
if (options == null)
|
||||
jsExecutor.executeScript("keycloak.login()");
|
||||
else {
|
||||
jsExecutor.executeScript("keycloak.login(" + options + ")");
|
||||
}
|
||||
waitForPageToLoad();
|
||||
|
||||
if (validator != null) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
}
|
||||
|
||||
configured = false; // Getting out of testApp page => loosing keycloak variable etc.
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor loginForm(UserRepresentation user) {
|
||||
return loginForm(user, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor loginForm(UserRepresentation user, JavascriptStateValidator validator) {
|
||||
loginPage.form().login(user);
|
||||
waitForPageToLoad();
|
||||
|
||||
if (validator != null) {
|
||||
validator.validate(jsDriver, null, events);
|
||||
}
|
||||
|
||||
configured = false; // Getting out of testApp page => loosing keycloak variable etc.
|
||||
// this is necessary in case we skipped login button for example in login-required mode
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor logout() {
|
||||
return logout(null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor logout(JavascriptStateValidator validator) {
|
||||
return logout(validator, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor logout(JavascriptStateValidator validator, LogoutConfirmPage logoutConfirmPage) {
|
||||
return logout(validator, logoutConfirmPage, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor logout(JavascriptStateValidator validator, LogoutConfirmPage logoutConfirmPage, JSObjectBuilder logoutOptions) {
|
||||
String logoutOptionsString = logoutOptions == null ? "" : logoutOptions.toString();
|
||||
jsExecutor.executeScript("keycloak.logout(" + logoutOptionsString + ")");
|
||||
|
||||
try {
|
||||
// simple check if we are at the logout confirm page, if so just click 'Yes'
|
||||
if (logoutConfirmPage != null && logoutConfirmPage.isCurrent(jsDriver)) {
|
||||
logoutConfirmPage.confirmLogout(jsDriver);
|
||||
waitForPageToLoad();
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
// ignore errors when checking logoutConfirm page, if an error tests will also fail
|
||||
logger.error("Exception during checking logout confirmation page", ex);
|
||||
}
|
||||
|
||||
if (validator != null) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
}
|
||||
|
||||
configured = false; // Loosing keycloak variable so we need to create it when init next session
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor configure() {
|
||||
return configure(null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor configure(JSObjectBuilder argumentsBuilder) {
|
||||
// a nasty hack: redirect console.warn to events
|
||||
// mainly for FF as it doesn't yet support reading console.warn directly through webdriver
|
||||
// see https://github.com/mozilla/geckodriver/issues/284
|
||||
jsExecutor.executeScript("console.warn = event;");
|
||||
|
||||
if (argumentsBuilder == null) {
|
||||
jsExecutor.executeScript("window.keycloak = new Keycloak('./keycloak.json');");
|
||||
} else {
|
||||
String configArguments = argumentsBuilder.build();
|
||||
jsExecutor.executeScript("window.keycloak = new Keycloak(" + configArguments + ");");
|
||||
}
|
||||
|
||||
jsExecutor.executeScript("window.keycloak.onAuthSuccess = function () {event('Auth Success')};"); // event function is declared in index.html
|
||||
jsExecutor.executeScript("window.keycloak.onAuthError = function () {event('Auth Error')}");
|
||||
jsExecutor.executeScript("window.keycloak.onAuthRefreshSuccess = function () {event('Auth Refresh Success')}");
|
||||
jsExecutor.executeScript("window.keycloak.onAuthRefreshError = function () {event('Auth Refresh Error')}");
|
||||
jsExecutor.executeScript("window.keycloak.onAuthLogout = function () {event('Auth Logout')}");
|
||||
jsExecutor.executeScript("window.keycloak.onTokenExpired = function () {event('Access token expired.')}");
|
||||
jsExecutor.executeScript("window.keycloak.onActionUpdate = function (status) {event('AIA status: ' + status)}");
|
||||
|
||||
configured = true;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder) {
|
||||
return init(argumentsBuilder, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder, JavascriptStateValidator validator) {
|
||||
return init(argumentsBuilder, validator, false);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor init(JSObjectBuilder argumentsBuilder, JavascriptStateValidator validator, boolean expectPromptNoneRedirect) {
|
||||
if(!configured) {
|
||||
configure();
|
||||
}
|
||||
|
||||
String arguments = argumentsBuilder != null ? argumentsBuilder.build() : "";
|
||||
|
||||
String script = "var callback = arguments[arguments.length - 1];" +
|
||||
" window.keycloak.init(" + arguments + ").then(function (authenticated) {" +
|
||||
" callback(\"Init Success (\" + (authenticated ? \"Authenticated\" : \"Not Authenticated\") + \")\");" +
|
||||
" }).catch(function (error) {" +
|
||||
" callback(error);" +
|
||||
" });";
|
||||
|
||||
Object output;
|
||||
|
||||
if (expectPromptNoneRedirect) {
|
||||
try {
|
||||
output = jsExecutor.executeAsyncScript(script);
|
||||
fail("Redirect to Keycloak was expected");
|
||||
}
|
||||
catch (WebDriverException e) {
|
||||
waitForPageToLoad();
|
||||
configured = false;
|
||||
// the redirect should use prompt=none, that means KC should immediately redirect back to the app (regardless login state)
|
||||
return init(argumentsBuilder, validator, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
output = jsExecutor.executeAsyncScript(script);
|
||||
}
|
||||
|
||||
if (validator != null) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor logInAndInit(JSObjectBuilder argumentsBuilder,
|
||||
UserRepresentation user, JavascriptStateValidator validator) {
|
||||
init(argumentsBuilder);
|
||||
login();
|
||||
loginForm(user);
|
||||
init(argumentsBuilder, validator);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor refreshToken(int value) {
|
||||
return refreshToken(value, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor refreshToken(int value, JavascriptStateValidator validator) {
|
||||
String script = "var callback = arguments[arguments.length - 1];" +
|
||||
" window.keycloak.updateToken(" + Integer.toString(value) + ").then(function (refreshed) {" +
|
||||
" if (refreshed) {" +
|
||||
" callback(window.keycloak.tokenParsed);" +
|
||||
" } else {" +
|
||||
" callback('Token not refreshed, valid for ' + Math.round(window.keycloak.tokenParsed.exp + window.keycloak.timeSkew - new Date().getTime() / 1000) + ' seconds');" +
|
||||
" }" +
|
||||
" }).catch(function () {" +
|
||||
" callback('Failed to refresh token');" +
|
||||
" });";
|
||||
|
||||
Object output = jsExecutor.executeAsyncScript(script);
|
||||
|
||||
if(validator != null) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor openAccountPage(JavascriptStateValidator validator) {
|
||||
jsExecutor.executeScript("window.keycloak.accountManagement()");
|
||||
waitForPageToLoad();
|
||||
|
||||
// Leaving page -> loosing keycloak variable
|
||||
configured = false;
|
||||
|
||||
if (validator != null) {
|
||||
validator.validate(jsDriver, null, null);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor getProfile() {
|
||||
return getProfile(null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor getProfile(JavascriptStateValidator validator) {
|
||||
|
||||
String script = "var callback = arguments[arguments.length - 1];" +
|
||||
" window.keycloak.loadUserProfile().then(function (profile) {" +
|
||||
" callback(profile);" +
|
||||
" }, function () {" +
|
||||
" callback('Failed to load profile');" +
|
||||
" });";
|
||||
|
||||
Object output = jsExecutor.executeAsyncScript(script);
|
||||
|
||||
if(validator != null) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor sendXMLHttpRequest(XMLHttpRequest request, ResponseValidator validator) {
|
||||
validator.validate(request.send(jsExecutor));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor refresh() {
|
||||
jsDriver.navigate().refresh();
|
||||
configured = false; // Refreshing webpage => Loosing window.keycloak variable
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor addTimeSkew(int addition) {
|
||||
jsExecutor.executeScript("window.keycloak.timeSkew += " + Integer.toString(addition));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor checkTimeSkew(JavascriptStateValidator validator) {
|
||||
Object timeSkew = jsExecutor.executeScript("return window.keycloak.timeSkew");
|
||||
|
||||
validator.validate(jsDriver, timeSkew, events);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor executeScript(String script) {
|
||||
return executeScript(script, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor executeScript(String script, JavascriptStateValidator validator) {
|
||||
Object output = jsExecutor.executeScript(script);
|
||||
|
||||
if(validator != null) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public boolean isLoggedIn() {
|
||||
return (boolean) jsExecutor.executeScript("if (typeof keycloak !== 'undefined') {" +
|
||||
"return keycloak.authenticated" +
|
||||
"} else { return false}");
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor executeAsyncScript(String script) {
|
||||
return executeAsyncScript(script, null);
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor executeAsyncScript(String script, JavascriptStateValidator validator) {
|
||||
Object output = jsExecutor.executeAsyncScript(script);
|
||||
|
||||
if(validator != null) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor errorResponse(JavascriptStateValidator validator) {
|
||||
Object output = jsExecutor.executeScript("return \"Error: \" + getParameterByName(\"error\") + \"\\n\" + \"Error description: \" + getParameterByName(\"error_description\")");
|
||||
|
||||
validator.validate(jsDriver, output, events);
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor wait(long millis, JavascriptStateValidator validator) {
|
||||
pause(millis);
|
||||
|
||||
if (validator != null) {
|
||||
validator.validate(jsDriver, null, events);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public JavascriptTestExecutor validateOutputField(JavascriptStateValidator validator) {
|
||||
validator.validate(jsDriver, output, events);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package org.keycloak.testsuite.util.javascript;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public interface ResponseValidator extends Serializable {
|
||||
|
||||
void validate(Map<String, Object> response);
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.keycloak.testsuite.util.javascript;
|
||||
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class XMLHttpRequest {
|
||||
|
||||
private String url;
|
||||
private String method;
|
||||
private Map<String, String> headers;
|
||||
private String content;
|
||||
|
||||
public static XMLHttpRequest create() {
|
||||
return new XMLHttpRequest();
|
||||
}
|
||||
|
||||
private XMLHttpRequest() {}
|
||||
|
||||
public XMLHttpRequest url(String url) {
|
||||
this.url = url;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public XMLHttpRequest method(String method) {
|
||||
this.method = method;
|
||||
return this;
|
||||
}
|
||||
|
||||
public XMLHttpRequest content(String content) {
|
||||
this.content = content;
|
||||
return this;
|
||||
}
|
||||
|
||||
public XMLHttpRequest addHeader(String key, String value) {
|
||||
if (headers == null) {
|
||||
headers = new HashMap<>();
|
||||
}
|
||||
|
||||
headers.put(key, value);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public XMLHttpRequest includeBearerToken() {
|
||||
addHeader("Authorization", "Bearer ' + keycloak.token + '");
|
||||
return this;
|
||||
}
|
||||
|
||||
public XMLHttpRequest includeRpt() {
|
||||
addHeader("Authorization", "Bearer ' + authorization.rpt + '");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Object> send(JavascriptExecutor jsExecutor) {
|
||||
String requestCode = "var callback = arguments[arguments.length - 1];" +
|
||||
"var req = new XMLHttpRequest();" +
|
||||
" req.open('" + method + "', '" + url + "', true);" +
|
||||
getHeadersString() +
|
||||
" req.onreadystatechange = function () {" +
|
||||
" if (req.readyState == 4) {" +
|
||||
" callback({\"status\" : req.status, \"responseHeaders\" : req.getAllResponseHeaders(), \"res\" : req.response})" +
|
||||
" }" +
|
||||
" };" +
|
||||
" req.send(" + content + ");";
|
||||
|
||||
return (Map<String, Object>) jsExecutor.executeAsyncScript(requestCode);
|
||||
}
|
||||
|
||||
private String getHeadersString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
builder.append("req.setRequestHeader('")
|
||||
.append(entry.getKey())
|
||||
.append("', '")
|
||||
.append(entry.getValue())
|
||||
.append("');");
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
package org.keycloak.testsuite.javascript;
|
||||
|
||||
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.junit.BeforeClass;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractAuthTest;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.auth.page.login.OIDCLogin;
|
||||
import org.keycloak.testsuite.util.ClientBuilder;
|
||||
import org.keycloak.testsuite.util.ContainerAssume;
|
||||
import org.keycloak.testsuite.util.JavascriptBrowser;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.RolesBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.javascript.JavascriptStateValidator;
|
||||
import org.keycloak.testsuite.util.javascript.ResponseValidator;
|
||||
import org.openqa.selenium.By;
|
||||
import org.openqa.selenium.Cookie;
|
||||
import org.openqa.selenium.JavascriptExecutor;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebDriver.Options;
|
||||
import org.openqa.selenium.WebElement;
|
||||
import org.openqa.selenium.support.FindBy;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.anyOf;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.collection.IsMapContaining.hasEntry;
|
||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST;
|
||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST2;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public abstract class AbstractJavascriptTest extends AbstractAuthTest {
|
||||
|
||||
@FunctionalInterface
|
||||
interface QuadFunction<T, U, V, W> {
|
||||
void apply(T a, U b, V c, W d);
|
||||
}
|
||||
|
||||
public static final String JS_APP_HOST = AUTH_SERVER_HOST2;
|
||||
public static final String CLIENT_ID = "js-console";
|
||||
public static final String REALM_NAME = "test";
|
||||
public static final String SPACE_REALM_NAME = "Example realm";
|
||||
public static final String JAVASCRIPT_URL = "/auth/realms/" + REALM_NAME + "/testing/javascript";
|
||||
public static final String JAVASCRIPT_ENCODED_SPACE_URL = "/auth/realms/Example%20realm/testing/javascript";
|
||||
public static final String JAVASCRIPT_SPACE_URL = "/auth/realms/Example realm/testing/javascript";
|
||||
public static int TOKEN_LIFESPAN_LEEWAY = 3; // seconds
|
||||
public static final String USER_PASSWORD = "password";
|
||||
|
||||
|
||||
protected JavascriptExecutor jsExecutor;
|
||||
|
||||
// Javascript browser needed KEYCLOAK-4703
|
||||
@Drone
|
||||
@JavascriptBrowser
|
||||
protected WebDriver jsDriver;
|
||||
|
||||
@Page
|
||||
@JavascriptBrowser
|
||||
protected OIDCLogin jsDriverTestRealmLoginPage;
|
||||
|
||||
@FindBy(id = "output")
|
||||
@JavascriptBrowser
|
||||
protected WebElement outputArea;
|
||||
|
||||
@FindBy(id = "events")
|
||||
@JavascriptBrowser
|
||||
protected WebElement eventsArea;
|
||||
|
||||
public static final UserRepresentation testUser;
|
||||
public static final UserRepresentation unauthorizedUser;
|
||||
|
||||
static {
|
||||
testUser = UserBuilder.create().username("test-user@localhost").password(USER_PASSWORD).build();
|
||||
unauthorizedUser = UserBuilder.create().username("unauthorized").password(USER_PASSWORD).build();
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
public static void enabledOnlyWithSSL() {
|
||||
ContainerAssume.assumeAuthServerSSL();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void beforeJavascriptTest() {
|
||||
jsExecutor = (JavascriptExecutor) jsDriver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addTestRealms(List<RealmRepresentation> testRealms) {
|
||||
testRealms.add(updateRealm(RealmBuilder.create()
|
||||
.name(REALM_NAME)
|
||||
.roles(
|
||||
RolesBuilder.create()
|
||||
.realmRole(new RoleRepresentation("user", "", false))
|
||||
.realmRole(new RoleRepresentation("admin", "", false))
|
||||
)
|
||||
.user(
|
||||
UserBuilder.create()
|
||||
.username("test-user@localhost").password("password")
|
||||
.addRoles("user")
|
||||
.role("realm-management", "view-realm")
|
||||
.role("realm-management", "manage-users")
|
||||
.role("account", "view-profile")
|
||||
.role("account", "manage-account")
|
||||
)
|
||||
.user(
|
||||
UserBuilder.create()
|
||||
.username("unauthorized").password("password")
|
||||
)
|
||||
.client(
|
||||
ClientBuilder.create()
|
||||
.clientId(CLIENT_ID)
|
||||
.redirectUris(oauth.SERVER_ROOT.replace(AUTH_SERVER_HOST, JS_APP_HOST) + JAVASCRIPT_URL + "/*", oauth.SERVER_ROOT + JAVASCRIPT_ENCODED_SPACE_URL + "/*")
|
||||
.addWebOrigin(oauth.SERVER_ROOT.replace(AUTH_SERVER_HOST, JS_APP_HOST))
|
||||
.publicClient()
|
||||
)
|
||||
.accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY)
|
||||
.testEventListener()
|
||||
));
|
||||
}
|
||||
|
||||
protected <T> JavascriptStateValidator buildFunction(QuadFunction<T, WebDriver, Object, WebElement> f, T x) {
|
||||
return (y,z,w) -> f.apply(x, y, z, w);
|
||||
}
|
||||
|
||||
protected void setImplicitFlowForClient() {
|
||||
ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realms().realm(REALM_NAME), CLIENT_ID);
|
||||
ClientRepresentation client = clientResource.toRepresentation();
|
||||
client.setImplicitFlowEnabled(true);
|
||||
client.setStandardFlowEnabled(false);
|
||||
clientResource.update(client);
|
||||
}
|
||||
|
||||
protected void setStandardFlowForClient() {
|
||||
ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realms().realm(REALM_NAME), CLIENT_ID);
|
||||
ClientRepresentation client = clientResource.toRepresentation();
|
||||
client.setImplicitFlowEnabled(false);
|
||||
client.setStandardFlowEnabled(true);
|
||||
clientResource.update(client);
|
||||
}
|
||||
|
||||
protected abstract RealmRepresentation updateRealm(RealmBuilder builder);
|
||||
|
||||
protected void assertInitAuth(WebDriver driver1, Object output, WebElement events) {
|
||||
buildFunction(this::assertOutputContains, "Init Success (Authenticated)").validate(driver1, output, events);
|
||||
waitUntilElement(events).text().contains("Auth Success");
|
||||
}
|
||||
|
||||
protected void assertInitNotAuth(WebDriver driver1, Object output, WebElement events) {
|
||||
buildFunction(this::assertOutputContains, "Init Success (Not Authenticated)").validate(driver1, output, events);
|
||||
}
|
||||
|
||||
protected void assertOnLoginPage(WebDriver driver1, Object output, WebElement events) {
|
||||
waitUntilElement(By.tagName("body")).is().present();
|
||||
assertCurrentUrlStartsWith(jsDriverTestRealmLoginPage, driver1);
|
||||
}
|
||||
|
||||
public void assertOutputWebElementContains(String value, WebDriver driver1, Object output, WebElement events) {
|
||||
waitUntilElement((WebElement) output).text().contains(value);
|
||||
}
|
||||
|
||||
public void assertLocaleCookie(String locale, WebDriver driver1, Object output, WebElement events) {
|
||||
waitForPageToLoad();
|
||||
Options ops = driver1.manage();
|
||||
Cookie cookie = ops.getCookieNamed("KEYCLOAK_LOCALE");
|
||||
Assert.assertNotNull(cookie);
|
||||
Assert.assertEquals(locale, cookie.getValue());
|
||||
}
|
||||
|
||||
public JavascriptStateValidator assertLocaleIsSet(String locale) {
|
||||
return buildFunction(this::assertLocaleCookie, locale);
|
||||
}
|
||||
|
||||
public void assertOutputContains(String value, WebDriver driver1, Object output, WebElement events) {
|
||||
if (output instanceof WebElement) {
|
||||
waitUntilElement((WebElement) output).text().contains(value);
|
||||
} else {
|
||||
assertThat((String) output, containsString(value));
|
||||
}
|
||||
}
|
||||
|
||||
public void assertEventsWebElementContains(String value, WebDriver driver1, Object output, WebElement events) {
|
||||
waitUntilElement(events).text().contains(value);
|
||||
}
|
||||
|
||||
public void assertEventsWebElementDoesntContain(String value, WebDriver driver1, Object output, WebElement events) {
|
||||
waitUntilElement(events).text().not().contains(value);
|
||||
}
|
||||
|
||||
public ResponseValidator assertResponseStatus(long status) {
|
||||
return output -> assertThat(output, hasEntry("status", status));
|
||||
}
|
||||
|
||||
public JavascriptStateValidator assertOutputContains(String text) {
|
||||
return buildFunction(this::assertOutputContains, text);
|
||||
}
|
||||
|
||||
public JavascriptStateValidator assertEventsContains(String text) {
|
||||
return buildFunction(this::assertEventsWebElementContains, text);
|
||||
}
|
||||
|
||||
public JavascriptStateValidator assertEventsDoesntContain(String text) {
|
||||
return buildFunction(this::assertEventsWebElementDoesntContain, text);
|
||||
}
|
||||
|
||||
public void assertErrorResponse(String expectedError, WebDriver drv, Object output, WebElement evt) {
|
||||
Assert.assertNotNull("Empty error response", output);
|
||||
Assert.assertTrue("Invalid error response type", output instanceof Map);
|
||||
assertThat((Map<String, String>) output, anyOf(hasEntry("error", expectedError), hasEntry("error_description", expectedError)));
|
||||
}
|
||||
|
||||
public JavascriptStateValidator assertErrorResponse(String expectedError) {
|
||||
return buildFunction(this::assertErrorResponse, expectedError);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,969 +0,0 @@
|
||||
package org.keycloak.testsuite.javascript;
|
||||
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.common.util.Retry;
|
||||
import org.keycloak.common.util.UriUtils;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.ClaimsRepresentation;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.arquillian.SuiteContext;
|
||||
import org.keycloak.testsuite.auth.page.login.OAuthGrant;
|
||||
import org.keycloak.testsuite.auth.page.login.UpdatePassword;
|
||||
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.JavascriptBrowser;
|
||||
import org.keycloak.testsuite.util.AccountHelper;
|
||||
import org.keycloak.testsuite.util.RealmBuilder;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.testsuite.util.oauth.AccessTokenResponse;
|
||||
import org.keycloak.testsuite.util.javascript.JSObjectBuilder;
|
||||
import org.keycloak.testsuite.util.javascript.JavascriptStateValidator;
|
||||
import org.keycloak.testsuite.util.javascript.JavascriptTestExecutor;
|
||||
import org.keycloak.testsuite.util.javascript.XMLHttpRequest;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.openqa.selenium.TimeoutException;
|
||||
import org.openqa.selenium.WebDriver;
|
||||
import org.openqa.selenium.WebDriverException;
|
||||
import org.openqa.selenium.WebElement;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static java.lang.Math.toIntExact;
|
||||
import static org.hamcrest.CoreMatchers.anyOf;
|
||||
import static org.hamcrest.CoreMatchers.both;
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.hamcrest.Matchers.lessThan;
|
||||
import static org.hamcrest.collection.IsMapContaining.hasEntry;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_HOST;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
|
||||
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.pause;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitForPageToLoad;
|
||||
import static org.keycloak.testsuite.util.WaitUtils.waitUntilElement;
|
||||
|
||||
/**
|
||||
* @author mhajas
|
||||
*/
|
||||
public class JavascriptAdapterTest extends AbstractJavascriptTest {
|
||||
|
||||
private String testAppUrl;
|
||||
private String testAppWithInitInHeadUrl;
|
||||
protected JavascriptTestExecutor testExecutor;
|
||||
private static int TIME_SKEW_TOLERANCE = 3;
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Page
|
||||
@JavascriptBrowser
|
||||
private OAuthGrant oAuthGrantPage;
|
||||
|
||||
@Page
|
||||
@JavascriptBrowser
|
||||
private UpdatePassword updatePasswordPage;
|
||||
|
||||
@Override
|
||||
protected RealmRepresentation updateRealm(RealmBuilder builder) {
|
||||
return builder.accessTokenLifespan(30 + TOKEN_LIFESPAN_LEEWAY).build();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setDefaultEnvironment() {
|
||||
String testAppRootUrl = authServerContextRootPage.toString().replace(AUTH_SERVER_HOST, JS_APP_HOST) + JAVASCRIPT_URL;
|
||||
testAppUrl = testAppRootUrl + "/index.html";
|
||||
testAppWithInitInHeadUrl = testAppRootUrl + "/init-in-head.html";
|
||||
|
||||
jsDriverTestRealmLoginPage.setAuthRealm(REALM_NAME);
|
||||
oAuthGrantPage.setAuthRealm(REALM_NAME);
|
||||
oauth.realm(REALM_NAME);
|
||||
|
||||
jsDriver.navigate().to(oauth.getLoginFormUrl());
|
||||
waitForPageToLoad();
|
||||
events.poll();
|
||||
jsDriver.manage().deleteAllCookies();
|
||||
|
||||
navigateToTestApp(testAppUrl);
|
||||
|
||||
testExecutor = JavascriptTestExecutor.create(jsDriver, jsDriverTestRealmLoginPage);
|
||||
|
||||
jsDriver.manage().deleteAllCookies();
|
||||
|
||||
setStandardFlowForClient();
|
||||
|
||||
//tests cleanup
|
||||
oauth.setDriver(driver);
|
||||
setTimeOffset(0);
|
||||
}
|
||||
|
||||
protected JSObjectBuilder defaultArguments() {
|
||||
return JSObjectBuilder.create().defaultSettings();
|
||||
}
|
||||
|
||||
private void assertOnTestAppUrl(WebDriver jsDriver, Object output, WebElement events) {
|
||||
assertOnTestAppUrl(jsDriver, output, events, testAppUrl);
|
||||
}
|
||||
|
||||
private void assertOnTestAppWithInitInHeadUrl(WebDriver jsDriver, Object output, WebElement events) {
|
||||
assertOnTestAppUrl(jsDriver, output, events, testAppWithInitInHeadUrl);
|
||||
}
|
||||
|
||||
private void assertOnTestAppUrl(WebDriver jsDriver, Object output, WebElement events, String testAppUrl) {
|
||||
waitForPageToLoad();
|
||||
assertCurrentUrlStartsWith(testAppUrl, jsDriver);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testJSConsoleAuth() {
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(UserBuilder.create().username("user").password("invalid-password").build(),
|
||||
(driver1, output, events) -> assertCurrentUrlDoesntStartWith(testAppUrl, driver1))
|
||||
.loginForm(UserBuilder.create().username("invalid-user").password("password").build(),
|
||||
(driver1, output, events) -> assertCurrentUrlDoesntStartWith(testAppUrl, driver1))
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitAuth)
|
||||
.logout(this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitNotAuth);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLoginWithPkceS256() {
|
||||
JSObjectBuilder pkceS256 = defaultArguments().pkceS256();
|
||||
testExecutor.init(pkceS256, this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(pkceS256, this::assertInitAuth)
|
||||
.logout(this::assertOnTestAppUrl)
|
||||
.init(pkceS256, this::assertInitNotAuth);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogoutWithDefaults() {
|
||||
boolean stillLoggedIn = testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitAuth)
|
||||
.logout(this::assertOnTestAppUrl)
|
||||
.isLoggedIn();
|
||||
assertFalse("still logged in", stillLoggedIn);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogoutWithInitOptionsPostMethod() {
|
||||
boolean stillLoggedIn = testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments().add("logoutMethod", "POST"), this::assertInitAuth)
|
||||
.logout(this::assertOnTestAppUrl, null)
|
||||
.isLoggedIn();
|
||||
assertFalse("still logged in", stillLoggedIn);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLogoutWithOptionsPostMethod() {
|
||||
boolean stillLoggedIn = testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitAuth)
|
||||
.logout(this::assertOnTestAppUrl, null, JSObjectBuilder.create().add("logoutMethod", "POST"))
|
||||
.isLoggedIn();
|
||||
assertFalse("still logged in", stillLoggedIn);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSilentCheckSso() {
|
||||
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad()
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage.toString().replace(AUTH_SERVER_HOST, JS_APP_HOST) + JAVASCRIPT_URL + "/silent-check-sso.html");
|
||||
|
||||
// when 3rd party cookies are disabled, the adapter has to do a full redirect to KC to check whether the user
|
||||
// is logged in or not – it can't rely on silent check-sso iframe
|
||||
testExecutor.init(checkSSO, this::assertInitNotAuth, SuiteContext.BROWSER_STRICT_COOKIES)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(checkSSO, this::assertInitAuth, false)
|
||||
.refresh()
|
||||
.init(checkSSO
|
||||
, this::assertInitAuth, SuiteContext.BROWSER_STRICT_COOKIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSilentCheckSsoLoginWithLoginIframeDisabled() {
|
||||
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad()
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage.toString().replace(AUTH_SERVER_HOST, JS_APP_HOST) + JAVASCRIPT_URL + "/silent-check-sso.html");
|
||||
|
||||
testExecutor.init(checkSSO, this::assertInitNotAuth, SuiteContext.BROWSER_STRICT_COOKIES)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(checkSSO, this::assertInitAuth, false)
|
||||
.refresh()
|
||||
.init(checkSSO
|
||||
.disableCheckLoginIframe()
|
||||
, this::assertInitAuth, SuiteContext.BROWSER_STRICT_COOKIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSilentCheckSsoWithFallbackDisabled() {
|
||||
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad().disableSilentCheckSSOFallback()
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage.toString().replace(AUTH_SERVER_HOST, JS_APP_HOST) + JAVASCRIPT_URL + "/silent-check-sso.html");
|
||||
|
||||
testExecutor.init(checkSSO, this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(checkSSO, this::assertInitAuth)
|
||||
.refresh()
|
||||
.init(checkSSO
|
||||
// with the fall back disabled, the adapter won't do full redirect to KC
|
||||
, SuiteContext.BROWSER_STRICT_COOKIES ? this::assertInitNotAuth : this::assertInitAuth);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitNoOptions() {
|
||||
testExecutor.init(null, this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(null, this::assertInitAuth)
|
||||
.logout(this::assertOnTestAppUrl)
|
||||
.init(null, this::assertInitNotAuth);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCheckSso() {
|
||||
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad();
|
||||
|
||||
// when 3rd party cookies are disabled, the adapter has to do a full redirect to KC to check whether the user
|
||||
// is logged in or not – it can't rely on the login iframe
|
||||
testExecutor.init(checkSSO, this::assertInitNotAuth, SuiteContext.BROWSER_STRICT_COOKIES)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(checkSSO, this::assertInitAuth, false)
|
||||
.refresh()
|
||||
.init(checkSSO, this::assertInitAuth, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSilentCheckSsoNotAuthenticated() {
|
||||
JSObjectBuilder checkSSO = defaultArguments().checkSSOOnLoad()
|
||||
.add("checkLoginIframe", false)
|
||||
.add("silentCheckSsoRedirectUri", authServerContextRootPage.toString().replace(AUTH_SERVER_HOST, JS_APP_HOST) + JAVASCRIPT_URL + "/silent-check-sso.html");
|
||||
|
||||
testExecutor.init(checkSSO
|
||||
, this::assertInitNotAuth, SuiteContext.BROWSER_STRICT_COOKIES);
|
||||
}
|
||||
|
||||
@Test
|
||||
// KEYCLOAK-13206
|
||||
public void testIframeInit() {
|
||||
JSObjectBuilder iframeInterval = defaultArguments().setCheckLoginIframeIntervalTo1(); // to speed up the test a bit
|
||||
testExecutor.init(iframeInterval)
|
||||
.login()
|
||||
.loginForm(testUser)
|
||||
.init(iframeInterval)
|
||||
.wait(2000, (driver1, output, events) -> { // iframe is initialized after ~1 second, 2 seconds is just to be sure
|
||||
assertAdapterIsLoggedIn(driver1, output, events);
|
||||
final String logMsg = "Your browser is blocking access to 3rd-party cookies, this means:";
|
||||
if (SuiteContext.BROWSER_STRICT_COOKIES) {
|
||||
// this is here not really to test the log but also to make sure the browser is configured properly
|
||||
// and cookies were blocked
|
||||
assertEventsWebElementContains(logMsg, driver1, output, events);
|
||||
}
|
||||
else {
|
||||
assertEventsWebElementDoesntContain(logMsg, driver1, output, events);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshToken() {
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.refreshToken(9999, assertOutputContains("Failed to refresh token"))
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitAuth)
|
||||
.refreshToken(9999, assertEventsContains("Auth Refresh Success"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRefreshTokenIfUnder30s() {
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitAuth)
|
||||
.refreshToken(30, assertOutputContains("Token not refreshed, valid for"))
|
||||
.addTimeSkew(-5) // instead of wait move in time
|
||||
.refreshToken(30, assertEventsContains("Auth Refresh Success"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetProfile() {
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.getProfile(assertOutputContains("Failed to load profile"))
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitAuth)
|
||||
.getProfile((driver1, output, events) -> assertThat((Map<String, String>) output, hasEntry("username", testUser.getUsername())));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void grantBrowserBasedApp() {
|
||||
ClientResource clientResource = ApiUtil.findClientResourceByClientId(adminClient.realm(REALM_NAME), CLIENT_ID);
|
||||
ClientRepresentation client = clientResource.toRepresentation();
|
||||
try {
|
||||
client.setConsentRequired(true);
|
||||
clientResource.update(client);
|
||||
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, (driver1, output, events) -> assertTrue(oAuthGrantPage.isCurrent(driver1))
|
||||
// I am not sure why is this driver1 argument to isCurrent necessary, but I got exception without it
|
||||
);
|
||||
|
||||
oAuthGrantPage.accept();
|
||||
|
||||
EventRepresentation loginEvent = events.expectLogin()
|
||||
.client(CLIENT_ID)
|
||||
.detail(Details.CONSENT, Details.CONSENT_VALUE_CONSENT_GRANTED)
|
||||
.detail(Details.REDIRECT_URI, testAppUrl)
|
||||
.detail(Details.USERNAME, testUser.getUsername())
|
||||
.assertEvent();
|
||||
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||
|
||||
testExecutor.init(defaultArguments(), this::assertInitAuth);
|
||||
|
||||
driver.navigate().to(oauth.getLoginFormUrl());
|
||||
events.expectCodeToToken(codeId, loginEvent.getSessionId()).client(CLIENT_ID).assertEvent();
|
||||
|
||||
AccountHelper.revokeConsents(adminClient.realm(REALM_NAME), testUser.getUsername(),CLIENT_ID);
|
||||
Assert.assertTrue(AccountHelper.getUserConsents(adminClient.realm(REALM_NAME), testUser.getUsername()).isEmpty());
|
||||
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
testExecutor.configure() // need to configure because we refreshed page
|
||||
.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login((driver1, output, events) -> assertTrue(oAuthGrantPage.isCurrent(driver1)));
|
||||
} finally {
|
||||
// Clean
|
||||
client.setConsentRequired(false);
|
||||
clientResource.update(client);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void implicitFlowTest() {
|
||||
testExecutor.init(defaultArguments().implicitFlow(), this::assertInitNotAuth)
|
||||
.login(this::assertOnTestAppUrl)
|
||||
.errorResponse(assertOutputContains("Implicit flow is disabled for the client"));
|
||||
|
||||
setImplicitFlowForClient();
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnTestAppUrl)
|
||||
.errorResponse(assertOutputContains("Standard flow is disabled for the client"));
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
|
||||
testExecutor.init(defaultArguments().implicitFlow(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments().implicitFlow(), this::assertInitAuth);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCertEndpoint() {
|
||||
testExecutor.logInAndInit(defaultArguments(), testUser, this::assertInitAuth)
|
||||
.sendXMLHttpRequest(XMLHttpRequest.create()
|
||||
.url(authServerContextRootPage + "/auth/realms/" + REALM_NAME + "/protocol/openid-connect/certs")
|
||||
.method("GET")
|
||||
.addHeader("Accept", "application/json")
|
||||
.addHeader("Authorization", "Bearer ' + keycloak.token + '"),
|
||||
assertResponseStatus(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void implicitFlowQueryTest() {
|
||||
setImplicitFlowForClient();
|
||||
testExecutor.init(JSObjectBuilder.create().implicitFlow().queryResponse(), this::assertInitNotAuth)
|
||||
.login((driver1, output, events1) -> Retry.execute(
|
||||
() -> assertThat(driver1.getCurrentUrl(), containsString("Response_mode+%27query%27+not+allowed")),
|
||||
20, 50)
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void implicitFlowRefreshTokenTest() {
|
||||
setImplicitFlowForClient();
|
||||
testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertInitAuth)
|
||||
.refreshToken(9999, assertOutputContains("Failed to refresh token"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void implicitFlowOnTokenExpireTest() {
|
||||
try (RealmAttributeUpdater rau = new RealmAttributeUpdater(adminClient.realms().realm(REALM_NAME))
|
||||
.setAccessTokenLifespanForImplicitFlow(3)
|
||||
.update()
|
||||
) {
|
||||
setImplicitFlowForClient();
|
||||
|
||||
testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertInitAuth);
|
||||
assertThat(driver.getPageSource(), not(containsString("Access token expired")));
|
||||
|
||||
// Here we can't move in time because we are waiting for onTokenExpired execution which is already
|
||||
// scheduled by setTimeout method, so we can't make it execute sooner
|
||||
pause(1000);
|
||||
|
||||
waitUntilElement(eventsArea).text().contains("Access token expired");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void implicitFlowCertEndpoint() {
|
||||
setImplicitFlowForClient();
|
||||
testExecutor.logInAndInit(defaultArguments().implicitFlow(), testUser, this::assertInitAuth)
|
||||
.sendXMLHttpRequest(XMLHttpRequest.create()
|
||||
.url(authServerContextRootPage + "/auth/realms/" + REALM_NAME + "/protocol/openid-connect/certs")
|
||||
.method("GET")
|
||||
.addHeader("Accept", "application/json")
|
||||
.addHeader("Authorization", "Bearer ' + keycloak.token + '"),
|
||||
assertResponseStatus(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBearerRequest() {
|
||||
XMLHttpRequest request = XMLHttpRequest.create()
|
||||
.url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/roles")
|
||||
.method("GET")
|
||||
.addHeader("Accept", "application/json")
|
||||
.addHeader("Authorization", "Bearer ' + keycloak.token + '");
|
||||
|
||||
testExecutor.init(defaultArguments())
|
||||
// Possibility of 0 and 401 is caused by this issue: https://issues.redhat.com/browse/KEYCLOAK-12686
|
||||
.sendXMLHttpRequest(request, response -> assertThat(response, hasEntry(is("status"), anyOf(is(0L), is(401L)))))
|
||||
.refresh();
|
||||
testExecutor.logInAndInit(defaultArguments(), unauthorizedUser, this::assertInitAuth)
|
||||
.sendXMLHttpRequest(request, output -> assertThat(output, hasEntry("status", 403L)))
|
||||
.logout(this::assertOnTestAppUrl)
|
||||
.refresh();
|
||||
testExecutor.logInAndInit(defaultArguments(), testUser, this::assertInitAuth)
|
||||
.sendXMLHttpRequest(request, assertResponseStatus(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginRequiredAction() {
|
||||
try {
|
||||
testExecutor.init(defaultArguments().loginRequiredOnLoad());
|
||||
// This throws exception because when JavascriptExecutor waits for AsyncScript to finish
|
||||
// it is redirected to login page and executor gets no response
|
||||
|
||||
throw new RuntimeException("Probably the login-required OnLoad mode doesn't work, because testExecutor should fail with error that page was redirected.");
|
||||
} catch (WebDriverException ex) {
|
||||
// should happen
|
||||
}
|
||||
|
||||
testExecutor.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), this::assertInitAuth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for scope handling via {@code initOptions}: <pre>{@code
|
||||
* Keycloak keycloak = new Keycloak(); keycloak.init({.... scope: "profile email phone"})
|
||||
* }</pre>
|
||||
* See KEYCLOAK-14412
|
||||
*/
|
||||
@Test
|
||||
public void testScopeInInitOptionsShouldBeConsideredByLoginUrl() {
|
||||
|
||||
JSObjectBuilder initOptions = defaultArguments()
|
||||
.loginRequiredOnLoad()
|
||||
// phone is optional client scope
|
||||
.add("scope", "openid profile email phone");
|
||||
|
||||
try {
|
||||
testExecutor.init(initOptions);
|
||||
// This throws exception because when JavascriptExecutor waits for AsyncScript to finish
|
||||
// it is redirected to login page and executor gets no response
|
||||
|
||||
throw new RuntimeException("Probably the login-required OnLoad mode doesn't work, because testExecutor should fail with error that page was redirected.");
|
||||
} catch (WebDriverException ex) {
|
||||
// should happen
|
||||
}
|
||||
|
||||
testExecutor.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(initOptions, this::assertAdapterIsLoggedIn)
|
||||
.executeScript("return window.keycloak.tokenParsed.scope", assertOutputContains("phone"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for scope handling via {@code loginOptions}: <pre>{@code
|
||||
* Keycloak keycloak = new Keycloak(); keycloak.login({.... scope: "profile email phone"})
|
||||
* }</pre>
|
||||
* See KEYCLOAK-14412
|
||||
*/
|
||||
@Test
|
||||
public void testScopeInLoginOptionsShouldBeConsideredByLoginUrl() {
|
||||
|
||||
testExecutor.configure().init(defaultArguments());
|
||||
|
||||
JSObjectBuilder loginOptions = JSObjectBuilder.create().add("scope", "profile email phone");
|
||||
|
||||
testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> {
|
||||
assertThat(driver.getCurrentUrl(), containsString("&scope=openid%20profile%20email%20phone"));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for acr handling via {@code loginOptions}: <pre>{@code
|
||||
* Keycloak keycloak = new Keycloak(); keycloak.login({.... acr: { values: ["foo", "bar"], essential: false}})
|
||||
* }</pre>
|
||||
*/
|
||||
@Test
|
||||
public void testAcrInLoginOptionsShouldBeConsideredByLoginUrl() {
|
||||
// Test when no "acr" option given. Claims parameter won't be passed to Keycloak server
|
||||
testExecutor.configure().init(defaultArguments());
|
||||
JSObjectBuilder loginOptions = JSObjectBuilder.create();
|
||||
|
||||
testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> {
|
||||
try {
|
||||
String queryString = new URL(driver.getCurrentUrl()).getQuery();
|
||||
String claimsParam = UriUtils.decodeQueryString(queryString).getFirst(OIDCLoginProtocol.CLAIMS_PARAM);
|
||||
Assert.assertNull(claimsParam);
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
});
|
||||
|
||||
// Test given "acr" option will be translated into the "claims" parameter passed to Keycloak server
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
testExecutor.configure().init(defaultArguments());
|
||||
|
||||
JSObjectBuilder acr1 = JSObjectBuilder.create()
|
||||
.add("values", new String[] {"foo", "bar"})
|
||||
.add("essential", false);
|
||||
loginOptions = JSObjectBuilder.create().add("acr", acr1);
|
||||
|
||||
testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> {
|
||||
try {
|
||||
String queryString = new URL(driver.getCurrentUrl()).getQuery();
|
||||
String claimsParam = UriUtils.decodeQueryString(queryString).getFirst(OIDCLoginProtocol.CLAIMS_PARAM);
|
||||
Assert.assertNotNull(claimsParam);
|
||||
|
||||
ClaimsRepresentation claimsRep = JsonSerialization.readValue(claimsParam, ClaimsRepresentation.class);
|
||||
ClaimsRepresentation.ClaimValue<String> claimValue = claimsRep.getClaimValue(IDToken.ACR, ClaimsRepresentation.ClaimContext.ID_TOKEN, String.class);
|
||||
Assert.assertNames(claimValue.getValues(), "foo", "bar");
|
||||
assertThat(claimValue.isEssential(), is(false));
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test for {@code acr_values} handling via {@code loginOptions}: <pre>{@code
|
||||
* Keycloak keycloak = new Keycloak(); keycloak.login({...., acrValues: "1"})
|
||||
* }</pre>
|
||||
*/
|
||||
@Test
|
||||
public void testAcrValuesInLoginOptionsShouldBeConsideredByLoginUrl() {
|
||||
|
||||
testExecutor.configure().init(defaultArguments());
|
||||
JSObjectBuilder loginOptions = JSObjectBuilder.create();
|
||||
|
||||
testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> {
|
||||
try {
|
||||
String queryString = new URL(driver.getCurrentUrl()).getQuery();
|
||||
String acrValues = UriUtils.decodeQueryString(queryString).getFirst(OIDCLoginProtocol.ACR_PARAM);
|
||||
Assert.assertNull(acrValues);
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
});
|
||||
|
||||
// Test given "acrValues" option will be translated into the "acr_values" parameter passed to Keycloak server
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
testExecutor.configure().init(defaultArguments());
|
||||
|
||||
loginOptions = JSObjectBuilder.create().acrValues("2fa");
|
||||
|
||||
testExecutor.login(loginOptions, (JavascriptStateValidator) (driver, output, events) -> {
|
||||
try {
|
||||
String queryString = new URL(driver.getCurrentUrl()).getQuery();
|
||||
String acrValuesParam = UriUtils.decodeQueryString(queryString).getFirst(OIDCLoginProtocol.ACR_PARAM);
|
||||
Assert.assertNotNull(acrValuesParam);
|
||||
assertThat(acrValuesParam, is("2fa"));
|
||||
} catch (IOException ioe) {
|
||||
throw new AssertionError(ioe);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateToken() {
|
||||
XMLHttpRequest request = XMLHttpRequest.create()
|
||||
.url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/roles")
|
||||
.method("GET")
|
||||
.addHeader("Accept", "application/json")
|
||||
.addHeader("Authorization", "Bearer ' + keycloak.token + '");
|
||||
|
||||
testExecutor.logInAndInit(defaultArguments(), testUser, this::assertInitAuth)
|
||||
.addTimeSkew(-33);
|
||||
setTimeOffset(33);
|
||||
testExecutor.refreshToken(5, assertEventsContains("Auth Refresh Success"));
|
||||
|
||||
setTimeOffset(67);
|
||||
testExecutor.addTimeSkew(-34)
|
||||
// Possibility of 0 and 401 is caused by this issue: https://issues.redhat.com/browse/KEYCLOAK-12686
|
||||
.sendXMLHttpRequest(request, response -> assertThat(response, hasEntry(is("status"), anyOf(is(0L), is(401L)))))
|
||||
.refreshToken(5, assertEventsContains("Auth Refresh Success"))
|
||||
.sendXMLHttpRequest(request, assertResponseStatus(200));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void timeSkewTest() {
|
||||
testExecutor.logInAndInit(defaultArguments(), testUser, this::assertInitAuth)
|
||||
.checkTimeSkew((driver1, output, events) -> assertThat(toIntExact((long) output),
|
||||
is(
|
||||
both(greaterThan(0 - TIME_SKEW_TOLERANCE))
|
||||
.and(lessThan(TIME_SKEW_TOLERANCE))
|
||||
)
|
||||
));
|
||||
|
||||
setTimeOffset(40);
|
||||
|
||||
testExecutor.refreshToken(9999, assertEventsContains("Auth Refresh Success"))
|
||||
.checkTimeSkew((driver1, output, events) -> assertThat(toIntExact((long) output),
|
||||
is(
|
||||
both(greaterThan(-40 - TIME_SKEW_TOLERANCE))
|
||||
.and(lessThan(-40 + TIME_SKEW_TOLERANCE))
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneSecondTimeSkewTokenUpdate() {
|
||||
setTimeOffset(1);
|
||||
|
||||
testExecutor.logInAndInit(defaultArguments(), testUser, this::assertInitAuth)
|
||||
.refreshToken(9999, assertEventsContains("Auth Refresh Success"));
|
||||
|
||||
try {
|
||||
// The events element should contain "Auth logout" but we need to wait for it
|
||||
// and text().not().contains() doesn't wait. With KEYCLOAK-4179 it took some time for "Auth Logout" to be present
|
||||
waitUntilElement(eventsArea).text().contains("Auth Logout");
|
||||
|
||||
throw new RuntimeException("The events element shouldn't contain \"Auth Logout\" text");
|
||||
} catch (TimeoutException e) {
|
||||
// OK
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocationHeaderInResponse() {
|
||||
XMLHttpRequest request = XMLHttpRequest.create()
|
||||
.url(authServerContextRootPage + "/auth/admin/realms/" + REALM_NAME + "/users")
|
||||
.method("POST")
|
||||
.content("JSON.stringify(JSON.parse('{\"emailVerified\" : false, \"enabled\" : true, \"username\": \"mhajas\", \"firstName\" :\"First\", \"lastName\":\"Last\",\"email\":\"email@redhat.com\", \"attributes\": {}}'))")
|
||||
.addHeader("Accept", "application/json")
|
||||
.addHeader("Authorization", "Bearer ' + keycloak.token + '")
|
||||
.addHeader("Content-Type", "application/json; charset=UTF-8");
|
||||
|
||||
testExecutor.logInAndInit(defaultArguments(), testUser, this::assertInitAuth)
|
||||
.sendXMLHttpRequest(request, response -> {
|
||||
List<UserRepresentation> users = adminClient.realm(REALM_NAME).users().search("mhajas", 0, 1);
|
||||
assertEquals("There should be created user mhajas", 1, users.size());
|
||||
|
||||
assertThat(((String) response.get("responseHeaders")).toLowerCase(), containsString("location: " + authServerContextRootPage.toString() + "/auth/admin/realms/" + REALM_NAME + "/users/" + users.get(0).getId()));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsSignInRedirectUrl() {
|
||||
testAppUrl = authServerContextRootPage.toString().replace(AUTH_SERVER_HOST, JS_APP_HOST) + JAVASCRIPT_URL + "/index.html?test=bla=bla&super=man";
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
|
||||
JSObjectBuilder arguments = defaultArguments();
|
||||
|
||||
testExecutor.init(arguments, this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(arguments, (driver1, output1, events2) -> {
|
||||
assertTrue(driver1.getCurrentUrl().contains("bla=bla"));
|
||||
assertInitAuth(driver1, output1, events2);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void spaceInRealmNameTest() {
|
||||
try {
|
||||
adminClient.realm(REALM_NAME).update(RealmBuilder.edit(adminClient.realm(REALM_NAME).toRepresentation()).name(SPACE_REALM_NAME).build());
|
||||
|
||||
JSObjectBuilder configuration = JSObjectBuilder.create()
|
||||
.add("url", authServerContextRootPage + "/auth")
|
||||
.add("realm", SPACE_REALM_NAME)
|
||||
.add("clientId", CLIENT_ID);
|
||||
|
||||
testAppUrl = authServerContextRootPage + JAVASCRIPT_ENCODED_SPACE_URL + "/index.html";
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
jsDriverTestRealmLoginPage.setAuthRealm(SPACE_REALM_NAME);
|
||||
|
||||
testExecutor.configure(configuration)
|
||||
.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.configure(configuration)
|
||||
.init(defaultArguments(), this::assertInitAuth);
|
||||
} finally {
|
||||
adminClient.realm(SPACE_REALM_NAME).update(RealmBuilder.edit(adminClient.realm(SPACE_REALM_NAME).toRepresentation()).name(REALM_NAME).build());
|
||||
jsDriverTestRealmLoginPage.setAuthRealm(REALM_NAME);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeWithTokenTest() {
|
||||
oauth.setDriver(jsDriver);
|
||||
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(CLIENT_ID);
|
||||
oauth.redirectUri(testAppUrl);
|
||||
oauth.doLogin(testUser);
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
String token = tokenResponse.getAccessToken();
|
||||
String refreshToken = tokenResponse.getRefreshToken();
|
||||
|
||||
testExecutor.init(JSObjectBuilder.create()
|
||||
.add("token", token)
|
||||
.add("refreshToken", refreshToken)
|
||||
, (driver1, output, events) -> {
|
||||
assertInitAuth(driver1, output, events);
|
||||
if (SuiteContext.BROWSER_STRICT_COOKIES) {
|
||||
// iframe is unsupported so a token refresh had to be performed
|
||||
assertEventsContains("Auth Refresh Success").validate(driver1, output, events);
|
||||
}
|
||||
else {
|
||||
assertEventsDoesntContain("Auth Refresh Success").validate(driver1, output, events);
|
||||
}
|
||||
})
|
||||
.refreshToken(9999, assertEventsContains("Auth Refresh Success"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void initializeWithTimeSkew() {
|
||||
oauth.setDriver(jsDriver); // Oauth need to login with jsDriver
|
||||
|
||||
// Get access token and refresh token to initialize with
|
||||
setTimeOffset(600);
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(CLIENT_ID);
|
||||
oauth.redirectUri(testAppUrl);
|
||||
oauth.doLogin(testUser);
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
String token = tokenResponse.getAccessToken();
|
||||
String refreshToken = tokenResponse.getRefreshToken();
|
||||
|
||||
// Perform test
|
||||
testExecutor.init(JSObjectBuilder.create()
|
||||
.add("token", token)
|
||||
.add("refreshToken", refreshToken)
|
||||
.add("timeSkew", -600)
|
||||
, this::assertInitAuth)
|
||||
.checkTimeSkew((driver1, output, events) -> assertThat((Long) output, is(
|
||||
both(greaterThan(-600L - TIME_SKEW_TOLERANCE))
|
||||
.and(lessThan(-600L + TIME_SKEW_TOLERANCE))
|
||||
)))
|
||||
.refreshToken(9999, assertEventsContains("Auth Refresh Success"))
|
||||
.checkTimeSkew((driver1, output, events) -> assertThat((Long) output, is(
|
||||
both(greaterThan(-600L - TIME_SKEW_TOLERANCE))
|
||||
.and(lessThan(-600L + TIME_SKEW_TOLERANCE))
|
||||
)));
|
||||
}
|
||||
|
||||
@Test
|
||||
// KEYCLOAK-4503
|
||||
public void initializeWithRefreshToken() {
|
||||
|
||||
oauth.setDriver(jsDriver); // Oauth need to login with jsDriver
|
||||
|
||||
oauth.realm(REALM_NAME);
|
||||
oauth.clientId(CLIENT_ID);
|
||||
oauth.redirectUri(testAppUrl);
|
||||
oauth.doLogin(testUser);
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
String token = tokenResponse.getAccessToken();
|
||||
String refreshToken = tokenResponse.getRefreshToken();
|
||||
|
||||
testExecutor.init(JSObjectBuilder.create()
|
||||
.add("refreshToken", refreshToken)
|
||||
, (driver1, output, events) -> {
|
||||
assertInitNotAuth(driver1, output, events);
|
||||
waitUntilElement(events).text().not().contains("Auth Success");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void reentrancyCallbackTest() {
|
||||
testExecutor.logInAndInit(defaultArguments(), testUser, this::assertInitAuth)
|
||||
.executeAsyncScript(
|
||||
"var callback = arguments[arguments.length - 1];" +
|
||||
"keycloak.updateToken(60).then(function () {" +
|
||||
" event(\"First callback\");" +
|
||||
" keycloak.updateToken(60).then(function () {" +
|
||||
" event(\"Second callback\");" +
|
||||
" callback(\"Success\");" +
|
||||
" });" +
|
||||
" }" +
|
||||
");"
|
||||
, (driver1, output, events) -> {
|
||||
waitUntilElement(events).text().contains("First callback");
|
||||
waitUntilElement(events).text().contains("Second callback");
|
||||
waitUntilElement(events).text().not().contains("Auth Logout");
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fragmentInURLTest() {
|
||||
jsDriver.navigate().to(testAppUrl + "#fragmentPart");
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), (driver1, output, events1) -> {
|
||||
assertInitAuth(driver1, output, events1);
|
||||
assertThat(driver1.getCurrentUrl(), containsString("#fragmentPart"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void fragmentInLoginFunction() {
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(JSObjectBuilder.create()
|
||||
.add("redirectUri", testAppUrl + "#fragmentPart")
|
||||
.build(), this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppUrl)
|
||||
.init(defaultArguments(), (driver1, output, events1) -> {
|
||||
assertInitAuth(driver1, output, events1);
|
||||
assertThat(driver1.getCurrentUrl(), containsString("#fragmentPart"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAIAFromJavascriptAdapterSuccess() {
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(JSObjectBuilder.create()
|
||||
.add("action", "UPDATE_PASSWORD")
|
||||
.build(), this::assertOnLoginPage)
|
||||
.loginForm(testUser);
|
||||
|
||||
updatePasswordPage.updatePasswords(USER_PASSWORD, USER_PASSWORD);
|
||||
|
||||
testExecutor.init(defaultArguments(), (driver1, output, events1) -> {
|
||||
assertInitAuth(driver1, output, events1);
|
||||
waitUntilElement(events1).text().contains("AIA status: success");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAIAFromJavascriptAdapterCancelled() {
|
||||
testExecutor.init(defaultArguments(), this::assertInitNotAuth)
|
||||
.login(JSObjectBuilder.create()
|
||||
.add("action", "UPDATE_PASSWORD")
|
||||
.build(), this::assertOnLoginPage)
|
||||
.loginForm(testUser);
|
||||
|
||||
updatePasswordPage.cancel();
|
||||
|
||||
testExecutor.init(defaultArguments(), (driver1, output, events1) -> {
|
||||
assertInitAuth(driver1, output, events1);
|
||||
waitUntilElement(events1).text().contains("AIA status: cancelled");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
// KEYCLOAK-15158
|
||||
public void testInitInHead() {
|
||||
navigateToTestApp(testAppWithInitInHeadUrl);
|
||||
|
||||
testExecutor.validateOutputField(this::assertInitNotAuth)
|
||||
.login(this::assertOnLoginPage)
|
||||
.loginForm(testUser, this::assertOnTestAppWithInitInHeadUrl)
|
||||
.validateOutputField(this::assertInitAuth);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void check3pCookiesMessageCallbackTest() {
|
||||
testExecutor.attachCheck3pCookiesIframeMutationObserver()
|
||||
.init(defaultArguments(), this::assertInitNotAuth);
|
||||
}
|
||||
|
||||
// In case of incorrect/unavailable realm provided in KeycloakConfig,
|
||||
// JavaScript Adapter init() should fail-fast and reject Promise with KeycloakError.
|
||||
@Test
|
||||
public void checkInitWithInvalidRealm() {
|
||||
|
||||
JSObjectBuilder keycloakConfig = JSObjectBuilder.create()
|
||||
.add("url", authServerContextRootPage + "/auth")
|
||||
.add("realm", "invalid-realm-name")
|
||||
.add("clientId", CLIENT_ID);
|
||||
|
||||
JSObjectBuilder initOptions = defaultArguments().add("messageReceiveTimeout", 5000);
|
||||
|
||||
testExecutor
|
||||
.configure(keycloakConfig)
|
||||
.init(initOptions, assertErrorResponse("Timeout when waiting for 3rd party check iframe message."));
|
||||
|
||||
}
|
||||
|
||||
// In case of unavailable Authorization Server due to network or other kind of problems,
|
||||
// JavaScript Adapter init() should fail-fast and reject Promise with KeycloakError.
|
||||
@Test
|
||||
public void checkInitWithUnavailableAuthServer() {
|
||||
|
||||
JSObjectBuilder keycloakConfig = JSObjectBuilder.create()
|
||||
.add("url", "https://localhost:12345/auth")
|
||||
.add("realm", REALM_NAME)
|
||||
.add("clientId", CLIENT_ID);
|
||||
|
||||
JSObjectBuilder initOptions = defaultArguments().add("messageReceiveTimeout", 5000);
|
||||
|
||||
testExecutor
|
||||
.configure(keycloakConfig)
|
||||
.init(initOptions, assertErrorResponse("Timeout when waiting for 3rd party check iframe message."));
|
||||
|
||||
}
|
||||
|
||||
protected void assertAdapterIsLoggedIn(WebDriver driver1, Object output, WebElement events) {
|
||||
assertTrue(testExecutor.isLoggedIn());
|
||||
}
|
||||
|
||||
protected void navigateToTestApp(final String testAppUrl) {
|
||||
jsDriver.navigate().to(testAppUrl);
|
||||
waitUntilElement(outputArea).is().present();
|
||||
assertCurrentUrlStartsWith(testAppUrl, jsDriver);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ feature,4
|
||||
federation,5
|
||||
forms,5
|
||||
i18n,5
|
||||
javascript,5
|
||||
keys,4
|
||||
login,4
|
||||
metrics,4
|
||||
|
||||
Reference in New Issue
Block a user