Remove Keycloak JS from repository (#37057)

Closes #36645

Signed-off-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Jon Koops 2025-02-12 17:31:21 +01:00 committed by GitHub
parent f2d931ba44
commit 3a793916a9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 16 additions and 5298 deletions

1
.github/CODEOWNERS vendored
View File

@ -30,7 +30,6 @@
# Core Clients (@keycloak/core-clients-maintainers)
###################################################################################################
/js/libs/keycloak-js/ @keycloak/core-clients-maintainers
###################################################################################################
# Cloud Native (@keycloak/cloud-native-maintainers)

View File

@ -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/

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -1,3 +0,0 @@
# Keycloak JS
The documentation can be found in the [Keycloak documentation](https://www.keycloak.org/securing-apps/javascript-adapter).

View File

@ -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 RPTs 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;

View File

@ -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;

View File

@ -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

View File

@ -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"
]
}

View File

@ -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>

View File

@ -1,5 +0,0 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true
}
}

View File

@ -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
View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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);

View File

@ -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");
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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
}

View File

@ -1 +0,0 @@
<html><body><script>parent.postMessage(location.href, location.origin)</script></body></html>

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -19,7 +19,6 @@ feature,4
federation,5
forms,5
i18n,5
javascript,5
keys,4
login,4
metrics,4