Merge pull request #2957 from pedroigor/authz-changes

Changes to authz examples and some minor improvements
This commit is contained in:
Stian Thorgersen 2016-06-23 07:49:47 +02:00 committed by GitHub
commit 2e2f34d94e
73 changed files with 800 additions and 382 deletions

View file

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

View file

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

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -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" : {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,5 +8,7 @@
"credentials": {
"secret": "secret"
},
"policy-enforcer": {}
"policy-enforcer": {
"on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp"
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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