commit
1738831d27
27 changed files with 765 additions and 205 deletions
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
34
core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
Executable file
34
core/src/main/java/org/keycloak/representations/idm/RoleRepresentation.java
Executable 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;
|
||||
}
|
||||
}
|
|
@ -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
61
examples/as7-eap-demo/README.md
Executable 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
43
examples/as7-eap-demo/pom.xml
Executable 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>
|
|
@ -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" : [
|
||||
|
|
|
@ -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">
|
||||
|
||||
|
|
82
examples/as7-eap-demo/server/src/main/webapp/oauthGrantForm.jsp
Executable file
82
examples/as7-eap-demo/server/src/main/webapp/oauthGrantForm.jsp
Executable 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
61
examples/as7-eap-demo/third-party/pom.xml
vendored
Executable 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>
|
69
examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/Bootstrap.java
vendored
Executable file
69
examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/Bootstrap.java
vendored
Executable 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();
|
||||
}
|
||||
}
|
69
examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java
vendored
Executable file
69
examples/as7-eap-demo/third-party/src/main/java/org/jboss/resteasy/example/oauth/ProductDatabaseClient.java
vendored
Executable 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();
|
||||
}
|
||||
}
|
||||
}
|
9
examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
vendored
Executable file
9
examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
vendored
Executable 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>
|
20
examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/web.xml
vendored
Executable file
20
examples/as7-eap-demo/third-party/src/main/webapp/WEB-INF/web.xml
vendored
Executable 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>
|
6
examples/as7-eap-demo/third-party/src/main/webapp/index.html
vendored
Normal file
6
examples/as7-eap-demo/third-party/src/main/webapp/index.html
vendored
Normal 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>
|
21
examples/as7-eap-demo/third-party/src/main/webapp/pull_data.jsp
vendored
Normal file
21
examples/as7-eap-demo/third-party/src/main/webapp/pull_data.jsp
vendored
Normal 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>
|
3
examples/as7-eap-demo/third-party/src/main/webapp/redirect.jsp
vendored
Normal file
3
examples/as7-eap-demo/third-party/src/main/webapp/redirect.jsp
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
<%
|
||||
org.jboss.resteasy.example.oauth.ProductDatabaseClient.redirect(request, response);
|
||||
%>
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue