KEYCLOAK-14817 Allow JS adapter to be bundled as ES module (#9351)

This commit is contained in:
Jon Koops 2022-01-13 08:28:30 +01:00 committed by GitHub
parent 8ea09d3816
commit dea123169f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 3333 additions and 2062 deletions

2
adapters/oidc/js/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node
node_modules

1237
adapters/oidc/js/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,22 @@
{
"name": "keycloak-js-adapter",
"private": true,
"scripts": {
"build": "rollup --config --configPlugin typescript"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-inject": "^4.0.3",
"@rollup/plugin-node-resolve": "^13.1.1",
"@rollup/plugin-typescript": "^8.3.0",
"@types/node": "^17.0.5",
"rollup": "^2.62.0",
"rollup-plugin-terser": "^7.0.2",
"typescript": "^4.5.4"
},
"dependencies": {
"base64-js": "^1.5.1",
"es6-promise": "^4.2.8",
"js-sha256": "^0.9.0"
}
}

View file

@ -16,75 +16,50 @@
~ limitations under the License. ~ limitations under the License.
--> -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <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">
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent> <parent>
<artifactId>keycloak-parent</artifactId> <artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<version>17.0.0-SNAPSHOT</version> <version>17.0.0-SNAPSHOT</version>
<relativePath>../../../pom.xml</relativePath> <relativePath>../../../pom.xml</relativePath>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-js-adapter</artifactId> <artifactId>keycloak-js-adapter</artifactId>
<name>Keycloak JS Integration</name> <name>Keycloak JS Integration</name>
<description />
<dependencies>
</dependencies>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>com.samaxes.maven</groupId> <groupId>com.github.eirslett</groupId>
<artifactId>minify-maven-plugin</artifactId> <artifactId>frontend-maven-plugin</artifactId>
<configuration> <executions>
<jsEngine>CLOSURE</jsEngine> <execution>
<closureLanguageIn>ECMASCRIPT5</closureLanguageIn> <id>install node and npm</id>
<closureCreateSourceMap>true</closureCreateSourceMap> <goals>
</configuration> <goal>install-node-and-npm</goal>
<executions> </goals>
<execution> </execution>
<id>min-js</id> <execution>
<phase>compile</phase> <id>npm install</id>
<configuration> <goals>
<charset>utf-8</charset> <goal>npm</goal>
<webappSourceDir>${basedir}/src/main/resources</webappSourceDir> </goals>
<jsSourceDir>.</jsSourceDir> </execution>
<jsSourceFiles> <execution>
<jsSourceFile>keycloak.js</jsSourceFile> <id>npm run build</id>
</jsSourceFiles> <goals>
<goal>npm</goal>
<webappTargetDir>${project.build.directory}/classes</webappTargetDir> </goals>
<jsTargetDir>.</jsTargetDir> <configuration>
<jsFinalFile>keycloak.js</jsFinalFile> <arguments>run build</arguments>
</configuration> </configuration>
<goals> </execution>
<goal>minify</goal> </executions>
</goals> <configuration>
</execution> <nodeVersion>${node.version}</nodeVersion>
<execution> </configuration>
<id>min-authz-js</id> </plugin>
<phase>compile</phase>
<configuration>
<charset>utf-8</charset>
<webappSourceDir>${basedir}/src/main/resources</webappSourceDir>
<jsSourceDir>.</jsSourceDir>
<jsSourceFiles>
<jsSourceFile>keycloak-authz.js</jsSourceFile>
</jsSourceFiles>
<webappTargetDir>${project.build.directory}/classes</webappTargetDir>
<jsTargetDir>.</jsTargetDir>
<jsFinalFile>keycloak-authz.js</jsFinalFile>
</configuration>
<goals>
<goal>minify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins> </plugins>
</build> </build>
</project> </project>

View file

@ -0,0 +1,81 @@
import commonjs from "@rollup/plugin-commonjs";
import inject from "@rollup/plugin-inject";
import { nodeResolve } from "@rollup/plugin-node-resolve";
import path from "node:path";
import type { OutputOptions, RollupOptions } from "rollup";
import { defineConfig } from "rollup";
import { terser } from "rollup-plugin-terser";
interface DefineOptionsArgs {
file: string;
name: string;
amdId: string;
}
function defineOptions({
file,
name,
amdId,
}: DefineOptionsArgs): RollupOptions[] {
const sourceDir = "src/main/js";
const targetDir = "target/classes";
const commonOptions: RollupOptions = {
input: path.join(sourceDir, `${file}.js`),
plugins: [commonjs(), nodeResolve()],
};
const umdOutput: OutputOptions = {
format: "umd",
name,
amd: { id: amdId },
};
return [
// Modern ES module variant, with externalized dependencies.
{
...commonOptions,
output: [
{
file: path.join(targetDir, `${file}.mjs`),
},
],
external: ["base64-js", "js-sha256"],
},
// Legacy Universal Module Definition, or “UMD”, with inlined dependencies.
{
...commonOptions,
output: [
{
...umdOutput,
file: path.join(targetDir, `${file}.js`),
},
{
...umdOutput,
file: path.join(targetDir, `${file}.min.js`),
sourcemap: true,
sourcemapExcludeSources: true,
plugins: [terser()],
},
],
plugins: [
...commonOptions.plugins,
inject({
Promise: ["es6-promise/dist/es6-promise.min.js", "Promise"],
}),
],
},
];
}
export default defineConfig([
...defineOptions({
file: "keycloak",
name: "Keycloak",
amdId: "keycloak",
}),
...defineOptions({
file: "keycloak-authz",
name: "KeycloakAuthorization",
amdId: "keycloak-authorization",
}),
]);

View file

@ -0,0 +1,221 @@
/*
* 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;
var resolve = function () {};
var reject = function () {};
// detects if browser supports promises
if (typeof Promise !== "undefined" && Promise.toString().indexOf("[native code]") !== -1) {
this.ready = new Promise(function (res, rej) {
resolve = res;
reject = rej;
});
}
this.init = function () {
var request = new XMLHttpRequest();
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma2-configuration');
request.onreadystatechange = function () {
if (request.readyState == 4) {
if (request.status == 200) {
_instance.config = JSON.parse(request.responseText);
resolve();
} else {
console.error('Could not obtain configuration from server.');
reject();
}
}
}
request.send(null);
};
/**
* 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 = function (onGrant, onDeny, onError) {
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 = function (onGrant, onDeny, onError) {
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 (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;
};
this.init(this);
return this;
};
export default KeycloakAuthorization;

File diff suppressed because it is too large Load diff

View file

@ -1,232 +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.
*
*/
(function( window, undefined ) {
var KeycloakAuthorization = function (keycloak, options) {
var _instance = this;
this.rpt = null;
var resolve = function () {};
var reject = function () {};
// detects if browser supports promises
if (typeof Promise !== "undefined" && Promise.toString().indexOf("[native code]") !== -1) {
this.ready = new Promise(function (res, rej) {
resolve = res;
reject = rej;
});
}
this.init = function () {
var request = new XMLHttpRequest();
request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma2-configuration');
request.onreadystatechange = function () {
if (request.readyState == 4) {
if (request.status == 200) {
_instance.config = JSON.parse(request.responseText);
resolve();
} else {
console.error('Could not obtain configuration from server.');
reject();
}
}
}
request.send(null);
};
/**
* 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 = function (onGrant, onDeny, onError) {
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 = function (onGrant, onDeny, onError) {
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 (i = 0; i < permissions.length; i++) {
var resource = permissions[i];
var permission = resource.id;
if (resource.scopes && resource.scopes.length > 0) {
permission += "#";
for (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;
};
this.init(this);
return this;
};
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
module.exports = KeycloakAuthorization;
} else {
window.KeycloakAuthorization = KeycloakAuthorization;
if ( typeof define === "function" && define.amd ) {
define( "keycloak-authorization", [], function () { return KeycloakAuthorization; } );
}
}
})( window );

File diff suppressed because one or more lines are too long

View file

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

View file

@ -37,6 +37,7 @@
<outputDirectory>dist/</outputDirectory> <outputDirectory>dist/</outputDirectory>
<includes> <includes>
<include>**/*.js</include> <include>**/*.js</include>
<include>**/*.mjs</include>
<include>**/*.map</include> <include>**/*.map</include>
<include>**/*.d.ts</include> <include>**/*.d.ts</include>
</includes> </includes>

View file

@ -53,7 +53,7 @@
<includeGroupIds>org.keycloak</includeGroupIds> <includeGroupIds>org.keycloak</includeGroupIds>
<includeArtifactIds>keycloak-js-adapter</includeArtifactIds> <includeArtifactIds>keycloak-js-adapter</includeArtifactIds>
<outputDirectory>${project.build.directory}/unpacked/js-adapter</outputDirectory> <outputDirectory>${project.build.directory}/unpacked/js-adapter</outputDirectory>
<includes>*.js,*.map,*.d.ts</includes> <includes>*.js,*.mjs,*.map,*.d.ts</includes>
<excludes>**/welcome-content/*</excludes> <excludes>**/welcome-content/*</excludes>
</configuration> </configuration>
</execution> </execution>

View file

@ -3,6 +3,7 @@
"version": "${project.version}", "version": "${project.version}",
"description": "Keycloak Adapter", "description": "Keycloak Adapter",
"main": "dist/keycloak.js", "main": "dist/keycloak.js",
"module": "dist/keycloak.mjs",
"typings": "dist/keycloak.d.ts", "typings": "dist/keycloak.d.ts",
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1" "test": "echo \"Error: no test specified\" && exit 1"
@ -22,7 +23,7 @@
"authentication" "authentication"
], ],
"dependencies": { "dependencies": {
"base64-js": "1.3.1", "base64-js": "^1.5.1",
"js-sha256": "0.9.0" "js-sha256": "^0.9.0"
} }
} }

View file

@ -30,6 +30,7 @@
<outputDirectory></outputDirectory> <outputDirectory></outputDirectory>
<includes> <includes>
<include>**/*.js</include> <include>**/*.js</include>
<include>**/*.mjs</include>
<include>**/*.map</include> <include>**/*.map</include>
<include>**/*.d.ts</include> <include>**/*.d.ts</include>
<include>**/*.html</include> <include>**/*.html</include>

View file

@ -53,7 +53,7 @@
<includeGroupIds>org.keycloak</includeGroupIds> <includeGroupIds>org.keycloak</includeGroupIds>
<includeArtifactIds>keycloak-js-adapter</includeArtifactIds> <includeArtifactIds>keycloak-js-adapter</includeArtifactIds>
<outputDirectory>${project.build.directory}/unpacked/js-adapter</outputDirectory> <outputDirectory>${project.build.directory}/unpacked/js-adapter</outputDirectory>
<includes>*.js,*.map,*.d.ts</includes> <includes>*.js,*.mjs,*.map,*.d.ts</includes>
<excludes>**/welcome-content/*</excludes> <excludes>**/welcome-content/*</excludes>
</configuration> </configuration>
</execution> </execution>

View file

@ -212,6 +212,8 @@
<!-- Version suffix that is appended to directories. Default is the maven GAV version but this can be edited to use a short form version --> <!-- Version suffix that is appended to directories. Default is the maven GAV version but this can be edited to use a short form version -->
<server.output.dir.version>${project.version}</server.output.dir.version> <server.output.dir.version>${project.version}</server.output.dir.version>
<!-- Frontend -->
<node.version>v16.13.1</node.version>
</properties> </properties>
<url>http://keycloak.org</url> <url>http://keycloak.org</url>

View file

@ -108,7 +108,7 @@ public class ThemeResourceProviderTest extends AbstractTestRealmKeycloakTest {
}, Boolean.class); }, Boolean.class);
assertEncoded(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/resources/" + resourcesVersion + "/welcome/keycloak/css/welcome.css", "body {"); assertEncoded(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/resources/" + resourcesVersion + "/welcome/keycloak/css/welcome.css", "body {");
assertEncoded(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/js/keycloak.js", "function(root, factory)"); assertEncoded(suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/js/keycloak.js", "function Keycloak (config)");
// Check no files exists inside "/tmp" directory. We need to skip this test in the rare case when there are thombstone files created by different user // Check no files exists inside "/tmp" directory. We need to skip this test in the rare case when there are thombstone files created by different user
if (filesNotExistsInTmp) { if (filesNotExistsInTmp) {

View file

@ -71,7 +71,7 @@
</execution> </execution>
</executions> </executions>
<configuration> <configuration>
<nodeVersion>v16.13.0</nodeVersion> <nodeVersion>${node.version}</nodeVersion>
<installDirectory>${project.basedir}</installDirectory> <installDirectory>${project.basedir}</installDirectory>
</configuration> </configuration>
</plugin> </plugin>