From 905421a292021de1ed44c3c408904311a7cda5de Mon Sep 17 00:00:00 2001 From: Pedro Igor Date: Wed, 22 Jun 2016 14:28:02 -0300 Subject: [PATCH] [KEYCLOAK-3152] - Keycloak Authorization JS Adapter --- adapters/oidc/js/pom.xml | 19 ++ .../js/src/main/resources/keycloak-authz.js | 170 ++++++++++++++++++ .../services/resources/JsResource.java | 89 ++++++--- 3 files changed, 251 insertions(+), 27 deletions(-) create mode 100644 adapters/oidc/js/src/main/resources/keycloak-authz.js diff --git a/adapters/oidc/js/pom.xml b/adapters/oidc/js/pom.xml index 315f4b7fa0..b03ccea898 100755 --- a/adapters/oidc/js/pom.xml +++ b/adapters/oidc/js/pom.xml @@ -58,6 +58,25 @@ minify + + min-authz-js + compile + + utf-8 + ${basedir}/src/main/resources + . + + keycloak-authz.js + + + ${project.build.directory}/classes + . + keycloak-authz.js + + + minify + + diff --git a/adapters/oidc/js/src/main/resources/keycloak-authz.js b/adapters/oidc/js/src/main/resources/keycloak-authz.js new file mode 100644 index 0000000000..7658352ade --- /dev/null +++ b/adapters/oidc/js/src/main/resources/keycloak-authz.js @@ -0,0 +1,170 @@ +/* + * 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) { + var _instance = this; + this.rpt = null; + + this.init = function () { + var request = new XMLHttpRequest(); + + request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/.well-known/uma-configuration'); + request.onreadystatechange = function () { + if (request.readyState == 4) { + if (request.status == 200) { + _instance.config = JSON.parse(request.responseText); + } else { + console.error('Could not obtain configuration from server.'); + } + } + } + + request.send(null); + }; + + /** + * This method enables client applications to better integrate with resource servers protected by a Keycloak + * policy enforcer. + * + * In this case, the resource server will respond with a 401 status code and a WWW-Authenticate header holding the + * necessary information to ask a Keycloak server for authorization data using both UMA and Entitlement protocol, + * depending on how the policy enforcer at the resource server was configured. + */ + this.authorize = function (wwwAuthenticateHeader) { + this.then = function (onGrant, onDeny, onError) { + if (wwwAuthenticateHeader.startsWith('UMA')) { + var params = wwwAuthenticateHeader.split(','); + + for (i = 0; i < params.length; i++) { + var param = params[i].split('='); + + if (param[0] == 'ticket') { + var request = new XMLHttpRequest(); + + request.open('POST', _instance.config.rpt_endpoint, true); + request.setRequestHeader('Content-Type', 'application/json') + 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).rpt; + _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 ticket = param[1].substring(1, param[1].length - 1).trim(); + + request.send(JSON.stringify( + { + ticket: ticket, + rpt: _instance.rpt + } + )); + } + } + } else if (wwwAuthenticateHeader.startsWith('KC_ETT')) { + var params = wwwAuthenticateHeader.substring('KC_ETT'.length).trim().split(','); + var clientId = null; + + for (i = 0; i < params.length; i++) { + var param = params[i].split('='); + + if (param[0] == 'realm') { + clientId = param[1].substring(1, param[1].length - 1).trim(); + } + } + + _instance.entitlement(clientId).then(onGrant, onDeny, onError); + } + }; + + /** + * Obtains all entitlements from a Keycloak Server based on a give resourceServerId. + */ + this.entitlement = function (resourceSeververId) { + this.then = function (onGrant, onDeny, onError) { + var request = new XMLHttpRequest(); + + request.open('GET', keycloak.authServerUrl + '/realms/' + keycloak.realm + '/authz/entitlement/' + resourceSeververId, true); + 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).rpt; + _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.'); + } + } + } + }; + + request.send(null); + }; + + return this; + }; + + return this; + }; + + this.init(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 ); \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/JsResource.java b/services/src/main/java/org/keycloak/services/resources/JsResource.java index 32161a4827..c74abf0e52 100755 --- a/services/src/main/java/org/keycloak/services/resources/JsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/JsResource.java @@ -44,35 +44,82 @@ public class JsResource { @GET @Path("/keycloak.js") @Produces("text/javascript") - public Response getJs() { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("keycloak.js"); - if (inputStream != null) { - CacheControl cacheControl = new CacheControl(); - cacheControl.setNoTransform(false); - cacheControl.setMaxAge(Config.scope("theme").getInt("staticMaxAge", -1)); - - return Response.ok(inputStream).type("text/javascript").cacheControl(cacheControl).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } + public Response getKeycloakJs() { + return getJs("keycloak.js"); } @GET @Path("/{version}/keycloak.js") @Produces("text/javascript") - public Response getJsWithVersion(@PathParam("version") String version) { + public Response getKeycloakJsWithVersion(@PathParam("version") String version) { if (!version.equals(Version.RESOURCES_VERSION)) { return Response.status(Response.Status.NOT_FOUND).build(); } - return getJs(); + return getKeycloakJs(); } @GET @Path("/keycloak.min.js") @Produces("text/javascript") - public Response getMinJs() { - InputStream inputStream = getClass().getClassLoader().getResourceAsStream("keycloak.min.js"); + public Response getKeycloakMinJs() { + return getJs("keycloak.min.js"); + } + + @GET + @Path("/{version}/keycloak.min.js") + @Produces("text/javascript") + public Response getKeycloakMinJsWithVersion(@PathParam("version") String version) { + if (!version.equals(Version.RESOURCES_VERSION)) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return getKeycloakMinJs(); + } + + /** + * Get keycloak-authz.js file for javascript clients + * + * @return + */ + @GET + @Path("/keycloak-authz.js") + @Produces("text/javascript") + public Response getKeycloakAuthzJs() { + return getJs("keycloak-authz.js"); + } + + @GET + @Path("/{version}/keycloak-authz.js") + @Produces("text/javascript") + public Response getKeycloakAuthzJsWithVersion(@PathParam("version") String version) { + if (!version.equals(Version.RESOURCES_VERSION)) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return getKeycloakAuthzJs(); + } + + @GET + @Path("/keycloak-authz.min.js") + @Produces("text/javascript") + public Response getKeycloakAuthzMinJs() { + return getJs("keycloak-authz.min.js"); + } + + @GET + @Path("/{version}/keycloak-authz.min.js") + @Produces("text/javascript") + public Response getKeycloakAuthzMinJsWithVersion(@PathParam("version") String version) { + if (!version.equals(Version.RESOURCES_VERSION)) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + return getKeycloakAuthzMinJs(); + } + + private Response getJs(String name) { + InputStream inputStream = getClass().getClassLoader().getResourceAsStream(name); if (inputStream != null) { CacheControl cacheControl = new CacheControl(); cacheControl.setNoTransform(false); @@ -83,16 +130,4 @@ public class JsResource { return Response.status(Response.Status.NOT_FOUND).build(); } } - - @GET - @Path("/{version}/keycloak.min.js") - @Produces("text/javascript") - public Response getMinJsWithVersion(@PathParam("version") String version) { - if (!version.equals(Version.RESOURCES_VERSION)) { - return Response.status(Response.Status.NOT_FOUND).build(); - } - - return getMinJs(); - } - }