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