Merge pull request #12 from patriot1burke/master

oauth
This commit is contained in:
Bill Burke 2013-07-29 07:40:50 -07:00
commit 1738831d27
27 changed files with 765 additions and 205 deletions

View file

@ -28,6 +28,7 @@ public class AbstractOAuthClient {
protected String codeUrl;
protected String stateCookieName = "OAuth_Token_Request_State";
protected Client client;
protected boolean isSecure;
protected final AtomicLong counter = new AtomicLong();
protected String getStateCode() {
@ -109,6 +110,8 @@ public class AbstractOAuthClient {
Form codeForm = new Form()
.param("grant_type", "authorization_code")
.param("code", code)
.param("client_id", clientId)
.param("Password", password)
.param("redirect_uri", redirectUri);
Response res = client.target(codeUrl).request().header(HttpHeaders.AUTHORIZATION, authHeader).post(Entity.form(codeForm));
try {

View file

@ -18,7 +18,7 @@ public class RealmRepresentation {
protected boolean cookieLoginAllowed;
protected String privateKey;
protected String publicKey;
protected Set<String> roles;
protected List<RoleRepresentation> roles;
protected List<RequiredCredentialRepresentation> requiredCredentials;
protected List<UserRepresentation> users;
protected List<RoleMappingRepresentation> roleMappings;
@ -146,11 +146,11 @@ public class RealmRepresentation {
this.accessCodeLifespan = accessCodeLifespan;
}
public Set<String> getRoles() {
public List<RoleRepresentation> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
public void setRoles(List<RoleRepresentation> roles) {
this.roles = roles;
}

View file

@ -16,7 +16,7 @@ public class ResourceRepresentation {
protected boolean surrogateAuthRequired;
protected boolean useRealmMappings;
protected List<CredentialRepresentation> credentials;
protected Set<String> roles;
protected List<RoleRepresentation> roles;
protected List<RoleMappingRepresentation> roleMappings;
protected List<ScopeMappingRepresentation> scopeMappings;
@ -44,17 +44,17 @@ public class ResourceRepresentation {
this.surrogateAuthRequired = surrogateAuthRequired;
}
public Set<String> getRoles() {
public List<RoleRepresentation> getRoles() {
return roles;
}
public void setRoles(Set<String> roles) {
public void setRoles(List<RoleRepresentation> roles) {
this.roles = roles;
}
public ResourceRepresentation role(String role) {
if (this.roles == null) this.roles = new HashSet<String>();
this.roles.add(role);
public ResourceRepresentation role(String role, String description) {
if (this.roles == null) this.roles = new ArrayList<RoleRepresentation>();
this.roles.add(new RoleRepresentation(role, description));
return this;
}

View file

@ -0,0 +1,34 @@
package org.keycloak.representations.idm;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class RoleRepresentation {
protected String name;
protected String description;
public RoleRepresentation() {
}
public RoleRepresentation(String name, String description) {
this.name = name;
this.description = description;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View file

@ -55,7 +55,7 @@ public class ServletOAuthClient extends AbstractOAuthClient {
if (cookiePath.equals("")) cookiePath = "/";
Cookie cookie = new Cookie(stateCookieName, state);
cookie.setSecure(true);
cookie.setSecure(isSecure);
cookie.setPath(cookiePath);
response.addCookie(cookie);
response.sendRedirect(url.toString());

61
examples/as7-eap-demo/README.md Executable file
View file

@ -0,0 +1,61 @@
Login, Distributed SSO, Distributed Logout, and Oauth Token Grant AS7 Examples
===================================
The following examples requires JBoss AS7 or EAP 6.1, and Resteasy 3.0.2 and has been tested on version EAP 6.1. Here's the highlights of the examples
* Delegating authentication of a web app to the remote authentication server via OAuth 2 protocols
* Distributed Single-Sign-On and Single-Logout
* Transferring identity and role mappings via a special bearer token (Skeleton Key Token).
* Bearer token authentication and authorization of JAX-RS services
* Obtaining bearer tokens via the OAuth2 protocol
There are 5 WAR projects. These all will run on the same jboss instance, but pretend each one is running on a different
machine on the network or Internet.
* **auth-server**: This is the keycloak SSO auth server
* **customer-app** A WAR applications that does remote login using OAUTH2 browser redirects with the auth server
* **product-app** A WAR applications that does remote login using OAUTH2 browser redirects with the auth server
* **database-service** JAX-RS services authenticated by bearer tokens only. The customer and product app invoke on it
to get data
* **third-party** Simple WAR that obtain a bearer token using OAuth2 using browser redirects to the auth-server.
The UI of each of these applications is very crude and exists just to show our OAuth2 implementation in action.
Step 1: Make sure you've upgraded Resteasy
--------------------------------------
The first thing you is upgrade Resteasy to 3.0.2 within JBoss as described [here](http://docs.jboss.org/resteasy/docs/3.0.2.Final/userguide/html/Installation_Configuration.html#upgrading-as7)
Step 2: Boot JBoss
---------------------------------------
Boot JBoss in 'standalone' mode.
Step 3: Build and deploy
---------------------------------------
next you must build and deploy
1. cd as7-eap-demo
2. mvn clean install
3. mvn jboss-as:deploy
Step 4: Login and Observe Apps
---------------------------------------
Try going to the customer app and viewing customer data:
[http://localhost:8080/customer-portal/customers/view.jsp](http://localhost:8080/customer-portal/customers/view.jsp)
This should take you to the auth-server login screen. Enter username: bburke@redhat.com and password: password.
If you click on the products link, you'll be take to the products app and show a product listing. The redirects
are still happening, but the auth-server knows you are already logged in so the login is bypassed.
If you click on the logout link of either of the product or customer app, you'll be logged out of all the applications.
Step 5: Traditional OAuth2 Example
----------------------------------
The customer and product apps are logins. The third-party app is the traditional OAuth2 usecase of a client wanting
to get permission to access a user's data. To run this example
[http://localhost:8080/oauth-client](http://localhost:8080/oauth-client)
If you area already logged in, you will not be asked for a username and password, but you will be redirected to
an oauth grant page. This page asks you if you want to grant certain permissions to the third-part app.

43
examples/as7-eap-demo/pom.xml Executable file
View file

@ -0,0 +1,43 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-1</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<name>Examples</name>
<description/>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak</groupId>
<artifactId>as7-eap-demo-pom</artifactId>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>7.1.1.Final</version>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<modules>
<module>server</module>
<module>customer-app</module>
<module>product-app</module>
<module>database-service</module>
<module>third-party</module>
</modules>
</project>

View file

@ -25,12 +25,34 @@
{ "type" : "Password",
"value" : "password" }
]
},
{
"username" : "third-party",
"enabled" : true,
"credentials" : [
{ "type" : "Password",
"value" : "password" }
]
}
],
"roles" : [
{ "name" : "user", "description" : "Have User privileges" },
{ "name" : "admin", "description" : "Have Administrator privileges" }
],
"roleMappings" : [
{
"username" : "bburke@redhat.com",
"roles" : ["user"]
},
{
"username" : "third-party",
"roles" : ["KEYCLOAK_IDENTITY_REQUESTER"]
}
],
"scopeMappings" : [
{
"username" : "third-party",
"roles" : ["user"]
}
],
"resources" : [

View file

@ -9,7 +9,7 @@
<head>
<meta charset="utf-8">
<title>Keycloak</title>
<title>Keycloak Realm Login Page</title>
<link rel="shortcut icon" type="image/x-icon" href="<%=application.getContextPath()%>/img/favicon.ico">

View file

@ -0,0 +1,82 @@
<%@ page import="org.picketlink.idm.model.*,org.keycloak.services.models.*,org.keycloak.services.resources.*,javax.ws.rs.core.*,java.util.*" language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%
RealmModel realm = (RealmModel)request.getAttribute(RealmModel.class.getName());
String username = (String)request.getAttribute("username");
%>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Keycloak</title>
<link rel="shortcut icon" type="image/x-icon" href="<%=application.getContextPath()%>/img/favicon.ico">
<link href="<%=application.getContextPath()%>/lib/bootstrap/css/bootstrap.css" rel="stylesheet">
<link href="<%=application.getContextPath()%>/lib/font-awesome/css/font-awesome.css" rel="stylesheet">
<link href="<%=application.getContextPath()%>/css/reset.css" rel="stylesheet">
<link href="<%=application.getContextPath()%>/css/base.css" rel="stylesheet">
</head>
<body>
<%
User client = (User)request.getAttribute("client");
List<Role> realmRolesRequested = (List<Role>)request.getAttribute("realmRolesRequested");
MultivaluedMap<String, Role> resourceRolesRequested = (MultivaluedMap<String, Role>)request.getAttribute("resourceRolesRequested");
%>
<h1>Grant request for: <%=client.getLoginName()%></h1>
<div class="modal-body">
<p>This app would like to:</p>
<hr/>
<%
if (realmRolesRequested.size() > 0) {
%> <ul> <%
for (Role role : realmRolesRequested) {
String desc = "Have " + role.getName() + " privileges.";
Attribute roleDesc = role.getAttribute("description");
if (roleDesc != null) {
desc = (String)roleDesc.getValue();
}
%>
<li><%=desc%></li>
<%
}
%> </ul> <%
}
for (String resource : resourceRolesRequested.keySet()) {
List<Role> roles = resourceRolesRequested.get(resource);
out.println("<i>For application " + resource + ":</i> ");
out.println("<ul>");
for (Role role : roles) {
String desc = "Have " + role.getName() + " privileges.";
Attribute roleDesc = role.getAttribute("description");
if (roleDesc != null) {
desc = (String)roleDesc.getValue();
}
out.println("<li>" + desc + "</li>");
}
out.println("</ul>");
}
%>
<hr/>
<form class="form-horizontal" name="oauthGrant" action="<%=request.getAttribute("action")%>" method="POST">
<input type="hidden" name="code" value="<%=request.getAttribute("code")%>">
<div class="control-group">
<div class="controls">
<input type="submit" name="accept" class="btn btn-primary" value="Accept">
<input type="submit" name="cancel" class="btn btn-primary" value="Cancel">
</div>
</div>
</form>
</div>
<footer>
<p>Powered By Keycloak</p>
</body>
</html>

61
examples/as7-eap-demo/third-party/pom.xml vendored Executable file
View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.0-alpha-1</version>
<relativePath>../../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak.example.as7.demo</groupId>
<artifactId>oauth-client-example</artifactId>
<packaging>war</packaging>
<name>Simple OAuth Client</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<version>1.0.1.Final</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<finalName>oauth-client</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>7.4.Final</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,69 @@
package org.jboss.resteasy.example.oauth;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.servlet.ServletOAuthClient;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import java.io.File;
import java.io.FileInputStream;
import java.security.KeyStore;
/**
* Stupid init code to load up the truststore so we can make appropriate SSL connections
* You really should use a better way of initializing this stuff.
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Bootstrap implements ServletContextListener {
private ServletOAuthClient client;
private static KeyStore loadKeyStore(String filename, String password) throws Exception {
KeyStore trustStore = KeyStore.getInstance(KeyStore
.getDefaultType());
File truststoreFile = new File(filename);
FileInputStream trustStream = new FileInputStream(truststoreFile);
trustStore.load(trustStream, password.toCharArray());
trustStream.close();
return trustStore;
}
@Override
public void contextInitialized(ServletContextEvent sce) {
client = new ServletOAuthClient();
/*
// hardcoded, WARNING, you should really have a better way of doing this
// configuration. Either use something like Spring or CDI, or even pull
// config vales from context-params
String truststorePath = "${jboss.server.config.dir}/client-truststore.ts";
String truststorePassword = "password";
truststorePath = EnvUtil.replace(truststorePath);
KeyStore truststore = null;
try
{
truststore = loadKeyStore(truststorePath, truststorePassword);
}
catch (Exception e)
{
throw new RuntimeException(e);
}
client.setTruststore(truststore);
*/
client.setClientId("third-party");
client.setPassword("password");
client.setAuthUrl("http://localhost:8080/auth-server/rest/realms/demo/tokens/login");
client.setCodeUrl("http://localhost:8080/auth-server/rest/realms/demo/tokens/access/codes");
client.setClient(new ResteasyClientBuilder().build());
client.start();
sce.getServletContext().setAttribute(ServletOAuthClient.class.getName(), client);
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
client.stop();
}
}

View file

@ -0,0 +1,69 @@
package org.jboss.resteasy.example.oauth;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.servlet.ServletOAuthClient;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ProductDatabaseClient {
public static void redirect(HttpServletRequest request, HttpServletResponse response) {
// This is really the worst code ever. The ServletOAuthClient is obtained by getting a context attribute
// that is set in the Bootstrap context listenr in this project.
// You really should come up with a better way to initialize
// and obtain the ServletOAuthClient. I actually suggest downloading the ServletOAuthClient code
// and take a look how it works.
ServletOAuthClient oAuthClient = (ServletOAuthClient) request.getServletContext().getAttribute(ServletOAuthClient.class.getName());
try {
oAuthClient.redirectRelative("pull_data.jsp", request, response);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static List<String> getProducts(HttpServletRequest request) {
// This is really the worst code ever. The ServletOAuthClient is obtained by getting a context attribute
// that is set in the Bootstrap context listenr in this project.
// You really should come up with a better way to initialize
// and obtain the ServletOAuthClient. I actually suggest downloading the ServletOAuthClient code
// and take a look how it works.
ServletOAuthClient oAuthClient = (ServletOAuthClient) request.getServletContext().getAttribute(ServletOAuthClient.class.getName());
String token = oAuthClient.getBearerToken(request);
ResteasyClient client = new ResteasyClientBuilder()
.trustStore(oAuthClient.getTruststore())
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build();
try {
// invoke without the Authorization header
Response response = client.target("http://localhost:8080/database/products").request().get();
response.close();
if (response.getStatus() != 401) {
response.close();
client.close();
throw new RuntimeException("Expecting an auth status code: " + response.getStatus());
}
} finally {
}
try {
Response response = client.target("http://localhost:8080/database/products").request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + token).get();
if (response.getStatus() != 200) {
response.close();
throw new RuntimeException("Failed to access!: " + response.getStatus());
}
return response.readEntity(new GenericType<List<String>>() {
});
} finally {
client.close();
}
}
}

View file

@ -0,0 +1,9 @@
<jboss-deployment-structure>
<deployment>
<!-- This allows you to define additional dependencies, it is the same as using the Dependencies: manifest attribute -->
<dependencies>
<module name="org.jboss.resteasy.resteasy-jaxrs" services="import"/>
<module name="org.jboss.resteasy.resteasy-jackson-provider" services="import"/>
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<listener>
<listener-class>org.jboss.resteasy.example.oauth.Bootstrap</listener-class>
</listener>
<!--
<security-constraint>
<web-resource-collection>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
-->
</web-app>

View file

@ -0,0 +1,6 @@
<html>
<body>
<h1>Third Party App That Pulls Data Using OAuth</h1>
<a href="redirect.jsp">Pull Data</a>
</body>
</html>

View file

@ -0,0 +1,21 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<head>
<title>Pull Page</title>
</head>
<body>
<h2>Pulled Product Listing</h2>
<%
java.util.List<String> list = org.jboss.resteasy.example.oauth.ProductDatabaseClient.getProducts(request);
for (String prod : list)
{
out.print("<p>");
out.print(prod);
out.println("</p>");
}
%>
<br><br>
</body>
</html>

View file

@ -0,0 +1,3 @@
<%
org.jboss.resteasy.example.oauth.ProductDatabaseClient.redirect(request, response);
%>

View file

@ -34,9 +34,6 @@
</plugins>
</build>
<modules>
<module>as7-eap-demo/server</module>
<module>as7-eap-demo/customer-app</module>
<module>as7-eap-demo/product-app</module>
<module>as7-eap-demo/database-service</module>
<module>as7-eap-demo</module>
</modules>
</project>

View file

@ -1,8 +1,13 @@
package org.keycloak.services.managers;
import org.keycloak.representations.SkeletonKeyToken;
import org.picketlink.idm.model.Role;
import org.picketlink.idm.model.User;
import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
@ -11,9 +16,16 @@ import java.util.UUID;
*/
public class AccessCodeEntry {
protected String id = UUID.randomUUID().toString() + System.currentTimeMillis();
protected String code;
protected String state;
protected String redirectUri;
protected long expiration;
protected SkeletonKeyToken token;
protected User user;
protected User client;
protected List<Role> realmRolesRequested = new ArrayList<Role>();
MultivaluedMap<String, Role> resourceRolesRequested = new MultivaluedHashMap<String, Role>();
public boolean isExpired() {
return expiration != 0 && (System.currentTimeMillis() / 1000) > expiration;
@ -23,6 +35,14 @@ public class AccessCodeEntry {
return id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public long getExpiration() {
return expiration;
}
@ -46,4 +66,36 @@ public class AccessCodeEntry {
public void setClient(User client) {
this.client = client;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
public List<Role> getRealmRolesRequested() {
return realmRolesRequested;
}
public MultivaluedMap<String, Role> getResourceRolesRequested() {
return resourceRolesRequested;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getRedirectUri() {
return redirectUri;
}
public void setRedirectUri(String redirectUri) {
this.redirectUri = redirectUri;
}
}

View file

@ -5,6 +5,7 @@ import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.RequiredCredentialRepresentation;
import org.keycloak.representations.idm.ResourceRepresentation;
import org.keycloak.representations.idm.RoleMappingRepresentation;
import org.keycloak.representations.idm.RoleRepresentation;
import org.keycloak.representations.idm.ScopeMappingRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.models.RealmModel;
@ -23,6 +24,7 @@ import org.picketlink.idm.model.User;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import java.io.Serializable;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
@ -38,6 +40,9 @@ import java.util.concurrent.atomic.AtomicLong;
*/
public class RealmManager {
private static AtomicLong counter = new AtomicLong(1);
public static final String RESOURCE_ROLE = "KEYCLOAK_RESOURCE";
public static final String IDENTITY_REQUESTER_ROLE = "KEYCLOAK_IDENTITY_REQUESTER";
public static final String WILDCARD_ROLE = "*";
public static String generateId() {
return counter.getAndIncrement() + "-" + System.currentTimeMillis();
@ -71,7 +76,9 @@ public class RealmManager {
SimpleAgent agent = new SimpleAgent(RealmModel.REALM_AGENT_ID);
idm.add(agent);
RealmModel realm = new RealmModel(newRealm, identitySession);
idm.add(new SimpleRole("*"));
idm.add(new SimpleRole(WILDCARD_ROLE));
idm.add(new SimpleRole(RESOURCE_ROLE));
idm.add(new SimpleRole(IDENTITY_REQUESTER_ROLE));
return realm;
}
@ -145,8 +152,9 @@ public class RealmManager {
}
if (rep.getRoles() != null) {
for (String roleString : rep.getRoles()) {
SimpleRole role = new SimpleRole(roleString.trim());
for (RoleRepresentation roleRep : rep.getRoles()) {
SimpleRole role = new SimpleRole(roleRep.getName());
if (roleRep.getDescription() != null) role.setAttribute(new Attribute<String>("description", roleRep.getDescription()));
newRealm.getIdm().add(role);
}
}
@ -186,6 +194,7 @@ public class RealmManager {
}
protected void createResources(RealmRepresentation rep, RealmModel realm, Map<String, User> userMap) {
Role loginRole = realm.getIdm().getRole(RealmManager.RESOURCE_ROLE);
for (ResourceRepresentation resourceRep : rep.getResources()) {
ResourceModel resource = realm.addResource(resourceRep.getName());
resource.setManagementUrl(resourceRep.getAdminUrl());
@ -202,11 +211,13 @@ public class RealmManager {
}
}
userMap.put(resourceUser.getLoginName(), resourceUser);
realm.getIdm().grantRole(resourceUser, loginRole);
if (resourceRep.getRoles() != null) {
for (String roleString : resourceRep.getRoles()) {
SimpleRole role = new SimpleRole(roleString.trim());
for (RoleRepresentation roleRep : resourceRep.getRoles()) {
SimpleRole role = new SimpleRole(roleRep.getName());
if (roleRep.getDescription() != null) role.setAttribute(new Attribute<String>("description", roleRep.getDescription()));
resource.getIdm().add(role);
}
}

View file

@ -3,19 +3,16 @@ package org.keycloak.services.managers;
import org.jboss.resteasy.jose.Base64Url;
import org.jboss.resteasy.jose.jws.JWSBuilder;
import org.jboss.resteasy.jwt.JsonSerialization;
import org.jboss.resteasy.spi.HttpResponse;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.representations.SkeletonKeyScope;
import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.ResourceModel;
import org.keycloak.services.resources.RealmsResource;
import org.picketlink.idm.model.Role;
import org.picketlink.idm.model.User;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@ -40,6 +37,9 @@ public class TokenManager {
accessCodeMap.clear();
}
public AccessCodeEntry getAccessCode(String key) {
return accessCodeMap.get(key);
}
public AccessCodeEntry pullAccessCode(String key) {
return accessCodeMap.remove(key);
@ -54,16 +54,59 @@ public class TokenManager {
return cookie;
}
public String createAccessCode(String scopeParam, RealmModel realm, User client, User user)
{
SkeletonKeyToken token = null;
if (scopeParam != null) token = createScopedToken(scopeParam, realm, client, user);
else token = createUnscopedToken(realm, client, user);
public AccessCodeEntry createAccessCode(String scopeParam, String state, String redirect, RealmModel realm, User client, User user) {
AccessCodeEntry code = new AccessCodeEntry();
SkeletonKeyScope scopeMap = null;
if (scopeParam != null) scopeMap = decodeScope(scopeParam);
List<Role> realmRolesRequested = code.getRealmRolesRequested();
MultivaluedMap<String, Role> resourceRolesRequested = code.getResourceRolesRequested();
Set<String> realmMapping = realm.getRoleMappings(user);
if (realmMapping != null && realmMapping.size() > 0 && (scopeMap == null || scopeMap.containsKey("realm"))) {
Set<String> scope = realm.getScope(client);
if (scope.size() > 0) {
Set<String> scopeRequest = null;
if (scopeMap != null) {
scopeRequest.addAll(scopeMap.get("realm"));
if (scopeRequest.contains(RealmManager.WILDCARD_ROLE)) scopeRequest = null;
}
for (String role : realmMapping) {
if (
(scopeRequest == null || scopeRequest.contains(role)) &&
(scope.contains("*") || scope.contains(role))
)
realmRolesRequested.add(realm.getIdm().getRole(role));
}
}
}
for (ResourceModel resource : realm.getResources()) {
Set<String> mapping = resource.getRoleMappings(user);
if (mapping != null && mapping.size() > 0 && (scopeMap == null || scopeMap.containsKey(resource.getName()))) {
Set<String> scope = resource.getScope(client);
if (scope.size() > 0) {
Set<String> scopeRequest = null;
if (scopeMap != null) {
scopeRequest.addAll(scopeMap.get(resource.getName()));
if (scopeRequest.contains(RealmManager.WILDCARD_ROLE)) scopeRequest = null;
}
for (String role : mapping) {
if (
(scopeRequest == null || scopeRequest.contains(role)) &&
(scope.contains("*") || scope.contains(role))
)
resourceRolesRequested.add(resource.getName(), resource.getIdm().getRole(role));
}
}
}
}
createToken(code, realm, client, user);
code.setExpiration((System.currentTimeMillis() / 1000) + realm.getAccessCodeLifespan());
code.setToken(token);
code.setClient(client);
code.setUser(user);
code.setState(state);
code.setRedirectUri(redirect);
accessCodeMap.put(code.getId(), code);
String accessCode = null;
try {
@ -71,30 +114,8 @@ public class TokenManager {
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return accessCode;
}
public SkeletonKeyToken createScopedToken(SkeletonKeyScope scope, RealmModel realm, User client, User user) {
SkeletonKeyToken token = initToken(realm, client, user);
Map<String, ResourceModel> resourceMap = realm.getResourceMap();
for (String res : scope.keySet()) {
ResourceModel resource = resourceMap.get(res);
Set<String> scopeMapping = resource.getScope(client);
Set<String> roleMapping = resource.getRoleMappings(user);
SkeletonKeyToken.Access access = token.addAccess(resource.getName());
for (String role : scope.get(res)) {
if (!scopeMapping.contains("*") && !scopeMapping.contains(role)) {
throw new ForbiddenException(Response.status(403).entity("<h1>Security Alert</h1><p>Known client not authorized for the requested scope.</p>").type("text/html").build());
}
if (!roleMapping.contains(role)) {
throw new ForbiddenException(Response.status(403).entity("<h1>Security Alert</h1><p>Known client not authorized for the requested scope.</p>").type("text/html").build());
}
access.addRole(role);
}
}
return token;
code.setCode(accessCode);
return code;
}
protected SkeletonKeyToken initToken(RealmModel realm, User client, User user) {
@ -110,38 +131,29 @@ public class TokenManager {
return token;
}
public SkeletonKeyToken createScopedToken(String scopeParam, RealmModel realm, User client, User user) {
SkeletonKeyScope scope = decodeScope(scopeParam);
return createScopedToken(scope, realm, client, user);
}
public SkeletonKeyToken createUnscopedToken(RealmModel realm, User client, User user) {
protected void createToken(AccessCodeEntry accessCodeEntry, RealmModel realm, User client, User user) {
SkeletonKeyToken token = initToken(realm, client, user);
Set<String> realmMapping = realm.getRoleMappings(user);
if (realmMapping != null && realmMapping.size() > 0) {
Set<String> scope = realm.getScope(client);
if (accessCodeEntry.getRealmRolesRequested().size() > 0) {
SkeletonKeyToken.Access access = new SkeletonKeyToken.Access();
for (String role : realmMapping) {
if (scope.contains("*") || scope.contains(role)) access.addRole(role);
for (Role role : accessCodeEntry.getRealmRolesRequested()) {
access.addRole(role.getName());
}
token.setRealmAccess(access);
}
List<ResourceModel> resources = realm.getResources();
for (ResourceModel resource : resources) {
Set<String> scope = resource.getScope(client);
Set<String> mapping = resource.getRoleMappings(user);
if (mapping.size() == 0 || scope.size() == 0) continue;
SkeletonKeyToken.Access access = token.addAccess(resource.getName())
.verifyCaller(resource.isSurrogateAuthRequired());
for (String role : mapping) {
if (scope.contains("*") || scope.contains(role)) access.addRole(role);
if (accessCodeEntry.getResourceRolesRequested().size() > 0) {
Map<String, ResourceModel> resourceMap = realm.getResourceMap();
for (String resourceName : accessCodeEntry.getResourceRolesRequested().keySet()) {
ResourceModel resource = resourceMap.get(resourceName);
SkeletonKeyToken.Access access = token.addAccess(resourceName).verifyCaller(resource.isSurrogateAuthRequired());
for (Role role : accessCodeEntry.getResourceRolesRequested().get(resourceName)) {
access.addRole(role.getName());
}
}
}
return token;
accessCodeEntry.setToken(token);
}
public String encodeScope(SkeletonKeyScope scope) {

View file

@ -38,6 +38,8 @@ import java.util.Map;
import java.util.Set;
/**
* Meant to be a per-request object
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@ -57,6 +59,7 @@ public class RealmModel {
protected IdentitySession identitySession;
protected volatile transient PublicKey publicKey;
protected volatile transient PrivateKey privateKey;
protected IdentityManager idm;
public RealmModel(Realm realm, IdentitySession session) {
this.realm = realm;
@ -65,7 +68,8 @@ public class RealmModel {
}
public IdentityManager getIdm() {
return identitySession.createIdentityManager(realm);
if (idm == null) idm = identitySession.createIdentityManager(realm);
return idm;
}
public void updateRealm() {

View file

@ -25,6 +25,7 @@ public class ResourceModel {
protected ResourceRelationship agent;
protected RealmModel realm;
protected IdentitySession identitySession;
protected IdentityManager idm;
public ResourceModel(Tier tier, ResourceRelationship agent, RealmModel realm, IdentitySession session) {
this.tier = tier;
@ -34,7 +35,8 @@ public class ResourceModel {
}
public IdentityManager getIdm() {
return identitySession.createIdentityManager(tier);
if (idm == null) idm = identitySession.createIdentityManager(tier);
return idm;
}
public void updateResource() {

View file

@ -1,24 +1,23 @@
package org.keycloak.services.resources;
import org.jboss.resteasy.jose.Base64Url;
import org.jboss.resteasy.jose.jws.JWSBuilder;
import org.jboss.resteasy.jose.jws.JWSInput;
import org.jboss.resteasy.jose.jws.crypto.RSAProvider;
import org.jboss.resteasy.jwt.JsonSerialization;
import org.jboss.resteasy.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.SkeletonKeyScope;
import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.services.JspRequestParameters;
import org.keycloak.services.managers.AccessCodeEntry;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.managers.TokenManager;
import org.keycloak.services.models.RealmModel;
import org.keycloak.services.models.RequiredCredentialModel;
import org.keycloak.services.models.ResourceModel;
import org.picketlink.idm.IdentitySession;
import org.picketlink.idm.model.Role;
import org.picketlink.idm.model.User;
import javax.ws.rs.Consumes;
@ -39,9 +38,7 @@ import javax.ws.rs.core.UriInfo;
import javax.ws.rs.ext.Providers;
import java.security.PrivateKey;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -64,10 +61,13 @@ public class TokenService {
protected IdentitySession identitySession;
@Context
HttpRequest request;
@Context
HttpResponse response;
protected String securityFailurePath = "/securityFailure.jsp";
protected String loginFormPath = "/loginForm.jsp";
protected String oauthFormPath = "/oauthGrantForm.jsp";
protected RealmModel realm;
protected TokenManager tokenManager;
@ -108,6 +108,10 @@ public class TokenService {
return tokenServiceBaseUrl(uriInfo).path(TokenService.class, "processLogin");
}
public static UriBuilder processOAuthUrl(UriInfo uriInfo) {
return tokenServiceBaseUrl(uriInfo).path(TokenService.class, "processOAuth");
}
@Path("grants/identity-token")
@POST
@ -209,16 +213,37 @@ public class TokenService {
return null;
}
return redirectAccessCode(scopeParam, state, redirect, client, user);
return processAccessCode(scopeParam, state, redirect, client, user);
}
protected Response redirectAccessCode(String scopeParam, String state, String redirect, User client, User user) {
String accessCode = tokenManager.createAccessCode(scopeParam, realm, client, user);
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", accessCode);
protected Response processAccessCode(String scopeParam, String state, String redirect, User client, User user) {
Role resourceRole = realm.getIdm().getRole(RealmManager.RESOURCE_ROLE);
Role identityRequestRole = realm.getIdm().getRole(RealmManager.IDENTITY_REQUESTER_ROLE);
boolean isResource = realm.getIdm().hasRole(client, resourceRole);
if (!isResource && !realm.getIdm().hasRole(client, identityRequestRole)) {
securityFailureForward("Login requester not allowed to request login.");
identitySession.close();
return null;
}
AccessCodeEntry accessCode = tokenManager.createAccessCode(scopeParam, state, redirect, realm, client, user);
logger.info("processAccessCode: isResource: " + isResource);
logger.info("processAccessCode: go to oauth page?: " + (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)));
if (!isResource && (accessCode.getRealmRolesRequested().size() > 0 || accessCode.getResourceRolesRequested().size() > 0)) {
oauthGrantPage(accessCode, client);
identitySession.close();
return null;
}
return redirectAccessCode(accessCode, state, redirect);
}
protected Response redirectAccessCode(AccessCodeEntry accessCode, String state, String redirect) {
String code = accessCode.getCode();
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("code", code);
logger.info("redirectAccessCode: state: " + state);
if (state != null) redirectUri.queryParam("state", state);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
if (realm.isCookieLoginAllowed()) {
location.cookie(tokenManager.createLoginCookie(realm, user, uriInfo));
location.cookie(tokenManager.createLoginCookie(realm, accessCode.getUser(), uriInfo));
}
return location.build();
}
@ -390,11 +415,20 @@ public class TokenService {
return null;
}
Role resourceRole = realm.getIdm().getRole(RealmManager.RESOURCE_ROLE);
Role identityRequestRole = realm.getIdm().getRole(RealmManager.IDENTITY_REQUESTER_ROLE);
boolean isResource = realm.getIdm().hasRole(client, resourceRole);
if (!isResource && !realm.getIdm().hasRole(client, identityRequestRole)) {
securityFailureForward("Login requester not allowed to request login.");
identitySession.close();
return null;
}
User user = authManager.authenticateIdentityCookie(realm, uriInfo, headers);
if (user != null) {
return redirectAccessCode(scopeParam, state, redirect, client, user);
logger.info(user.getLoginName() + " already logged in.");
return processAccessCode(scopeParam, state, redirect, client, user);
}
// todo make sure client is allowed to request a login
forwardToLoginForm(redirect, clientId, scopeParam, state);
return null;
@ -415,117 +449,56 @@ public class TokenService {
return Response.status(302).location(UriBuilder.fromUri(redirectUri).build()).build();
}
private Response loginForm(String validationError, String redirect, String clientId, String scopeParam, String state, RealmModel realm, User client) {
StringBuffer html = new StringBuffer();
if (scopeParam != null) {
html.append("<h1>Grant Request For ").append(realm.getName()).append(" Realm</h1>");
if (validationError != null) {
try {
Thread.sleep(1000); // put in a delay
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
html.append("<p/><p><b>").append(validationError).append("</b></p>");
}
html.append("<p>A Third Party is requesting access to the following resources</p>");
html.append("<table>");
SkeletonKeyScope scope = tokenManager.decodeScope(scopeParam);
Map<String, ResourceModel> resourceMap = realm.getResourceMap();
for (String res : scope.keySet()) {
ResourceModel resource = resourceMap.get(res);
html.append("<tr><td><b>Resource: </b>").append(resource.getName()).append("</td><td><b>Roles:</b>");
Set<String> scopeMapping = resource.getScope(client);
for (String role : scope.get(res)) {
html.append(" ").append(role);
if (!scopeMapping.contains("*") && !scopeMapping.contains(role)) {
return Response.ok("<h1>Security Alert</h1><p>Known client not authorized for the requested scope.</p>").type("text/html").build();
}
}
html.append("</td></tr>");
}
html.append("</table><p>To Authorize, please login below</p>");
} else {
Set<String> scopeMapping = realm.getScope(client);
if (scopeMapping.contains("*")) {
html.append("<h1>Login For ").append(realm.getName()).append(" Realm</h1>");
if (validationError != null) {
try {
Thread.sleep(1000); // put in a delay
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
html.append("<p/><p><b>").append(validationError).append("</b></p>");
}
} else {
html.append("<h1>Grant Request For ").append(realm.getName()).append(" Realm</h1>");
if (validationError != null) {
try {
Thread.sleep(1000); // put in a delay
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
html.append("<p/><p><b>").append(validationError).append("</b></p>");
}
SkeletonKeyScope scope = new SkeletonKeyScope();
List<ResourceModel> resources = realm.getResources();
boolean found = false;
for (ResourceModel resource : resources) {
Set<String> resourceScope = resource.getScope(client);
if (resourceScope == null) continue;
if (resourceScope.size() == 0) continue;
if (!found) {
found = true;
html.append("<p>A Third Party is requesting access to the following resources</p>");
html.append("<table>");
}
html.append("<tr><td><b>Resource: </b>").append(resource.getName()).append("</td><td><b>Roles:</b>");
// todo add description of role
for (String role : resourceScope) {
html.append(" ").append(role);
scope.add(resource.getName(), role);
}
}
if (!found) {
return Response.ok("<h1>Security Alert</h1><p>Known client not authorized to access this realm.</p>").type("text/html").build();
}
html.append("</table>");
try {
String json = JsonSerialization.toString(scope, false);
scopeParam = Base64Url.encode(json.getBytes("UTF-8"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Path("oauth/grant")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response processOAuth(MultivaluedMap<String, String> formData) {
String code = formData.getFirst("code");
JWSInput input = new JWSInput(code, providers);
boolean verifiedCode = false;
try {
verifiedCode = RSAProvider.verify(input, realm.getPublicKey());
} catch (Exception ignored) {
logger.debug("Failed to verify signature", ignored);
}
if (!verifiedCode) {
securityFailureForward("Illegal access code.");
identitySession.close();
return null;
}
String key = input.readContent(String.class);
AccessCodeEntry accessCodeEntry = tokenManager.getAccessCode(key);
if (accessCodeEntry == null) {
securityFailureForward("Unknown access code.");
identitySession.close();
return null;
}
UriBuilder formActionUri = processLoginUrl(uriInfo);
String action = formActionUri.build(realm.getId()).toString();
html.append("<form action=\"").append(action).append("\" method=\"POST\">");
html.append("Username: <input type=\"text\" name=\"username\" size=\"20\"><br>");
String redirect = accessCodeEntry.getRedirectUri();
String state = accessCodeEntry.getState();
for (RequiredCredentialModel credential : realm.getRequiredCredentials()) {
if (!credential.isInput()) continue;
html.append(credential.getType()).append(": ");
if (credential.isSecret()) {
html.append("<input type=\"password\" name=\"").append(credential.getType()).append("\" size=\"20\"><br>");
if (formData.containsKey("cancel")) {
return redirectAccessDenied(redirect, state);
}
} else {
html.append("<input type=\"text\" name=\"").append(credential.getType()).append("\" size=\"20\"><br>");
}
}
html.append("<input type=\"hidden\" name=\"client_id\" value=\"").append(clientId).append("\">");
if (scopeParam != null) {
html.append("<input type=\"hidden\" name=\"scope\" value=\"").append(scopeParam).append("\">");
}
if (state != null) html.append("<input type=\"hidden\" name=\"state\" value=\"").append(state).append("\">");
html.append("<input type=\"hidden\" name=\"redirect_uri\" value=\"").append(redirect).append("\">");
html.append("<input type=\"submit\" value=\"");
if (scopeParam == null) html.append("Login");
else html.append("Grant Access");
html.append("\">");
html.append("</form>");
return Response.ok(html.toString()).type("text/html").build();
return redirectAccessCode(accessCodeEntry, state, redirect);
}
protected Response redirectAccessDenied(String redirect, String state) {
UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam("error", "access_denied");
if (state != null) redirectUri.queryParam("state", state);
Response.ResponseBuilder location = Response.status(302).location(redirectUri.build());
return location.build();
}
protected void oauthGrantPage(AccessCodeEntry accessCode, User client) {
request.setAttribute("realmRolesRequested", accessCode.getRealmRolesRequested());
request.setAttribute("resourceRolesRequested", accessCode.getResourceRolesRequested());
request.setAttribute("client", client);
request.setAttribute("action", processOAuthUrl(uriInfo).build(realm.getId()).toString());
request.setAttribute("code", accessCode.getCode());
request.forward(oauthFormPath);
}
}

View file

@ -166,7 +166,7 @@ public class AdapterTest {
idm.add(new SimpleRole("admin"));
idm.add(new SimpleRole("user"));
List<Role> roles = realmModel.getRoles();
Assert.assertEquals(3, roles.size());
Assert.assertEquals(5, roles.size());
SimpleUser user = new SimpleUser("bburke");
idm.add(user);
Role role = idm.getRole("user");

View file

@ -62,7 +62,10 @@
"resources" : [
{
"name" : "Application",
"roles" : ["admin", "user"],
"roles" : [
{ "name" : "admin" },
{ "name" : "user" }
],
"roleMappings" : [
{
"username" : "wburke",
@ -82,7 +85,10 @@
},
{
"name" : "OtherApp",
"roles" : ["admin", "user"],
"roles" : [
{ "name" : "admin" },
{ "name" : "user" }
],
"roleMappings" : [
{
"username" : "wburke",