Merge pull request #2957 from pedroigor/authz-changes
Changes to authz examples and some minor improvements
This commit is contained in:
commit
2e2f34d94e
73 changed files with 800 additions and 382 deletions
|
@ -129,7 +129,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||
Set<String> allowedScopes = permission.getScopes();
|
||||
|
||||
if (permission.getResourceSetId() != null) {
|
||||
if (permission.getResourceSetId().equals(actualPathConfig.getId())) {
|
||||
if (isResourcePermission(actualPathConfig, permission)) {
|
||||
if (((allowedScopes == null || allowedScopes.isEmpty()) && requiredScopes.isEmpty()) || allowedScopes.containsAll(requiredScopes)) {
|
||||
LOGGER.debugf("Authorization GRANTED for path [%s]. Permissions [%s].", actualPathConfig, permissions);
|
||||
if (request.getMethod().equalsIgnoreCase("DELETE") && actualPathConfig.isInstance()) {
|
||||
|
@ -211,6 +211,7 @@ public abstract class AbstractPolicyEnforcer {
|
|||
config.setScopes(originalConfig.getScopes());
|
||||
config.setMethods(originalConfig.getMethods());
|
||||
config.setInstance(true);
|
||||
config.setParentConfig(originalConfig);
|
||||
|
||||
this.paths.add(config);
|
||||
|
||||
|
@ -240,4 +241,16 @@ public abstract class AbstractPolicyEnforcer {
|
|||
private AuthorizationContext createAuthorizationContext(AccessToken accessToken) {
|
||||
return new AuthorizationContext(accessToken, this.paths);
|
||||
}
|
||||
|
||||
private boolean isResourcePermission(PathConfig actualPathConfig, Permission permission) {
|
||||
// first we try a match using resource id
|
||||
boolean resourceMatch = permission.getResourceSetId().equals(actualPathConfig.getId());
|
||||
|
||||
// as a fallback, check if the current path is an instance and if so, check if parent's id matches the permission
|
||||
if (!resourceMatch && actualPathConfig.isInstance()) {
|
||||
resourceMatch = permission.getResourceSetId().equals(actualPathConfig.getParentConfig().getId());
|
||||
}
|
||||
|
||||
return resourceMatch;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,25 @@
|
|||
<goal>minify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>min-authz-js</id>
|
||||
<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>
|
||||
|
|
170
adapters/oidc/js/src/main/resources/keycloak-authz.js
Normal file
170
adapters/oidc/js/src/main/resources/keycloak-authz.js
Normal file
|
@ -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 );
|
|
@ -89,7 +89,7 @@ public class HttpMethod<R> {
|
|||
int statusCode = statusLine.getStatusCode();
|
||||
|
||||
if (statusCode < 200 || statusCode >= 300) {
|
||||
throw new HttpResponseException(statusCode, statusLine.getReasonPhrase(), bytes);
|
||||
throw new HttpResponseException("Unexpected response from server: " + statusCode + " / " + statusLine.getReasonPhrase(), statusCode, statusLine.getReasonPhrase(), bytes);
|
||||
}
|
||||
|
||||
if (bytes == null) {
|
||||
|
|
|
@ -26,7 +26,8 @@ public class HttpResponseException extends RuntimeException {
|
|||
private final String reasonPhrase;
|
||||
private final byte[] bytes;
|
||||
|
||||
public HttpResponseException(int statusCode, String reasonPhrase, byte[] bytes) {
|
||||
public HttpResponseException(String message, int statusCode, String reasonPhrase, byte[] bytes) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.reasonPhrase = reasonPhrase;
|
||||
this.bytes = bytes;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
*/
|
||||
package org.keycloak.representations.adapters.config;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -99,6 +100,9 @@ public class PolicyEnforcerConfig {
|
|||
private String id;
|
||||
private boolean instance;
|
||||
|
||||
@JsonIgnore
|
||||
private PathConfig parentConfig;
|
||||
|
||||
public String getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
@ -169,6 +173,14 @@ public class PolicyEnforcerConfig {
|
|||
public void setInstance(boolean instance) {
|
||||
this.instance = instance;
|
||||
}
|
||||
|
||||
public void setParentConfig(PathConfig parentConfig) {
|
||||
this.parentConfig = parentConfig;
|
||||
}
|
||||
|
||||
public PathConfig getParentConfig() {
|
||||
return parentConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MethodConfig {
|
||||
|
|
47
examples/authz/hello-world-authz-service/README.md
Normal file
47
examples/authz/hello-world-authz-service/README.md
Normal file
|
@ -0,0 +1,47 @@
|
|||
# About the Example Application
|
||||
|
||||
This is a simple application to get you started with Keycloak Authorization Services.
|
||||
|
||||
It provides a single page application which is protected by a policy enforcer that decides whether an user can access
|
||||
that page or not based on the permissions obtained from a Keycloak Server.
|
||||
|
||||
## Create the Example Realm and a Resource Server
|
||||
|
||||
Considering that your Keycloak Server is up and running, log in to the Keycloak Administration Console.
|
||||
|
||||
Now, create a new realm based on the following configuration file:
|
||||
|
||||
examples/authz/hello-world-authz-service/hello-world-authz-realm.json
|
||||
|
||||
That will import a pre-configured realm with everything you need to run this example. For more details about how to import a realm
|
||||
into Keycloak, check the Keycloak's reference documentation.
|
||||
|
||||
After importing that file, you'll have a new realm called ``hello-world-authz``.
|
||||
|
||||
Now, let's import another configuration using the Administration Console in order to configure the client application ``hello-world-authz-service`` as a resource server with all resources, scopes, permissions and policies.
|
||||
|
||||
Click on ``Clients`` on the left side menu. Click on the ``hello-world-authz-service`` on the client listing page. This will
|
||||
open the ``Client Details`` page. Once there, click on the `Authorization` tab.
|
||||
|
||||
Click on the ``Select file`` button, which means you want to import a resource server configuration. Now select the file that is located at:
|
||||
|
||||
examples/authz/hello-world-authz-service/hello-world-authz-service.json
|
||||
|
||||
Now click ``Upload`` and the resource server will be updated accordingly.
|
||||
|
||||
## Deploy and Run the Example Application
|
||||
|
||||
To deploy the example application, follow these steps:
|
||||
|
||||
cd examples/authz/hello-world-authz-service
|
||||
mvn clean package wildfly:deploy
|
||||
|
||||
Now, try to access the client application using the following URL:
|
||||
|
||||
http://localhost:8080/hello-world-authz-service
|
||||
|
||||
If everything is correct, you will be redirect to Keycloak login page. You can login to the application with the following credentials:
|
||||
|
||||
* username: jdoe / password: jdoe
|
||||
* username: alice / password: alice
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"realm" : "hello-world-authz",
|
||||
"enabled" : true,
|
||||
"privateKey" : "MIIEpQIBAAKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQABAoIBAAwa4wVnKBOIS6srmYPfBTDNsTBBCEjxiYEErmn7JhoWxQ1DCPUxyxU6F177/q9Idqoj1FFOCtEO9P6/9+ym470HQmEQkR2Xxd1d3HOZy9oKuCro3ZbTDkVxY0JnlyxZz4MihGFxDH2e4MArfHy0sAgYbdIU+x2pWKGWSMzDd/TMSOExhc/sIQAg6ljbPCLLXCPQFAncoHRyGPrkRZs6UTZi5SJuCglVa2/3G+0drDdPuA83/mwsZfIBqQgbGbFgtq5T5C6CKMkPOQ42Rcclm7kEr6riTkJRo23EO1iOJVpxzI0tbxZsJAsW7zeqv0wWRyUgVfQAje6OdsNexp5aCtECgYEA6nMHCQ9xXvufCyzpIbYGxdAGqH6m1AR5gXerHqRiGNx+8UUt/E9cy/HTOhmZDK/eC4BT9tImeF01l1oSU/+wGKfux0SeAQchBhhq8GD6jmrtgczKAfZHp0Zrht7o9qu9KE7ZNWRmY1foJN9yNYmzY6qqHEy+zNo9amcqT7UZKO8CgYEA35sp9fMpMqkJE+NEJ9Ph/t2081BEkC0DYIuETZRSi+Ek5AliWTyEkg+oisTbWzi6fMQHS7W+M1SQP6djksLQNPP+353DKgup5gtKS+K/y2xNd7fSsNmkjW1bdJJpID7WzwwmwdahHxpcnFFuEXi5FkG3Vqmtd3cD0TYL33JlRy0CgYEA0+a3eybsDy9Zpp4m8IM3R98nxW8DlimdMLlafs2QpGvWiHdAgwWwF90wTxkHzgG+raKFQVbb0npcj7mnSyiUnxRZqt2H+eHZpUq4jR76F3LpzCGui2tvg+8QDMy4vwqmYyIxDCL8r9mqRnl3HpChBPoh2oY7BahTTjKEeZpzbR0CgYEAoNnVjX+mGzNNvGi4Fo5s/BIwoPcU20IGM+Uo/0W7O7Rx/Thi7x6BnzB0ZZ7GzRA51paNSQEsGXCzc5bOIjzR2cXLisDKK+zIAxwMDhrHLWZzM7OgdGeb38DTEUBhLzkE/VwYZUgoD1+/TxOkwhy9yCzt3gGhL1cF//GJCOwZvuECgYEAgsO4rdYScgCpsyePnHsFk+YtqtdORnmttF3JFcL3w2QneXuRwg2uW2Kfz8CVphrR9eOU0tiw38w6QTHIVeyRY8qqlHtiXj6dEYz7frh/k4hI29HwFx43rRpnAnN8kBEJYBYdbjaQ35Wsqkfu1tvHJ+6fxSwvQu/TVdGp0OfilAY=",
|
||||
"publicKey" : "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQAB",
|
||||
"certificate" : "MIICsTCCAZkCBgFVETX4AzANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFIZWxsbyBXb3JsZCBBdXRoWjAeFw0xNjA2MDIxMzAxMzdaFw0yNjA2MDIxMzAzMTdaMBwxGjAYBgNVBAMMEUhlbGxvIFdvcmxkIEF1dGhaMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQANm5gIT/c50lwjawM686gNXpppLA928WsCOn9NIIWjSKekP8Bf9S73kf7vWcsEppm5B8rRyRxolXmzwghv74L7uVDg8Injjgj+XbPVQP+cJqWpSaMZHF7UfWe0/4M945Xcbmsl5q+m9PmrPG0AaaZhqXHcp4ehB1H+awyRqiERpJUuwZNycw2+2kjDADpsFf8hZVUd1F6ReYyOkqUyUjbL+jYTC7ZBNa7Ok+w6HCXWgkgVATAgQXJRM3w14IOc5MH/vfMCrCl/eNQLbjGl9y7u8PKwh3MXHDO2OLqtg6hOTSrOGUPJZGmGtUAl+2/R7FzoWkML/BNe2hjsL6UJwg91",
|
||||
"requiredCredentials" : [ "password" ],
|
||||
"users" :
|
||||
[
|
||||
{
|
||||
"username" : "alice",
|
||||
"enabled" : true,
|
||||
"credentials" : [ {
|
||||
"type" : "password",
|
||||
"value" : "alice"
|
||||
} ],
|
||||
"realmRoles" : ["uma_authorization"]
|
||||
},
|
||||
{
|
||||
"username" : "jdoe",
|
||||
"enabled" : true,
|
||||
"credentials" : [ {
|
||||
"type" : "password",
|
||||
"value" : "jdoe"
|
||||
} ],
|
||||
"realmRoles" : ["uma_authorization"]
|
||||
},
|
||||
{
|
||||
"username" : "service-account-hello-world-authz-service",
|
||||
"enabled" : true,
|
||||
"serviceAccountClientId" : "hello-world-authz-service",
|
||||
"clientRoles": {
|
||||
"hello-world-authz-service" : ["uma_protection"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"clients" : [
|
||||
{
|
||||
"clientId" : "hello-world-authz-service",
|
||||
"secret" : "secret",
|
||||
"authorizationServicesEnabled" : true,
|
||||
"enabled" : true,
|
||||
"redirectUris" : [ "http://localhost:8080/hello-world-authz-service/*" ],
|
||||
"baseUrl": "http://localhost:8080/hello-world-authz-service",
|
||||
"adminUrl": "http://localhost:8080/hello-world-authz-service",
|
||||
"directAccessGrantsEnabled" : true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"resources": [
|
||||
{
|
||||
"name": "Default Resource",
|
||||
"uri": "/*",
|
||||
"type": "urn:hello-world-authz-service:resources:default"
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
{
|
||||
"name": "Only From Realm Policy",
|
||||
"description": "A policy that grants access only for users within this realm",
|
||||
"type": "js",
|
||||
"config": {
|
||||
"applyPolicies": "[]",
|
||||
"code": "var context = $evaluation.getContext();\n\n// using attributes from the evaluation context to obtain the realm\nvar contextAttributes = context.getAttributes();\nvar realmName = contextAttributes.getValue('kc.realm.name').asString(0);\n\n// using attributes from the identity to obtain the issuer\nvar identity = context.getIdentity();\nvar identityAttributes = identity.getAttributes();\nvar issuer = identityAttributes.getValue('iss').asString(0);\n\n// only users from the realm have access granted \nif (issuer.endsWith(realmName)) {\n $evaluation.grant();\n}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Default Permission",
|
||||
"description": "A permission that applies to the default resource type",
|
||||
"type": "resource",
|
||||
"config": {
|
||||
"defaultResourceType": "urn:hello-world-authz-service:resources:default",
|
||||
"default": "true",
|
||||
"applyPolicies": "[\"Only From Realm Policy\"]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
{
|
||||
"realm": "hello-world-authz",
|
||||
"realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwmm2Nso+rUOYUYc4hO67LSf4s0pAKcqUbWWycS3fcz6Q4jg/SsBbIBJJXOMVR9GqwyTCVTH5s8Rb0+0pA+UrbZfMG2XIDnJoaGfJj9DvJwQkD+vzTvaS5q0ilP0tPlbusI5pyMi9xx+cjJBOvKR2GxjhcKrgb21lpmGcA1F1CPO3y/DT8GzTKg+9/nPKt1dKEUD7P5Uy5N7d8zz1fuOSLb5G267T1fKJvi6am8kCgM+agFVQ23j7w/aJ7T1EHUCZdaJ+aSODSYl8dM4RFNTjda0KMHHXqMMvd2+g8lZ0lAfstHywqZtCcHc9ULClVvQmQyXovn2qTktHAcD6BHTAgQIDAQAB",
|
||||
"realm-public-key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzMhNM9HXNQWhVf1m64zS67SIyQjj+tV5GR+MqlRTWDXdo8GAWHd+alY1urRhfRoqMy4F499+8wh2REKFykNt0ng6s6wWnEaKDboS3SAUV6lybcOAkwIOCtCZj1ItddKG3m64fzxDDQrcpkbiAvw3S8KJ4UJK+pyh9iX01duSDtM/HhPawsPdY8JSMfuo1IxQ2Vxw+8RKwbbdUeew6cyYGYAeFYwA66mlM3otB0RBHh4bjwg8297+2g53TdwM2rbCHRbrorMQD3031OTyFSp7lXCtoMLWRfAFnOP/2yZWZMXbiJheC0R3sLbU7Ef0/cUbYyk4Ckfq6pcYDR+VZBF7AwIDAQAB",
|
||||
"auth-server-url": "http://localhost:8080/auth",
|
||||
"ssl-required": "external",
|
||||
"resource": "hello-world-authz-service",
|
||||
"credentials": {
|
||||
"secret": "a7672d93-ea27-44a3-baa6-ba3536609067"
|
||||
"secret": "secret"
|
||||
},
|
||||
"policy-enforcer": {
|
||||
"on-deny-redirect-to" : "/hello-world-authz-service/error.jsp"
|
||||
|
|
|
@ -12,16 +12,18 @@
|
|||
"enabled" : true,
|
||||
"credentials" : [ {
|
||||
"type" : "password",
|
||||
"value" : "password"
|
||||
} ]
|
||||
"value" : "alice"
|
||||
} ],
|
||||
"realmRoles" : ["uma_authorization"]
|
||||
},
|
||||
{
|
||||
"username" : "jdoe",
|
||||
"enabled" : true,
|
||||
"credentials" : [ {
|
||||
"type" : "password",
|
||||
"value" : "password"
|
||||
} ]
|
||||
"value" : "jdoe"
|
||||
} ],
|
||||
"realmRoles" : ["uma_authorization"]
|
||||
},
|
||||
{
|
||||
"username" : "service-account-hello-world-authz-service",
|
||||
|
@ -38,7 +40,9 @@
|
|||
"secret" : "secret",
|
||||
"authorizationServicesEnabled" : true,
|
||||
"enabled" : true,
|
||||
"redirectUris" : [ "http://localhost:8080/hello-world-authz-service" ],
|
||||
"redirectUris" : [ "http://localhost:8080/hello-world-authz-service/*" ],
|
||||
"baseUrl": "http://localhost:8080/hello-world-authz-service",
|
||||
"adminUrl": "http://localhost:8080/hello-world-authz-service",
|
||||
"directAccessGrantsEnabled" : true
|
||||
}
|
||||
]
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
{
|
||||
"resources": [
|
||||
{
|
||||
"name": "Hello World Resource"
|
||||
"name": "Default Resource",
|
||||
"uri": "/*",
|
||||
"type": "urn:hello-world-authz-service:resources:default"
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
{
|
||||
"name": "Only Special Users Policy",
|
||||
"type": "user",
|
||||
"logic": "POSITIVE",
|
||||
"name": "Only From Realm Policy",
|
||||
"description": "A policy that grants access only for users within this realm",
|
||||
"type": "js",
|
||||
"config": {
|
||||
"users": "[\"alice\"]"
|
||||
"applyPolicies": "[]",
|
||||
"code": "var context = $evaluation.getContext();\n\n// using attributes from the evaluation context to obtain the realm\nvar contextAttributes = context.getAttributes();\nvar realmName = contextAttributes.getValue('kc.realm.name').asString(0);\n\n// using attributes from the identity to obtain the issuer\nvar identity = context.getIdentity();\nvar identityAttributes = identity.getAttributes();\nvar issuer = identityAttributes.getValue('iss').asString(0);\n\n// only users from the realm have access granted \nif (issuer.endsWith(realmName)) {\n $evaluation.grant();\n}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Hello World Resource Permission",
|
||||
"name": "Default Permission",
|
||||
"description": "A permission that applies to the default resource type",
|
||||
"type": "resource",
|
||||
"config": {
|
||||
"resources": "[\"Hello World Resource\"]",
|
||||
"applyPolicies": "[\"Only Special Users Policy\"]"
|
||||
"defaultResourceType": "urn:hello-world-authz-service:resources:default",
|
||||
"default": "true",
|
||||
"applyPolicies": "[\"Only From Realm Policy\"]"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -49,7 +49,7 @@ public class AuthorizationClientExample {
|
|||
// query the server for a resource with a given name
|
||||
Set<String> resourceId = authzClient.protection()
|
||||
.resource()
|
||||
.findByFilter("name=Hello World Resource");
|
||||
.findByFilter("name=Default Resource");
|
||||
|
||||
// obtian a Entitlement API Token in order to get access to the Entitlement API.
|
||||
// this token is just an access token issued to a client on behalf of an user with a scope kc_entitlement
|
||||
|
@ -119,7 +119,7 @@ public class AuthorizationClientExample {
|
|||
EntitlementRequest request = new EntitlementRequest();
|
||||
PermissionRequest permission = new PermissionRequest();
|
||||
|
||||
permission.setResourceSetName("Hello World Resource");
|
||||
permission.setResourceSetName("Default Resource");
|
||||
|
||||
request.addPermission(permission);
|
||||
|
||||
|
@ -157,6 +157,6 @@ public class AuthorizationClientExample {
|
|||
* @return a string representing a EAT
|
||||
*/
|
||||
private static String getEntitlementAPIToken(AuthzClient authzClient) {
|
||||
return authzClient.obtainAccessToken("alice", "password").getToken();
|
||||
return authzClient.obtainAccessToken("alice", "alice").getToken();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,207 +0,0 @@
|
|||
var module = angular.module('photoz-uma', ['ngRoute', 'ngResource']);
|
||||
|
||||
var Identity = {};
|
||||
|
||||
angular.element(document).ready(function ($http) {
|
||||
var keycloakAuth = new Keycloak('keycloak.json');
|
||||
Identity.loggedIn = false;
|
||||
keycloakAuth.init({onLoad: 'login-required'}).success(function () {
|
||||
Identity.loggedIn = true;
|
||||
Identity.authz = keycloakAuth;
|
||||
Identity.logout = function () {
|
||||
Identity.loggedIn = false;
|
||||
Identity.claim = {};
|
||||
Identity.authc = null;
|
||||
window.location = this.authz.authServerUrl + "/realms/photoz-uma/protocol/openid-connect/logout?redirect_uri=http://localhost:8080/photoz-uma-html5-client/index.html";
|
||||
Identity.authz = null;
|
||||
};
|
||||
Identity.claim = {};
|
||||
Identity.claim.name = Identity.authz.idTokenParsed.name;
|
||||
Identity.hasRole = function (name) {
|
||||
if (Identity.authz && Identity.authz.hasRealmRole(name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Identity.isAdmin = function () {
|
||||
return this.hasRole("admin");
|
||||
};
|
||||
Identity.authc = {};
|
||||
Identity.authc.token = Identity.authz.token;
|
||||
module.factory('Identity', function () {
|
||||
return Identity;
|
||||
});
|
||||
angular.bootstrap(document, ["photoz-uma"]);
|
||||
}).error(function () {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Album, Identity) {
|
||||
Album.query(function (albums) {
|
||||
$scope.albums = albums;
|
||||
});
|
||||
|
||||
$scope.Identity = Identity;
|
||||
|
||||
$scope.deleteAlbum = function (album) {
|
||||
new Album(album).$delete({id: album.id}, function () {
|
||||
$route.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
module.controller('TokenCtrl', function ($scope, Identity) {
|
||||
$scope.showRpt = function () {
|
||||
document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.uma.rpt.rpt), null, ' ');
|
||||
}
|
||||
|
||||
$scope.showAccessToken = function () {
|
||||
document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authc.token), null, ' ');
|
||||
}
|
||||
|
||||
$scope.requestEntitlements = function () {
|
||||
var request = new XMLHttpRequest();
|
||||
|
||||
request.open("GET", "http://localhost:8080/auth/realms/photoz-uma/authz/entitlement/photoz-uma-restful-api", true);
|
||||
request.setRequestHeader("Authorization", "Bearer " + Identity.authc.token);
|
||||
request.onreadystatechange = function () {
|
||||
if (request.readyState == 4 && request.status == 200) {
|
||||
Identity.uma.rpt = JSON.parse(request.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
request.send(null);
|
||||
}
|
||||
});
|
||||
module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location, Album) {
|
||||
$scope.album = {};
|
||||
if ($routeParams.id) {
|
||||
$scope.album = Album.get({id: $routeParams.id});
|
||||
}
|
||||
$scope.create = function () {
|
||||
var newAlbum = new Album($scope.album);
|
||||
newAlbum.$save({}, function (data) {
|
||||
$location.path('/');
|
||||
});
|
||||
};
|
||||
});
|
||||
module.controller('ProfileCtrl', function ($scope, $http, $routeParams, $location, Profile) {
|
||||
$scope.profile = Profile.get();
|
||||
});
|
||||
module.controller('AdminAlbumCtrl', function ($scope, $http, $route, AdminAlbum, Album) {
|
||||
$scope.albums = {};
|
||||
$http.get('/photoz-uma-restful-api/admin/album').success(function (data) {
|
||||
$scope.albums = data;
|
||||
});
|
||||
$scope.deleteAlbum = function (album) {
|
||||
var newAlbum = new Album(album);
|
||||
newAlbum.$delete({id: album.id}, function () {
|
||||
$route.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
module.factory('Album', ['$resource', function ($resource) {
|
||||
return $resource('http://localhost:8080/photoz-uma-restful-api/album/:id');
|
||||
}]);
|
||||
module.factory('Profile', ['$resource', function ($resource) {
|
||||
return $resource('http://localhost:8080/photoz-uma-restful-api/profile');
|
||||
}]);
|
||||
module.factory('AdminAlbum', ['$resource', function ($resource) {
|
||||
return $resource('http://localhost:8080/photoz-uma-restful-api/admin/album/:id');
|
||||
}]);
|
||||
module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
|
||||
return {
|
||||
request: function (request) {
|
||||
document.getElementById("output").innerHTML = '';
|
||||
if (Identity.uma && Identity.uma.rpt && request.url.indexOf('/authorize') == -1) {
|
||||
retries = 0;
|
||||
request.headers.Authorization = 'Bearer ' + Identity.uma.rpt.rpt;
|
||||
} else {
|
||||
request.headers.Authorization = 'Bearer ' + Identity.authc.token;
|
||||
}
|
||||
return request;
|
||||
},
|
||||
responseError: function (rejection) {
|
||||
if (rejection.status == 403 || rejection.status == 401) {
|
||||
var retry = (!rejection.config.retry || rejection.config.retry < 1);
|
||||
|
||||
if (!retry) {
|
||||
document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
|
||||
if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
|
||||
if (rejection.status == 401) {
|
||||
console.log("Here");
|
||||
var authenticateHeader = rejection.headers('WWW-Authenticate');
|
||||
|
||||
if (authenticateHeader.startsWith('UMA')) {
|
||||
var params = authenticateHeader.split(',');
|
||||
|
||||
for (i = 0; i < params.length; i++) {
|
||||
var param = params[i].split('=');
|
||||
|
||||
if (param[0] == 'ticket') {
|
||||
var ticket = param[1].substring(1, param[1].length - 1).trim();
|
||||
|
||||
var data = JSON.stringify({
|
||||
ticket: ticket,
|
||||
rpt: Identity.uma ? Identity.uma.rpt.rpt : ""
|
||||
});
|
||||
|
||||
var $http = $injector.get("$http");
|
||||
|
||||
var deferred = $q.defer();
|
||||
|
||||
$http.post('http://localhost:8080/auth/realms/photoz-uma/authz/authorize', data, {headers: {"Authorization": "Bearer " + Identity.authc.token}})
|
||||
.then(function (authzResponse) {
|
||||
if (authzResponse.data) {
|
||||
Identity.uma = {};
|
||||
Identity.uma.rpt = authzResponse.data;
|
||||
}
|
||||
deferred.resolve(rejection);
|
||||
}, function (authzResponse) {
|
||||
document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
|
||||
});
|
||||
|
||||
var promise = deferred.promise;
|
||||
|
||||
return promise.then(function (res) {
|
||||
if (!res.config.retry) {
|
||||
res.config.retry = 1;
|
||||
} else {
|
||||
res.config.retry++;
|
||||
}
|
||||
return $http(res.config).then(function (response) {
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
};
|
||||
});
|
||||
module.config(function ($httpProvider, $routeProvider) {
|
||||
$httpProvider.interceptors.push('authInterceptor');
|
||||
$routeProvider.when('/', {
|
||||
templateUrl: 'partials/home.html',
|
||||
controller: 'GlobalCtrl'
|
||||
}).when('/album/create', {
|
||||
templateUrl: 'partials/album/create.html',
|
||||
controller: 'AlbumCtrl',
|
||||
}).when('/album/:id', {
|
||||
templateUrl: 'partials/album/detail.html',
|
||||
controller: 'AlbumCtrl',
|
||||
}).when('/admin/album', {
|
||||
templateUrl: 'partials/admin/albums.html',
|
||||
controller: 'AdminAlbumCtrl',
|
||||
}).when('/profile', {
|
||||
templateUrl: 'partials/profile.html',
|
||||
controller: 'ProfileCtrl',
|
||||
});
|
||||
});
|
|
@ -1 +0,0 @@
|
|||
Test
|
|
@ -4,17 +4,16 @@ This is a simple application based on HTML5+AngularJS+JAX-RS that will introduce
|
|||
|
||||
Basically, it is a project containing three modules:
|
||||
|
||||
* **photoz-uma-restful-api**, with a simple RESTFul API based on JAX-RS and acting as a regular **client application**.
|
||||
* **photoz-uma-html5-client**, with a HTML5+AngularJS client that will consume the RESTful API and acting as a **resource server**.
|
||||
* **photoz-uma-authz-policy**, with a simple project with some rule-based policies using JBoss Drools.
|
||||
* **photoz-restful-api**, a simple RESTFul API based on JAX-RS and acting as a resource server.
|
||||
* **photoz-html5-client**, a HTML5+AngularJS client that will consume the RESTful API published by a resource resourcer.
|
||||
* **photoz-authz-policy**, a simple project with some rule-based policies using JBoss Drools.
|
||||
|
||||
For this application, users can be regular users or administrators. Regular users can create/view/delete their albums
|
||||
and administrators can view the albums for all users.
|
||||
and administrators can do anything.
|
||||
|
||||
In Keycloak, albums are resources that must be protected based on a set of policies that defines who and how can access them.
|
||||
Beside that, resources belong to a specific resource server, in this case to the *photoz-uma-restful-api*.
|
||||
In Keycloak, albums are resources that must be protected based on a set of policies that defines who and how can access them.
|
||||
|
||||
The resources are also associated with a set of scopes that define a specific access context. In this case, albums have three main scopes:
|
||||
The resources are also associated with a set of scopes that defines a specific access context. In this case, albums have three main scopes:
|
||||
|
||||
* urn:photoz.com:scopes:album:create
|
||||
* urn:photoz.com:scopes:album:view
|
||||
|
@ -26,12 +25,14 @@ The authorization requirements for this example application are based on the fol
|
|||
|
||||
* For instance, Alice can create, view and delete her albums.
|
||||
|
||||
* Only the owner and administrators can delete albums. Here we are considering policies based on the *urn:photoz.com:scopes:album:delete*
|
||||
* Only the owner and administrators can delete albums. Here we are considering policies based on the *urn:photoz.com:scopes:album:delete* scope
|
||||
|
||||
* For instance, only Alice can delete her album.
|
||||
|
||||
* Only administrators can access the Administration API (which basically provides ways to query albums for all users)
|
||||
|
||||
* Administrators are only authorized to access resources if the client's ip address is well known
|
||||
|
||||
That said, this application will show you how to use the Keycloak to define policies using:
|
||||
|
||||
* Role-based Access Control
|
||||
|
@ -46,11 +47,11 @@ It also provides some background on how you can actually protect your JAX-RS end
|
|||
|
||||
## Create the Example Realm and a Resource Server
|
||||
|
||||
Considering that your AuthZ Server is up and running, log in to the Keycloak Administration Console.
|
||||
Considering that your Keycloak Server is up and running, log in to the Keycloak Administration Console.
|
||||
|
||||
Now, create a new realm based on the following configuration file:
|
||||
|
||||
examples/authz/photoz/photoz-uma-realm.json
|
||||
examples/authz/photoz/photoz-realm.json
|
||||
|
||||
That will import a pre-configured realm with everything you need to run this example. For more details about how to import a realm
|
||||
into Keycloak, check the Keycloak's reference documentation.
|
||||
|
@ -58,42 +59,40 @@ into Keycloak, check the Keycloak's reference documentation.
|
|||
After importing that file, you'll have a new realm called ``photoz``.
|
||||
|
||||
Back to the command-line, build the example application. This step is necessary given that we're using policies based on
|
||||
JBoss Drools, which require ``photoz-uma-authz-policy`` artifact installed into your local maven repository.
|
||||
JBoss Drools, which require ``photoz-authz-policy`` artifact installed into your local maven repository.
|
||||
|
||||
cd examples/authz/photoz
|
||||
mvn clean install
|
||||
|
||||
Now, let's import another configuration using the Administration Console in order to configure the ``photoz-uma-restful-api`` as a resource server with all resources, scopes, permissions and policies.
|
||||
Now, let's import another configuration using the Administration Console in order to configure the client application ``photoz-restful-api`` as a resource server with all resources, scopes, permissions and policies.
|
||||
|
||||
Click on ``Authorization`` on the left side menu. Click on the ``Create`` button on the top of the resource server table. This will
|
||||
open the page that allows you to create a new resource server.
|
||||
Click on ``Clients`` on the left side menu. Click on the ``photoz-restful-api`` on the client listing page. This will
|
||||
open the ``Client Details`` page. Once there, click on the `Authorization` tab.
|
||||
|
||||
Click on the ``Select file`` button, which means you want to import a resource server configuration. Now select the file that is located at:
|
||||
|
||||
examples/authz/photoz/photoz-uma-restful-api/photoz-uma-restful-api-authz-config.json
|
||||
examples/authz/photoz/photoz-restful-api/photoz-restful-api-authz-config.json
|
||||
|
||||
Now click ``Upload`` and a new resource server will be created based on the ``photoz-uma-restful-api`` client application.
|
||||
Now click ``Upload`` and the resource server will be updated accordingly.
|
||||
|
||||
## Deploy and Run the Example Applications
|
||||
|
||||
To deploy the example applications, follow these steps:
|
||||
|
||||
cd examples/authz/photoz/photoz-uma-html5-client
|
||||
mvn wildfly:deploy
|
||||
cd examples/authz/photoz/photoz-html5-client
|
||||
mvn clean package wildfly:deploy
|
||||
|
||||
And then:
|
||||
|
||||
cd examples/authz/photoz/photoz-uma-restful-api
|
||||
mvn wildfly:deploy
|
||||
cd examples/authz/photoz/photoz-restful-api
|
||||
mvn clean package wildfly:deploy
|
||||
|
||||
Now, try to access the client application using the following URL:
|
||||
|
||||
http://localhost:8080/photoz-uma-html5-client
|
||||
http://localhost:8080/photoz-html5-client
|
||||
|
||||
If everything is correct, you will be redirect to Keycloak login page. You can login to the application with the following credentials:
|
||||
|
||||
* username: jdoe / password: jdoe
|
||||
* username: alice / password: alice
|
||||
* username: admin / password: admin
|
||||
|
||||
|
||||
* username: admin / password: admin
|
|
@ -5,18 +5,18 @@
|
|||
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-authz-photoz-uma-parent</artifactId>
|
||||
<artifactId>keycloak-authz-photoz-parent</artifactId>
|
||||
<version>2.0.0.CR1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>photoz-uma-authz-policy</artifactId>
|
||||
<artifactId>photoz-authz-policy</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Keycloak Authz: Examples - Photoz UMA Authz Rule-based Policy</name>
|
||||
<name>Keycloak Authz: Examples - Photoz Authz Rule-based Policy</name>
|
||||
|
||||
<description>
|
||||
Photoz UMA Authz Rule-based Policies using JBoss Drools
|
||||
Photoz Authz Rule-based Policies using JBoss Drools
|
||||
</description>
|
||||
|
||||
</project>
|
|
@ -7,7 +7,7 @@ rule "Authorize Using Context Information"
|
|||
when
|
||||
$evaluation : Evaluation(
|
||||
$attributes: context.attributes,
|
||||
$attributes.containsValue("kc.authz.context.authc.method", "otp"),
|
||||
$attributes.containsValue("kc.identity.authc.method", "otp"),
|
||||
$attributes.containsValue("someAttribute", "you_can_access")
|
||||
)
|
||||
then
|
|
@ -4,16 +4,16 @@
|
|||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-authz-photoz-uma-parent</artifactId>
|
||||
<artifactId>keycloak-authz-photoz-parent</artifactId>
|
||||
<version>2.0.0.CR1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>photoz-uma-html5-client</artifactId>
|
||||
<artifactId>photoz-html5-client</artifactId>
|
||||
<packaging>war</packaging>
|
||||
|
||||
<name>Keycloak Authz: Photoz UMA HTML5 Client</name>
|
||||
<description>Photoz UMA HTML5 Client</description>
|
||||
<name>Keycloak Authz: Photoz HTML5 Client</name>
|
||||
<description>Photoz HTML5 Client</description>
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}</finalName>
|
|
@ -4,6 +4,6 @@
|
|||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
||||
version="3.0">
|
||||
|
||||
<module-name>photoz-uma-html5-client</module-name>
|
||||
<module-name>photoz-html5-client</module-name>
|
||||
|
||||
</web-app>
|
|
@ -12,6 +12,9 @@
|
|||
<script src="lib/jwt-decode.min.js"></script>
|
||||
|
||||
<script src="http://localhost:8080/auth/js/keycloak.js"></script>
|
||||
<script src="http://localhost:8080/auth/js/keycloak-authz.js"></script>
|
||||
<script src="js/security/keycloak-authorization.js" type="text/javascript"></script>
|
||||
<script src="js/identity.js" type="text/javascript"></script>
|
||||
<script src="js/app.js" type="text/javascript"></script>
|
||||
</head>
|
||||
|
168
examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
Executable file
168
examples/authz/photoz/photoz-html5-client/src/main/webapp/js/app.js
Executable file
|
@ -0,0 +1,168 @@
|
|||
var module = angular.module('photoz', ['ngRoute', 'ngResource']);
|
||||
|
||||
var resourceServerId = 'photoz-restful-api';
|
||||
var apiUrl = window.location.origin + '/' + resourceServerId;
|
||||
|
||||
angular.element(document).ready(function ($http) {
|
||||
var keycloak = new Keycloak('keycloak.json');
|
||||
keycloak.init({onLoad: 'login-required'}).success(function () {
|
||||
console.log('User is now authenticated.');
|
||||
|
||||
module.factory('Identity', function () {
|
||||
return new Identity(keycloak);
|
||||
});
|
||||
|
||||
angular.bootstrap(document, ["photoz"]);
|
||||
}).error(function () {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
|
||||
module.config(function ($httpProvider, $routeProvider) {
|
||||
$httpProvider.interceptors.push('authInterceptor');
|
||||
$routeProvider.when('/', {
|
||||
templateUrl: 'partials/home.html',
|
||||
controller: 'GlobalCtrl'
|
||||
}).when('/album/create', {
|
||||
templateUrl: 'partials/album/create.html',
|
||||
controller: 'AlbumCtrl',
|
||||
}).when('/album/:id', {
|
||||
templateUrl: 'partials/album/detail.html',
|
||||
controller: 'AlbumCtrl',
|
||||
}).when('/admin/album', {
|
||||
templateUrl: 'partials/admin/albums.html',
|
||||
controller: 'AdminAlbumCtrl',
|
||||
}).when('/profile', {
|
||||
templateUrl: 'partials/profile.html',
|
||||
controller: 'ProfileCtrl',
|
||||
});
|
||||
});
|
||||
|
||||
module.controller('GlobalCtrl', function ($scope, $http, $route, $location, Album, Identity) {
|
||||
Album.query(function (albums) {
|
||||
$scope.albums = albums;
|
||||
});
|
||||
|
||||
$scope.Identity = Identity;
|
||||
|
||||
$scope.deleteAlbum = function (album) {
|
||||
new Album(album).$delete({id: album.id}, function () {
|
||||
$route.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.controller('TokenCtrl', function ($scope, Identity) {
|
||||
$scope.showRpt = function () {
|
||||
document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authorization.rpt), null, ' ');
|
||||
}
|
||||
|
||||
$scope.showAccessToken = function () {
|
||||
document.getElementById("output").innerHTML = JSON.stringify(jwt_decode(Identity.authc.token), null, ' ');
|
||||
}
|
||||
|
||||
$scope.requestEntitlements = function () {
|
||||
Identity.authorization.entitlement('photoz-restful-api').then(function (rpt) {});
|
||||
}
|
||||
});
|
||||
|
||||
module.controller('AlbumCtrl', function ($scope, $http, $routeParams, $location, Album) {
|
||||
$scope.album = {};
|
||||
if ($routeParams.id) {
|
||||
$scope.album = Album.get({id: $routeParams.id});
|
||||
}
|
||||
$scope.create = function () {
|
||||
var newAlbum = new Album($scope.album);
|
||||
newAlbum.$save({}, function (data) {
|
||||
$location.path('/');
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.controller('ProfileCtrl', function ($scope, $http, $routeParams, $location, Profile) {
|
||||
$scope.profile = Profile.get();
|
||||
});
|
||||
|
||||
module.controller('AdminAlbumCtrl', function ($scope, $http, $route, AdminAlbum, Album) {
|
||||
$scope.albums = {};
|
||||
$http.get(apiUrl + '/admin/album').success(function (data) {
|
||||
$scope.albums = data;
|
||||
});
|
||||
$scope.deleteAlbum = function (album) {
|
||||
var newAlbum = new Album(album);
|
||||
newAlbum.$delete({id: album.id}, function () {
|
||||
$route.reload();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.factory('Album', ['$resource', function ($resource) {
|
||||
return $resource(apiUrl + '/album/:id');
|
||||
}]);
|
||||
|
||||
module.factory('Profile', ['$resource', function ($resource) {
|
||||
return $resource(apiUrl + '/profile');
|
||||
}]);
|
||||
|
||||
module.factory('AdminAlbum', ['$resource', function ($resource) {
|
||||
return $resource(apiUrl + '/admin/album/:id');
|
||||
}]);
|
||||
|
||||
module.factory('authInterceptor', function ($q, $injector, $timeout, Identity) {
|
||||
return {
|
||||
request: function (request) {
|
||||
document.getElementById("output").innerHTML = '';
|
||||
if (Identity.authorization && Identity.authorization.rpt && request.url.indexOf('/authorize') == -1) {
|
||||
retries = 0;
|
||||
request.headers.Authorization = 'Bearer ' + Identity.authorization.rpt;
|
||||
} else {
|
||||
request.headers.Authorization = 'Bearer ' + Identity.authc.token;
|
||||
}
|
||||
return request;
|
||||
},
|
||||
responseError: function (rejection) {
|
||||
var status = rejection.status;
|
||||
|
||||
if (status == 403 || status == 401) {
|
||||
var retry = (!rejection.config.retry || rejection.config.retry < 1);
|
||||
|
||||
if (!retry) {
|
||||
document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
|
||||
if (rejection.config.url.indexOf('/authorize') == -1 && retry) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
// here is the authorization logic, which tries to obtain an authorization token from the server in case the resource server
|
||||
// returns a 403 or 401.
|
||||
Identity.authorization.authorize(rejection.headers('WWW-Authenticate')).then(function (rpt) {
|
||||
deferred.resolve(rejection);
|
||||
}, function () {
|
||||
document.getElementById("output").innerHTML = 'You can not access or perform the requested operation on this resource.';
|
||||
}, function () {
|
||||
document.getElementById("output").innerHTML = 'Unexpected error from server.';
|
||||
});
|
||||
|
||||
var promise = deferred.promise;
|
||||
|
||||
return promise.then(function (res) {
|
||||
if (!res.config.retry) {
|
||||
res.config.retry = 1;
|
||||
} else {
|
||||
res.config.retry++;
|
||||
}
|
||||
|
||||
var $http = $injector.get("$http");
|
||||
|
||||
return $http(res.config).then(function (response) {
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return $q.reject(rejection);
|
||||
}
|
||||
};
|
||||
});
|
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Creates an Identity object holding the information obtained from the access token issued by Keycloak, after a successful authentication,
|
||||
* and a few utility methods to manage it.
|
||||
*/
|
||||
(function (window, undefined) {
|
||||
var Identity = function (keycloak) {
|
||||
this.loggedIn = true;
|
||||
|
||||
this.claims = {};
|
||||
this.claims.name = keycloak.idTokenParsed.name;
|
||||
|
||||
this.authc = {};
|
||||
this.authc.token = keycloak.token;
|
||||
|
||||
this.logout = function () {
|
||||
keycloak.logout();
|
||||
};
|
||||
|
||||
this.hasRole = function (name) {
|
||||
if (keycloak && keycloak.hasRealmRole(name)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
this.isAdmin = function () {
|
||||
return this.hasRole("admin");
|
||||
};
|
||||
|
||||
this.authorization = new KeycloakAuthorization(keycloak);
|
||||
}
|
||||
|
||||
if ( typeof module === "object" && module && typeof module.exports === "object" ) {
|
||||
module.exports = Identity;
|
||||
} else {
|
||||
window.Identity = Identity;
|
||||
|
||||
if ( typeof define === "function" && define.amd ) {
|
||||
define( "identity", [], function () { return Identity; } );
|
||||
}
|
||||
}
|
||||
})( window );
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"realm": "photoz-uma",
|
||||
"realm": "photoz",
|
||||
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url" : "http://localhost:8080/auth",
|
||||
"ssl-required" : "external",
|
||||
"resource" : "photoz-uma-html5-client",
|
||||
"resource" : "photoz-html5-client",
|
||||
"public-client" : true,
|
||||
"use-resource-role-mappings": "false",
|
||||
"scope" : {
|
|
@ -1,4 +1,4 @@
|
|||
<h2><span>Welcome To Photoz, {{Identity.claim.name}}</span> [<a href="" ng-click="Identity.logout()">Sign Out</a>]</h2>
|
||||
<h2><span>Welcome To Photoz, {{Identity.claims.name}}</span> [<a href="" ng-click="Identity.logout()">Sign Out</a>]</h2>
|
||||
<div data-ng-show="Identity.isAdmin()"><b>Administration: </b> [<a href="#/admin/album">All Albums</a>]</div>
|
||||
<hr/>
|
||||
<br/>
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"realm": "photoz-uma",
|
||||
"realm": "photoz",
|
||||
"enabled": true,
|
||||
"sslRequired": "external",
|
||||
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
|
||||
|
@ -62,12 +62,12 @@
|
|||
}
|
||||
},
|
||||
{
|
||||
"username": "service-account-photoz-uma-restful-api",
|
||||
"username": "service-account-photoz-restful-api",
|
||||
"enabled": true,
|
||||
"email": "service-account-photoz-uma-restful-api@placeholder.org",
|
||||
"serviceAccountClientId": "photoz-uma-restful-api",
|
||||
"email": "service-account-photoz-restful-api@placeholder.org",
|
||||
"serviceAccountClientId": "photoz-restful-api",
|
||||
"clientRoles": {
|
||||
"photoz-uma-restful-api" : ["uma_protection"]
|
||||
"photoz-restful-api" : ["uma_protection"]
|
||||
}
|
||||
}
|
||||
],
|
||||
|
@ -85,25 +85,25 @@
|
|||
},
|
||||
"clients": [
|
||||
{
|
||||
"clientId": "photoz-uma-html5-client",
|
||||
"clientId": "photoz-html5-client",
|
||||
"enabled": true,
|
||||
"adminUrl": "/photoz-uma-html5-client",
|
||||
"baseUrl": "/photoz-uma-html5-client",
|
||||
"adminUrl": "/photoz-html5-client",
|
||||
"baseUrl": "/photoz-html5-client",
|
||||
"publicClient": true,
|
||||
"redirectUris": [
|
||||
"/photoz-uma-html5-client/*"
|
||||
"/photoz-html5-client/*"
|
||||
],
|
||||
"webOrigins": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"clientId": "photoz-uma-restful-api",
|
||||
"clientId": "photoz-restful-api",
|
||||
"enabled": true,
|
||||
"baseUrl": "/photoz-uma-restful-api",
|
||||
"baseUrl": "/photoz-restful-api",
|
||||
"authorizationServicesEnabled" : true,
|
||||
"redirectUris": [
|
||||
"/photoz-uma-restful-api/*"
|
||||
"/photoz-restful-api/*"
|
||||
],
|
||||
"secret": "secret"
|
||||
}
|
|
@ -46,7 +46,7 @@
|
|||
"type": "drools",
|
||||
"config": {
|
||||
"mavenArtifactVersion": "2.0.0.CR1-SNAPSHOT",
|
||||
"mavenArtifactId": "photoz-uma-authz-policy",
|
||||
"mavenArtifactId": "photoz-authz-policy",
|
||||
"sessionName": "MainOwnerSession",
|
||||
"mavenArtifactGroupId": "org.keycloak",
|
||||
"moduleName": "PhotozAuthzOwnerPolicy",
|
||||
|
@ -77,7 +77,7 @@
|
|||
"logic": "POSITIVE",
|
||||
"decisionStrategy": "UNANIMOUS",
|
||||
"config": {
|
||||
"code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.authz.context.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}"
|
||||
"code": "var contextAttributes = $evaluation.getContext().getAttributes();\n\nif (contextAttributes.containsValue('kc.client.network.ip_address', '127.0.0.1')) {\n $evaluation.grant();\n}"
|
||||
}
|
||||
},
|
||||
{
|
|
@ -5,16 +5,16 @@
|
|||
|
||||
<parent>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-authz-photoz-uma-parent</artifactId>
|
||||
<artifactId>keycloak-authz-photoz-parent</artifactId>
|
||||
<version>2.0.0.CR1-SNAPSHOT</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>photoz-uma-restful-api</artifactId>
|
||||
<artifactId>photoz-restful-api</artifactId>
|
||||
<packaging>war</packaging>
|
||||
|
||||
<name>Keycloak Authz: Photoz UMA RESTful API</name>
|
||||
<description>Photoz UMA RESTful API</description>
|
||||
<name>Keycloak Authz: Photoz RESTful API</name>
|
||||
<description>Photoz RESTful API</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"realm": "photoz-uma",
|
||||
"realm": "photoz",
|
||||
"auth-server-url": "http://localhost:8080/auth",
|
||||
"resource": "photoz-uma-restful-api",
|
||||
"resource": "photoz-restful-api",
|
||||
"credentials": {
|
||||
"secret": "secret"
|
||||
}
|
|
@ -1,15 +1,14 @@
|
|||
{
|
||||
"realm": "photoz-uma",
|
||||
"realm": "photoz",
|
||||
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url": "http://localhost:8080/auth",
|
||||
"ssl-required": "external",
|
||||
"resource": "photoz-uma-restful-api",
|
||||
"resource": "photoz-restful-api",
|
||||
"bearer-only" : true,
|
||||
"credentials": {
|
||||
"secret": "secret"
|
||||
},
|
||||
"policy-enforcer": {
|
||||
"user-managed-access" : {},
|
||||
"paths": [
|
||||
{
|
||||
"path" : "/album/*",
|
||||
|
@ -31,10 +30,6 @@
|
|||
{
|
||||
"method": "DELETE",
|
||||
"scopes" : ["urn:photoz.com:scopes:album:delete"]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"scopes" : ["urn:photoz.com:scopes:album:view"]
|
||||
}
|
||||
]
|
||||
},
|
|
@ -4,7 +4,7 @@
|
|||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
||||
version="3.0">
|
||||
|
||||
<module-name>photoz-uma-restful-api</module-name>
|
||||
<module-name>photoz-restful-api</module-name>
|
||||
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
<login-config>
|
||||
<auth-method>KEYCLOAK</auth-method>
|
||||
<realm-name>photoz-uma</realm-name>
|
||||
<realm-name>photoz</realm-name>
|
||||
</login-config>
|
||||
|
||||
<security-role>
|
|
@ -10,15 +10,15 @@
|
|||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<artifactId>keycloak-authz-photoz-uma-parent</artifactId>
|
||||
<artifactId>keycloak-authz-photoz-parent</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Keycloak Authz: PhotoZ UMA Example Application Parent</name>
|
||||
<name>Keycloak Authz: PhotoZ Example Application Parent</name>
|
||||
<description>PhotoZ Example Application</description>
|
||||
|
||||
<modules>
|
||||
<module>photoz-uma-restful-api</module>
|
||||
<module>photoz-uma-html5-client</module>
|
||||
<module>photoz-uma-authz-policy</module>
|
||||
<module>photoz-restful-api</module>
|
||||
<module>photoz-html5-client</module>
|
||||
<module>photoz-authz-policy</module>
|
||||
</modules>
|
||||
</project>
|
|
@ -22,7 +22,7 @@
|
|||
</properties>
|
||||
|
||||
<modules>
|
||||
<module>photoz-uma</module>
|
||||
<module>photoz</module>
|
||||
<module>servlet-authz</module>
|
||||
<module>hello-world</module>
|
||||
<module>hello-world-authz-service</module>
|
||||
|
|
|
@ -14,7 +14,7 @@ This application will also show you how to create a dynamic menu with the permis
|
|||
|
||||
## Create the Example Realm and a Resource Server
|
||||
|
||||
Considering that your AuthZ Server is up and running, log in to the Keycloak Administration Console.
|
||||
Considering that your Keycloak Server is up and running, log in to the Keycloak Administration Console.
|
||||
|
||||
Now, create a new realm based on the following configuration file:
|
||||
|
||||
|
@ -25,26 +25,30 @@ into Keycloak, check the Keycloak's reference documentation.
|
|||
|
||||
After importing that file, you'll have a new realm called ``servlet-authz``.
|
||||
|
||||
Now, let's import another configuration using the Administration Console in order to configure the ``servlet-authz-app`` client application as a resource server with all resources, scopes, permissions and policies.
|
||||
Now, let's import another configuration using the Administration Console in order to configure the client application ``servlet-authz-app`` as a resource server with all resources, scopes, permissions and policies.
|
||||
|
||||
Click on ``Authorization`` on the left side menu. Click on the ``Create`` button on the top of the resource server table. This will
|
||||
open the page that allows you to create a new resource server.
|
||||
Click on ``Clients`` on the left side menu. Click on the ``servlet-authz-app`` on the client listing page. This will
|
||||
open the ``Client Details`` page. Once there, click on the `Authorization` tab.
|
||||
|
||||
Click on the ``Select file`` button, which means you want to import a resource server configuration. Now select the file that is located at:
|
||||
|
||||
examples/authz/servlet-authz/servlet-authz-app-config.json
|
||||
|
||||
Now click ``Upload`` and a new resource server will be created based on the ``servlet-authz-app`` client application.
|
||||
Now click ``Upload`` and the resource server will be updated accordingly.
|
||||
|
||||
## Deploy and Run the Example Applications
|
||||
|
||||
To deploy the example applications, follow these steps:
|
||||
To deploy the example application, follow these steps:
|
||||
|
||||
cd examples/authz/servlet-authz
|
||||
mvn wildfly:deploy
|
||||
mvn clean package wildfly:deploy
|
||||
|
||||
Now, try to access the client application using the following URL:
|
||||
|
||||
http://localhost:8080/servlet-authz-app
|
||||
|
||||
If everything is correct, you will be redirect to Keycloak login page. You can login to the application with the following credentials:
|
||||
|
||||
* username: jdoe / password: jdoe (premium user)
|
||||
* username: alice / password: alice (regular user)
|
||||
* username: admin / password: admin (administrator)
|
||||
* username: jdoe / password: jdoe
|
||||
* username: alice / password: alice
|
||||
* username: admin / password: admin
|
|
@ -8,5 +8,7 @@
|
|||
"credentials": {
|
||||
"secret": "secret"
|
||||
},
|
||||
"policy-enforcer": {}
|
||||
"policy-enforcer": {
|
||||
"on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp"
|
||||
}
|
||||
}
|
|
@ -1,8 +1,6 @@
|
|||
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
||||
<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
|
||||
<html>
|
||||
<body>
|
||||
<h2 style="color: red">You can not access this resource. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||
.queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to log in as a different user.</h2>
|
||||
<h2 style="color: red">You can not access this resource.</h2>
|
||||
<%@include file="logout-include.jsp"%>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,4 @@
|
|||
<%@page import="org.keycloak.AuthorizationContext" %>
|
||||
<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
|
||||
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
||||
<%@ page import="org.keycloak.KeycloakSecurityContext" %>
|
||||
<%@ page import="org.keycloak.representations.authorization.Permission" %>
|
||||
|
||||
|
@ -11,8 +9,7 @@
|
|||
|
||||
<html>
|
||||
<body>
|
||||
<h2>Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||
.queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2>
|
||||
<%@include file="logout-include.jsp"%>
|
||||
<h2>This is a public resource. Try to access one of these <i>protected</i> resources:</h2>
|
||||
|
||||
<p><a href="protected/dynamicMenu.jsp">Dynamic Menu</a></p>
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
|
||||
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
||||
<%
|
||||
String scheme = request.getScheme();
|
||||
String host = request.getServerName();
|
||||
int port = request.getServerPort();
|
||||
String contextPath = request.getContextPath();
|
||||
String redirectUri = scheme + "://" + host + ":" + port + contextPath;
|
||||
%>
|
||||
<h2>Click <a href="<%= KeycloakUriBuilder.fromUri("http://localhost:8080/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||
.queryParam("redirect_uri", redirectUri).build("servlet-authz").toString()%>">here</a> to logout.</h2>
|
|
@ -1,8 +1,6 @@
|
|||
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
||||
<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
|
||||
<html>
|
||||
<body>
|
||||
<h2>Only Administrators can access this page. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||
.queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2></h2>
|
||||
<h2>Only Administrators can access this page.</h2>
|
||||
<%@include file="../../logout-include.jsp"%>
|
||||
</body>
|
||||
</html>
|
|
@ -1,6 +1,4 @@
|
|||
<%@page import="org.keycloak.AuthorizationContext" %>
|
||||
<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
|
||||
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
||||
<%@ page import="org.keycloak.KeycloakSecurityContext" %>
|
||||
|
||||
<%
|
||||
|
@ -10,8 +8,8 @@
|
|||
|
||||
<html>
|
||||
<body>
|
||||
<h2>Any authenticated user can access this page. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||
.queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2>
|
||||
<h2>Any authenticated user can access this page.</h2>
|
||||
<%@include file="../logout-include.jsp"%>
|
||||
|
||||
<p>Here is a dynamic menu built from the permissions returned by the server:</p>
|
||||
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
<%@ page import="org.keycloak.common.util.KeycloakUriBuilder" %>
|
||||
<%@ page import="org.keycloak.constants.ServiceUrlConstants" %>
|
||||
<html>
|
||||
<body>
|
||||
<h2>Only for premium users. Click <a href="<%= KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
|
||||
.queryParam("redirect_uri", "/servlet-authz-app").build("servlet-authz").toString()%>">here</a> to logout.</h2>
|
||||
|
||||
<h2>Only for premium users.</h2>
|
||||
<%@include file="../../logout-include.jsp"%>
|
||||
</body>
|
||||
</html>
|
|
@ -463,7 +463,7 @@ public class ResourceServerService {
|
|||
"\n" +
|
||||
"// using attributes from the evaluation context to obtain the realm\n" +
|
||||
"var contextAttributes = context.getAttributes();\n" +
|
||||
"var realmName = contextAttributes.getValue('kc.authz.context.authc.realm').asString(0);\n" +
|
||||
"var realmName = contextAttributes.getValue('kc.realm.name').asString(0);\n" +
|
||||
"\n" +
|
||||
"// using attributes from the identity to obtain the issuer\n" +
|
||||
"var identity = context.getIdentity();\n" +
|
||||
|
|
|
@ -106,7 +106,10 @@ public class AuthorizationTokenService {
|
|||
List<Permission> entitlements = Permissions.allPermits(results);
|
||||
|
||||
if (entitlements.isEmpty()) {
|
||||
asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN));
|
||||
asyncResponse.resume(Cors.add(httpRequest, Response.status(Status.FORBIDDEN)
|
||||
.entity(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN)))
|
||||
.allowedOrigins(identity.getAccessToken())
|
||||
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
|
||||
} else {
|
||||
AuthorizationResponse response = new AuthorizationResponse(createRequestingPartyToken(entitlements, identity.getAccessToken()));
|
||||
asyncResponse.resume(Cors.add(httpRequest, Response.status(Status.CREATED).entity(response)).allowedOrigins(identity.getAccessToken())
|
||||
|
@ -217,12 +220,14 @@ public class AuthorizationTokenService {
|
|||
}
|
||||
|
||||
private PermissionTicket verifyPermissionTicket(AuthorizationRequest request) {
|
||||
if (!Tokens.verifySignature(request.getTicket(), getRealm().getPublicKey())) {
|
||||
String ticketString = request.getTicket();
|
||||
|
||||
if (ticketString == null || !Tokens.verifySignature(ticketString, getRealm().getPublicKey())) {
|
||||
throw new ErrorResponseException("invalid_ticket", "Ticket verification failed", Status.FORBIDDEN);
|
||||
}
|
||||
|
||||
try {
|
||||
PermissionTicket ticket = new JWSInput(request.getTicket()).readJsonContent(PermissionTicket.class);
|
||||
PermissionTicket ticket = new JWSInput(ticketString).readJsonContent(PermissionTicket.class);
|
||||
|
||||
if (!ticket.isActive()) {
|
||||
throw new ErrorResponseException("invalid_ticket", "Invalid permission ticket.", Status.FORBIDDEN);
|
||||
|
|
|
@ -57,23 +57,23 @@ public class KeycloakEvaluationContext implements EvaluationContext {
|
|||
public Attributes getAttributes() {
|
||||
HashMap<String, Collection<String>> attributes = new HashMap<>();
|
||||
|
||||
attributes.put("kc.authz.context.time.date_time", Arrays.asList(new SimpleDateFormat("MM/dd/yyyy hh:mm:ss").format(new Date())));
|
||||
attributes.put("kc.authz.context.client.network.ip_address", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteAddr()));
|
||||
attributes.put("kc.authz.context.client.network.host", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteHost()));
|
||||
attributes.put("kc.time.date_time", Arrays.asList(new SimpleDateFormat("MM/dd/yyyy hh:mm:ss").format(new Date())));
|
||||
attributes.put("kc.client.network.ip_address", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteAddr()));
|
||||
attributes.put("kc.client.network.host", Arrays.asList(this.keycloakSession.getContext().getConnection().getRemoteHost()));
|
||||
|
||||
AccessToken accessToken = this.identity.getAccessToken();
|
||||
|
||||
if (accessToken != null) {
|
||||
attributes.put("kc.authz.context.client_id", Arrays.asList(accessToken.getIssuedFor()));
|
||||
attributes.put("kc.client.id", Arrays.asList(accessToken.getIssuedFor()));
|
||||
}
|
||||
|
||||
List<String> userAgents = this.keycloakSession.getContext().getRequestHeaders().getRequestHeader("User-Agent");
|
||||
|
||||
if (userAgents != null) {
|
||||
attributes.put("kc.authz.context.client.user_agent", userAgents);
|
||||
attributes.put("kc.client.user_agent", userAgents);
|
||||
}
|
||||
|
||||
attributes.put("kc.authz.context.authc.realm", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
|
||||
attributes.put("kc.realm.name", Arrays.asList(this.keycloakSession.getContext().getRealm().getName()));
|
||||
|
||||
return Attributes.from(attributes);
|
||||
}
|
||||
|
|
|
@ -80,8 +80,9 @@ public class EntitlementService {
|
|||
this.authorization = authorization;
|
||||
}
|
||||
|
||||
@Path("{resource_server_id}")
|
||||
@OPTIONS
|
||||
public Response authorizePreFlight() {
|
||||
public Response authorizePreFlight(@PathParam("resource_server_id") String resourceServerId) {
|
||||
return Cors.add(this.request, Response.ok()).auth().preflight().build();
|
||||
}
|
||||
|
||||
|
@ -118,7 +119,10 @@ public class EntitlementService {
|
|||
List<Permission> entitlements = Permissions.allPermits(results);
|
||||
|
||||
if (entitlements.isEmpty()) {
|
||||
asyncResponse.resume(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN));
|
||||
asyncResponse.resume(Cors.add(request, Response.status(Status.FORBIDDEN)
|
||||
.entity(new ErrorResponseException("not_authorized", "Authorization denied.", Status.FORBIDDEN)))
|
||||
.allowedOrigins(identity.getAccessToken())
|
||||
.exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
|
||||
} else {
|
||||
asyncResponse.resume(Cors.add(request, Response.ok().entity(new EntitlementResponse(createRequestingPartyToken(entitlements)))).allowedOrigins(identity.getAccessToken()).allowedMethods("GET").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build());
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.keycloak.authorization.model.ResourceServer;
|
|||
import org.keycloak.authorization.model.Scope;
|
||||
import org.keycloak.authorization.permission.ResourcePermission;
|
||||
import org.keycloak.authorization.policy.evaluation.Result;
|
||||
import org.keycloak.authorization.store.ResourceStore;
|
||||
import org.keycloak.authorization.store.StoreFactory;
|
||||
import org.keycloak.representations.authorization.Permission;
|
||||
|
||||
|
@ -58,9 +59,10 @@ public final class Permissions {
|
|||
public static List<ResourcePermission> all(ResourceServer resourceServer, Identity identity, AuthorizationProvider authorization) {
|
||||
List<ResourcePermission> permissions = new ArrayList<>();
|
||||
StoreFactory storeFactory = authorization.getStoreFactory();
|
||||
ResourceStore resourceStore = storeFactory.getResourceStore();
|
||||
|
||||
storeFactory.getResourceStore().findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource)));
|
||||
storeFactory.getResourceStore().findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource)));
|
||||
resourceStore.findByOwner(resourceServer.getClientId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource)));
|
||||
resourceStore.findByOwner(identity.getId()).stream().forEach(resource -> permissions.addAll(createResourcePermissions(resource)));
|
||||
|
||||
return permissions;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -261,7 +261,7 @@ public abstract class AbstractPhotozAdminTest extends AbstractAuthorizationTest
|
|||
|
||||
config.put("code",
|
||||
"var contextAttributes = $evaluation.getContext().getAttributes();" +
|
||||
"var networkAddress = contextAttributes.getValue('kc.authz.context.client.network.ip_address');" +
|
||||
"var networkAddress = contextAttributes.getValue('kc.client.network.ip_address');" +
|
||||
"if ('127.0.0.1'.equals(networkAddress.asInetAddress(0).getHostAddress())) {" +
|
||||
"$evaluation.grant();" +
|
||||
"}");
|
||||
|
|
|
@ -1044,7 +1044,7 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
|
|||
custom: true
|
||||
},
|
||||
{
|
||||
key : "kc.authz.context.authc.method",
|
||||
key : "kc.identity.authc.method",
|
||||
name : "Authentication Method",
|
||||
values: [
|
||||
{
|
||||
|
@ -1062,23 +1062,23 @@ module.controller('PolicyEvaluateCtrl', function($scope, $http, $route, $locatio
|
|||
]
|
||||
},
|
||||
{
|
||||
key : "kc.authz.context.authc.realm",
|
||||
key : "kc.realm.name",
|
||||
name : "Realm"
|
||||
},
|
||||
{
|
||||
key : "kc.authz.context.time.date_time",
|
||||
key : "kc.time.date_time",
|
||||
name : "Date/Time (MM/dd/yyyy hh:mm:ss)"
|
||||
},
|
||||
{
|
||||
key : "kc.authz.context.client.network.ip_address",
|
||||
key : "kc.client.network.ip_address",
|
||||
name : "Client IPv4 Address"
|
||||
},
|
||||
{
|
||||
key : "kc.authz.context.client.network.host",
|
||||
key : "kc.client.network.host",
|
||||
name : "Client Host"
|
||||
},
|
||||
{
|
||||
key : "kc.authz.context.client.user_agent",
|
||||
key : "kc.client.user_agent",
|
||||
name : "Client/User Agent"
|
||||
}
|
||||
];
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<ul class="nav nav-tabs nav-tabs-pf" data-ng-hide="create && !path[4]" style="margin-left: 15px">
|
||||
<li ng-class="{active: !path[6]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/">Settings</a></li>
|
||||
<li ng-class="{active: path[6] == 'resource'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/resource">Resources</a></li>
|
||||
<li ng-class="{active: path[6] == 'scope'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope">Scopes</a></li>
|
||||
<li ng-class="{active: path[6] == 'scope'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/scope">Authorization Scopes</a></li>
|
||||
<li ng-class="{active: path[6] == 'policy'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/policy">Policies</a></li>
|
||||
<li ng-class="{active: path[6] == 'permission'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/permission">Permissions</a></li>
|
||||
<li ng-class="{active: path[6] == 'evaluate'}" data-ng-hide="create"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server/evaluate">Evaluate</a></li>
|
||||
|
|
Loading…
Reference in a new issue