wildfly adapter

This commit is contained in:
Bill Burke 2013-11-15 14:15:33 -05:00
parent 2861ea0e96
commit 965bc6dccb
73 changed files with 2738 additions and 307 deletions

View file

@ -2,6 +2,7 @@ package org.keycloak;
import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.keycloak.adapters.config.ManagedResourceConfig;
import javax.ws.rs.core.Form; import javax.ws.rs.core.Form;
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
@ -19,6 +20,12 @@ public class RealmConfiguration {
protected boolean sslRequired = true; protected boolean sslRequired = true;
protected String stateCookieName = "OAuth_Token_Request_State"; protected String stateCookieName = "OAuth_Token_Request_State";
public RealmConfiguration() {
}
public RealmConfiguration(ManagedResourceConfig config) {
}
public ResourceMetadata getMetadata() { public ResourceMetadata getMetadata() {
return metadata; return metadata;
} }

View file

@ -1,4 +1,4 @@
package org.keycloak.adapters.as7.config; package org.keycloak.adapters.config;
import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.annotate.JsonPropertyOrder; import org.codehaus.jackson.annotate.JsonPropertyOrder;
@ -59,6 +59,8 @@ public class ManagedResourceConfig {
protected String corsAllowedMethods; protected String corsAllowedMethods;
@JsonProperty("expose-token") @JsonProperty("expose-token")
protected boolean exposeToken; protected boolean exposeToken;
@JsonProperty("bearer-only")
protected boolean bearerOnly;
public boolean isUseResourceRoleMappings() { public boolean isUseResourceRoleMappings() {
return useResourceRoleMappings; return useResourceRoleMappings;
@ -239,4 +241,12 @@ public class ManagedResourceConfig {
public void setExposeToken(boolean exposeToken) { public void setExposeToken(boolean exposeToken) {
this.exposeToken = exposeToken; this.exposeToken = exposeToken;
} }
public boolean isBearerOnly() {
return bearerOnly;
}
public void setBearerOnly(boolean bearerOnly) {
this.bearerOnly = bearerOnly;
}
} }

View file

@ -1,60 +1,57 @@
package org.keycloak.adapters.as7.config; package org.keycloak.adapters.config;
import org.apache.catalina.Context;
import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize;
import org.jboss.logging.Logger;
import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.plugins.providers.RegisterBuiltin; import org.jboss.resteasy.plugins.providers.RegisterBuiltin;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.EnvUtil; import org.keycloak.EnvUtil;
import org.keycloak.PemUtils; import org.keycloak.PemUtils;
import org.keycloak.RealmConfiguration;
import org.keycloak.ResourceMetadata; import org.keycloak.ResourceMetadata;
import org.keycloak.representations.idm.PublishedRealmRepresentation; import org.keycloak.representations.idm.PublishedRealmRepresentation;
import javax.ws.rs.core.UriBuilder;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.KeyStore; import java.security.KeyStore;
import java.security.PublicKey; import java.security.PublicKey;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ManagedResourceConfigLoader { public class ManagedResourceConfigLoader {
static final Logger log = Logger.getLogger(ManagedResourceConfigLoader.class);
protected ManagedResourceConfig remoteSkeletonKeyConfig; protected ManagedResourceConfig remoteSkeletonKeyConfig;
protected ResourceMetadata resourceMetadata; protected ResourceMetadata resourceMetadata;
protected KeyStore clientCertKeystore; protected KeyStore clientCertKeystore;
protected KeyStore truststore; protected KeyStore truststore;
protected ResteasyClient client; protected ResteasyClient client;
protected RealmConfiguration realmConfiguration;
public ManagedResourceConfigLoader(Context context) { public ManagedResourceConfigLoader() {
ObjectMapper mapper = new ObjectMapper(); }
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT);
InputStream is = null; public ManagedResourceConfigLoader(InputStream is) {
String path = context.getServletContext().getInitParameter("keycloak.config.file"); loadConfig(is);
if (path == null) { }
is = context.getServletContext().getResourceAsStream("/WEB-INF/resteasy-oauth.json");
} else {
try { public static KeyStore loadKeyStore(String filename, String password) throws Exception {
is = new FileInputStream(path); KeyStore trustStore = KeyStore.getInstance(KeyStore
} catch (FileNotFoundException e) { .getDefaultType());
throw new RuntimeException(e); File truststoreFile = new File(filename);
} FileInputStream trustStream = new FileInputStream(truststoreFile);
} trustStore.load(trustStream, password.toCharArray());
remoteSkeletonKeyConfig = null; trustStream.close();
try { return trustStore;
remoteSkeletonKeyConfig = mapper.readValue(is, ManagedResourceConfig.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
} }
public void init(boolean setupClient) { public void init(boolean setupClient) {
String truststorePath = remoteSkeletonKeyConfig.getTruststore(); String truststorePath = remoteSkeletonKeyConfig.getTruststore();
if (truststorePath != null) { if (truststorePath != null) {
truststorePath = EnvUtil.replace(truststorePath); truststorePath = EnvUtil.replace(truststorePath);
@ -127,6 +124,31 @@ public class ManagedResourceConfigLoader {
resourceMetadata.setClientKeyPassword(clientKeyPassword); resourceMetadata.setClientKeyPassword(clientKeyPassword);
resourceMetadata.setTruststore(this.truststore); resourceMetadata.setTruststore(this.truststore);
if (!setupClient || remoteSkeletonKeyConfig.isBearerOnly()) return;
realmConfiguration = new RealmConfiguration();
String authUrl = remoteSkeletonKeyConfig.getAuthUrl();
if (authUrl == null) {
throw new RuntimeException("You must specify auth-url");
}
String tokenUrl = remoteSkeletonKeyConfig.getCodeUrl();
if (tokenUrl == null) {
throw new RuntimeException("You mut specify code-url");
}
realmConfiguration.setMetadata(resourceMetadata);
realmConfiguration.setSslRequired(!remoteSkeletonKeyConfig.isSslNotRequired());
for (Map.Entry<String, String> entry : getRemoteSkeletonKeyConfig().getCredentials().entrySet()) {
realmConfiguration.getResourceCredentials().param(entry.getKey(), entry.getValue());
}
ResteasyClient client = getClient();
realmConfiguration.setClient(client);
realmConfiguration.setAuthUrl(UriBuilder.fromUri(authUrl).queryParam("client_id", resourceMetadata.getResourceName()));
realmConfiguration.setCodeUrl(client.target(tokenUrl));
} }
protected void initClient() { protected void initClient() {
@ -158,16 +180,6 @@ public class ManagedResourceConfigLoader {
client = builder.build(); client = builder.build();
} }
public 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;
}
public ManagedResourceConfig getRemoteSkeletonKeyConfig() { public ManagedResourceConfig getRemoteSkeletonKeyConfig() {
return remoteSkeletonKeyConfig; return remoteSkeletonKeyConfig;
} }
@ -187,4 +199,19 @@ public class ManagedResourceConfigLoader {
public KeyStore getTruststore() { public KeyStore getTruststore() {
return truststore; return truststore;
} }
}
public RealmConfiguration getRealmConfiguration() {
return realmConfiguration;
}
protected void loadConfig(InputStream is) {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_DEFAULT);
remoteSkeletonKeyConfig = null;
try {
remoteSkeletonKeyConfig = mapper.readValue(is, ManagedResourceConfig.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

79
examples/wildfly-demo/README.md Executable file
View file

@ -0,0 +1,79 @@
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.4 within JBoss as described [here](http://docs.jboss.org/resteasy/docs/3.0.4.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.
Admin Console
==========================
1. Register or login
You'll have to first register and create an account
Login:
[http://localhost:8080/auth-server/rest/saas/login](http://localhost:8080/auth-server/rest/saas/login)
Register:
[http://localhost:8080/auth-server/rest/saas/registrations](http://localhost:8080/auth-server/rest/saas/registrations)
2. Next you'll be brought to the admin console. Click "New Realm" button and start doing stuff.

View file

@ -0,0 +1,73 @@
<?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.wildfly.demo</groupId>
<artifactId>customer-portal-example</artifactId>
<packaging>war</packaging>
<name>Customer Portal - Secured via Undertow</name>
<description/>
<repositories>
<repository>
<id>jboss</id>
<name>jboss repo</name>
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<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>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-undertow-adapter</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<finalName>customer-portal</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,36 @@
package org.jboss.resteasy.example.oauth;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.SkeletonKeySession;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class CustomerDatabaseClient
{
public static List<String> getCustomers(HttpServletRequest request)
{
SkeletonKeySession session = (SkeletonKeySession)request.getAttribute(SkeletonKeySession.class.getName());
ResteasyClient client = new ResteasyClientBuilder()
.trustStore(session.getMetadata().getTruststore())
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build();
try
{
Response response = client.target("http://localhost:8080/database/customers").request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + session.getTokenString()).get();
return response.readEntity(new GenericType<List<String>>(){});
}
finally
{
client.close();
}
}
}

View file

@ -0,0 +1,11 @@
<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.bouncycastle"/>
<module name="org.jboss.resteasy.resteasy-jaxrs" services="import"/>
<module name="org.jboss.resteasy.resteasy-jackson-provider" services="import"/>
<module name="org.jboss.resteasy.jose-jwt" />
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,5 @@
<jboss-web>
<valve>
<class-name>org.keycloak.adapters.as7.OAuthManagedResourceValve</class-name>
</valve>
</jboss-web>

View file

@ -0,0 +1,12 @@
{
"realm" : "demo",
"resource" : "customer-portal",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-url" : "http://localhost:8080/auth-server/rest/realms/demo/tokens/login",
"code-url" : "http://localhost:8080/auth-server/rest/realms/demo/tokens/access/codes",
"ssl-not-required" : true,
"expose-token" : true,
"credentials" : {
"password" : "password"
}
}

View file

@ -0,0 +1,49 @@
<?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">
<module-name>customer-portal</module-name>
<security-constraint>
<web-resource-collection>
<web-resource-name>Admins</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Customers</web-resource-name>
<url-pattern>/customers/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<!--
<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> -->
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>commerce</realm-name>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>

View file

@ -0,0 +1,11 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<head>
<title>Customer Admin Iterface</title>
</head>
<body bgcolor="#E3F6CE">
<h1>Customer Admin Interface</h1>
User <b><%=request.getUserPrincipal().getName()%></b> made this request.
</body>
</html>

View file

@ -0,0 +1,38 @@
<!doctype html>
<html lang="en">
<body>
<script type="text/javascript">
console.log('here!!!!!');
var xhr1 = new XMLHttpRequest();
xhr1.open('GET', '/customer-portal/K_QUERY_BEARER_TOKEN');
xhr1.onreadystatechange = function () {
console.log('got here');
if (this.status == 200 && this.readyState == 4) {
var token = this.responseText;
console.log('Access token: ' + token);
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8080/database/customers');
xhr.withCredentials = true;
xhr.setRequestHeader('Authorization', 'Bearer ' + token);
xhr.onreadystatechange = function () {
console.log('got auth success');
if (this.status == 200 && this.readyState == 4) {
console.log('db response: ' + this.responseText);
} else if (this.status != 200) {
console.log('there was an error:' + this.status);
}
};
xhr.send();
} else if (this.status != 200) {
console.log('there was an error on get bearer token:' + this.status);
}
};
xhr1.send();
</script>
</body>
</html>

View file

@ -0,0 +1,28 @@
<%@ page import="javax.ws.rs.core.UriBuilder" language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<head>
<title>Customer View Page</title>
</head>
<body bgcolor="#E3F6CE">
<%
String logoutUri = UriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/tokens/logout")
.queryParam("redirect_uri", "http://localhost:8080/customer-portal").build().toString();
%>
<p>Goto: <a href="http://localhost:8080/product-portal">products</a> | <a href="<%=logoutUri%>">logout</a></p>
User <b><%=request.getUserPrincipal().getName()%></b> made this request.
<h2>Customer Listing</h2>
<%
java.util.List<String> list = org.jboss.resteasy.example.oauth.CustomerDatabaseClient.getCustomers(request);
for (String cust : list)
{
out.print("<p>");
out.print(cust);
out.println("</p>");
}
%>
<br><br>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title></title>
</head>
<body bgcolor="#E3F6CE">
<h1>Customer Portal</h1>
<p><a href="customers/view.jsp">Customer Listing</a></p>
<p><a href="admin/admin.html">Customer Admin Interface</a></p>
</body>
</html>

View file

@ -0,0 +1,62 @@
<?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.wildfly.demo</groupId>
<artifactId>database-service</artifactId>
<packaging>war</packaging>
<name>JAX-RS Database Service Using OAuth Bearer Tokens</name>
<description/>
<url>http://maven.apache.org</url>
<repositories>
<repository>
<id>jboss</id>
<name>jboss repo</name>
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<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>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-undertow-adapter</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<finalName>database</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>7.5.Final</version>
</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,26 @@
package org.jboss.resteasy.example.oauth;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Path("customers")
public class CustomerService
{
@GET
@Produces("application/json")
public List<String> getCustomers()
{
ArrayList<String> rtn = new ArrayList<String>();
rtn.add("Bill Burke");
rtn.add("Ron Sigal");
rtn.add("Weinan Li");
return rtn;
}
}

View file

@ -0,0 +1,13 @@
package org.jboss.resteasy.example.oauth;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@ApplicationPath("/")
public class DataApplication extends Application
{
}

View file

@ -0,0 +1,26 @@
package org.jboss.resteasy.example.oauth;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Path("products")
public class ProductService
{
@GET
@Produces("application/json")
public List<String> getProducts()
{
ArrayList<String> rtn = new ArrayList<String>();
rtn.add("iphone");
rtn.add("ipad");
rtn.add("ipod");
return rtn;
}
}

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.bouncycastle"/>
<module name="org.jboss.resteasy.jose-jwt" />
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,5 @@
<jboss-web>
<valve>
<class-name>org.keycloak.adapters.as7.BearerTokenAuthenticatorValve</class-name>
</valve>
</jboss-web>

View file

@ -0,0 +1,8 @@
{
"realm" : "demo",
"resource" : "database-service",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"enable-cors" : true,
"bearer-only" : true
}

View file

@ -0,0 +1,29 @@
<?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">
<module-name>database</module-name>
<security-constraint>
<web-resource-collection>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<!-- <user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint> -->
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>commerce</realm-name>
</login-config>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>

43
examples/wildfly-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>wildfly-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.5.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

@ -0,0 +1,73 @@
<?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.wildfly.demo</groupId>
<artifactId>product-portal-example</artifactId>
<packaging>war</packaging>
<name>Product Portal - Secured via Undertow</name>
<description/>
<repositories>
<repository>
<id>jboss</id>
<name>jboss repo</name>
<url>http://repository.jboss.org/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<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>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-undertow-adapter</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<finalName>product-portal</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>7.5.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,36 @@
package org.jboss.resteasy.example.oauth;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.keycloak.SkeletonKeySession;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ProductDatabaseClient
{
public static List<String> getProducts(HttpServletRequest request)
{
SkeletonKeySession session = (SkeletonKeySession)request.getAttribute(SkeletonKeySession.class.getName());
ResteasyClient client = new ResteasyClientBuilder()
.trustStore(session.getMetadata().getTruststore())
.hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build();
try
{
Response response = client.target("http://localhost:8080/database/products").request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + session.getTokenString()).get();
return response.readEntity(new GenericType<List<String>>(){});
}
finally
{
client.close();
}
}
}

View file

@ -0,0 +1,11 @@
<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.bouncycastle"/>
<module name="org.jboss.resteasy.resteasy-jaxrs" services="import"/>
<module name="org.jboss.resteasy.resteasy-jackson-provider" services="import"/>
<module name="org.jboss.resteasy.jose-jwt" />
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,5 @@
<jboss-web>
<valve>
<class-name>org.keycloak.adapters.as7.OAuthManagedResourceValve</class-name>
</valve>
</jboss-web>

View file

@ -0,0 +1,11 @@
{
"realm" : "demo",
"resource" : "product-portal",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-url" : "http://localhost:8080/auth-server/rest/realms/demo/tokens/login",
"code-url" : "http://localhost:8080/auth-server/rest/realms/demo/tokens/access/codes",
"ssl-not-required" : true,
"credentials" : {
"password" : "password"
}
}

View file

@ -0,0 +1,49 @@
<?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">
<module-name>product-portal</module-name>
<security-constraint>
<web-resource-collection>
<web-resource-name>Admins</web-resource-name>
<url-pattern>/admin/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>admin</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Products</web-resource-name>
<url-pattern>/products/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<!--
<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>
-->
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>commerce</realm-name>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>

View file

@ -0,0 +1,11 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<head>
<title>Product Admin Interface</title>
</head>
<body bgcolor="#F5F6CE">
<h1>Product Admin Interface</h1>
User <b><%=request.getUserPrincipal().getName()%></b> made this request.
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<title></title>
</head>
<body bgcolor="#F5F6CE">
<h1>Product Portal</h1>
<p><a href="products/view.jsp">Product Listing</a></p>
<p><a href="admin/admin.html">Admin Interface</a></p>
</body>
</html>

View file

@ -0,0 +1,28 @@
<%@ page import="javax.ws.rs.core.UriBuilder" language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<head>
<title>Product View Page</title>
</head>
<body bgcolor="#F5F6CE">
<%
String logoutUri = UriBuilder.fromUri("http://localhost:8080/auth-server/rest/realms/demo/tokens/logout")
.queryParam("redirect_uri", "http://localhost:8080/product-portal").build().toString();
%>
<p>Goto: <a href="http://localhost:8080/customer-portal">customers</a> | <a href="<%=logoutUri%>">logout</a></p>
User <b><%=request.getUserPrincipal().getName()%></b> made this request.
<h2>Product Listing</h2>
<%
java.util.List<String> list = org.jboss.resteasy.example.oauth.ProductDatabaseClient.getProducts(request);
for (String cust : list)
{
out.print("<p>");
out.print(cust);
out.println("</p>");
}
%>
<br><br>
</body>
</html>

View file

@ -0,0 +1,142 @@
<?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.wildfly.demo</groupId>
<artifactId>keycloak-server</artifactId>
<packaging>war</packaging>
<name>Keycloak Demo</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jose-jwt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-social-core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-social-google</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-social-twitter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-social-facebook</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-forms</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-ui</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-ui-styles</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>provided</scope>
<exclusions>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongo-java-driver</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>auth-server</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<version>7.5.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,59 @@
package org.keycloak.example.demo;
import org.jboss.resteasy.jwt.JsonSerialization;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.ApplianceBootstrap;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.KeycloakApplication;
import javax.servlet.ServletContext;
import javax.ws.rs.core.Context;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class DemoApplication extends KeycloakApplication {
public DemoApplication(@Context ServletContext servletContext) {
super(servletContext);
KeycloakSession session = factory.createSession();
session.getTransaction().begin();
ApplianceBootstrap bootstrap = new ApplianceBootstrap();
bootstrap.bootstrap(session);
install(new RealmManager(session));
session.getTransaction().commit();
}
public void install(RealmManager manager) {
RealmRepresentation rep = loadJson("META-INF/testrealm.json");
RealmModel realm = manager.createRealm("demo", rep.getRealm());
manager.importRealm(rep, realm);
}
public static RealmRepresentation loadJson(String path)
{
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
ByteArrayOutputStream os = new ByteArrayOutputStream();
int c;
try {
while ( (c = is.read()) != -1)
{
os.write(c);
}
byte[] bytes = os.toByteArray();
//System.out.println(new String(bytes));
return JsonSerialization.fromBytes(RealmRepresentation.class, bytes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -0,0 +1,31 @@
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">
<persistence-unit name="jpa-keycloak-identity-store" transaction-type="RESOURCE_LOCAL">
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<class>org.keycloak.models.jpa.entities.ApplicationEntity</class>
<class>org.keycloak.models.jpa.entities.ApplicationScopeMappingEntity</class>
<class>org.keycloak.models.jpa.entities.ApplicationUserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.CredentialEntity</class>
<class>org.keycloak.models.jpa.entities.OAuthClientEntity</class>
<class>org.keycloak.models.jpa.entities.RealmEntity</class>
<class>org.keycloak.models.jpa.entities.RealmScopeMappingEntity</class>
<class>org.keycloak.models.jpa.entities.RealmUserRoleMappingEntity</class>
<class>org.keycloak.models.jpa.entities.RequiredCredentialEntity</class>
<class>org.keycloak.models.jpa.entities.RoleEntity</class>
<class>org.keycloak.models.jpa.entities.SocialLinkEntity</class>
<class>org.keycloak.models.jpa.entities.UserEntity</class>
<class>org.keycloak.models.jpa.entities.UserRoleMappingEntity</class>
<exclude-unlisted-classes>true</exclude-unlisted-classes>
<properties>
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<property name="hibernate.show_sql" value="false" />
<property name="hibernate.format_sql" value="false" />
</properties>
</persistence-unit>
</persistence>

View file

@ -0,0 +1,91 @@
{
"realm": "demo",
"enabled": true,
"tokenLifespan": 300,
"accessCodeLifespan": 10,
"accessCodeLifespanUserAction": 600,
"sslNotRequired": true,
"cookieLoginAllowed": true,
"registrationAllowed": true,
"social": true,
"automaticRegistrationAfterSocialLogin": false,
"privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=",
"publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"requiredCredentials": [ "password" ],
"requiredApplicationCredentials": [ "password" ],
"requiredOAuthClientCredentials": [ "password" ],
"defaultRoles": [ "user" ],
"users" : [
{
"username" : "bburke@redhat.com",
"enabled": true,
"attributes" : {
"email" : "bburke@redhat.com"
},
"credentials" : [
{ "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"]
}
],
"applications": [
{
"name": "customer-portal",
"enabled": true,
"adminUrl": "http://localhost:8080/customer-portal/j_admin_request",
"useRealmMappings": true,
"credentials": [
{
"type": "password",
"value": "password"
}
]
},
{
"name": "product-portal",
"enabled": true,
"adminUrl": "http://localhost:8080/product-portal/j_admin_request",
"useRealmMappings": true,
"credentials": [
{
"type": "password",
"value": "password"
}
]
}
]
}

View file

@ -0,0 +1,10 @@
<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.jose-jwt"/>
<module name="org.jboss.resteasy.resteasy-crypto"/>
<module name="org.bouncycastle"/>
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,53 @@
<?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">
<module-name>auth-server</module-name>
<servlet>
<servlet-name>Resteasy</servlet-name>
<servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServlet30Dispatcher</servlet-class>
<init-param>
<param-name>javax.ws.rs.Application</param-name>
<param-value>org.keycloak.example.demo.DemoApplication</param-value>
</init-param>
<init-param>
<param-name>resteasy.servlet.mapping.prefix</param-name>
<param-value>/rest</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<listener>
<listener-class>org.keycloak.services.listeners.MongoRunnerListener</listener-class>
</listener>
<filter>
<filter-name>Keycloak Session Management</filter-name>
<filter-class>org.keycloak.services.filters.KeycloakSessionServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>Keycloak Session Management</filter-name>
<url-pattern>/rest/*</url-pattern>
</filter-mapping>
<servlet-mapping>
<servlet-name>Resteasy</servlet-name>
<url-pattern>/rest/*</url-pattern>
</servlet-mapping>
<!--
<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>

61
examples/wildfly-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.wildfly.demo</groupId>
<artifactId>oauth-client-example</artifactId>
<packaging>war</packaging>
<name>Simple OAuth Wildfly 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.5.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,23 @@
<?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">
<module-name>oauth-client</module-name>
<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

@ -8,7 +8,7 @@ import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase; import org.apache.catalina.valves.ValveBase;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.SkeletonKeySession; import org.keycloak.SkeletonKeySession;
import org.keycloak.adapters.as7.config.ManagedResourceConfig; import org.keycloak.adapters.config.ManagedResourceConfig;
import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.representations.SkeletonKeyToken;
import javax.management.ObjectName; import javax.management.ObjectName;
@ -66,6 +66,7 @@ public class AuthenticatedActionsValve extends ValveBase {
protected void queryBearerToken(Request request, Response response, SkeletonKeySession session) throws IOException, ServletException { protected void queryBearerToken(Request request, Response response, SkeletonKeySession session) throws IOException, ServletException {
log.debugv("queryBearerToken {0}",request.getRequestURI()); log.debugv("queryBearerToken {0}",request.getRequestURI());
if (abortTokenResponse(request, response, session)) return; if (abortTokenResponse(request, response, session)) return;
response.setStatus(200);
response.setContentType("text/plain"); response.setContentType("text/plain");
response.getOutputStream().write(session.getTokenString().getBytes()); response.getOutputStream().write(session.getTokenString().getBytes());
response.getOutputStream().flush(); response.getOutputStream().flush();

View file

@ -12,8 +12,9 @@ import org.apache.catalina.deploy.LoginConfig;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.ResourceMetadata; import org.keycloak.ResourceMetadata;
import org.keycloak.adapters.as7.config.ManagedResourceConfig; import org.keycloak.adapters.as7.config.CatalinaManagedResourceConfigLoader;
import org.keycloak.adapters.as7.config.ManagedResourceConfigLoader; import org.keycloak.adapters.config.ManagedResourceConfig;
import org.keycloak.adapters.config.ManagedResourceConfigLoader;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import javax.servlet.ServletException; import javax.servlet.ServletException;
@ -45,7 +46,7 @@ public class BearerTokenAuthenticatorValve extends AuthenticatorBase implements
} }
protected void init() { protected void init() {
ManagedResourceConfigLoader managedResourceConfigLoader = new ManagedResourceConfigLoader(context); ManagedResourceConfigLoader managedResourceConfigLoader = new CatalinaManagedResourceConfigLoader(context);
remoteSkeletonKeyConfig = managedResourceConfigLoader.getRemoteSkeletonKeyConfig(); remoteSkeletonKeyConfig = managedResourceConfigLoader.getRemoteSkeletonKeyConfig();
managedResourceConfigLoader.init(false); managedResourceConfigLoader.init(false);
resourceMetadata = managedResourceConfigLoader.getResourceMetadata(); resourceMetadata = managedResourceConfigLoader.getResourceMetadata();

View file

@ -3,7 +3,7 @@ package org.keycloak.adapters.as7;
import org.apache.catalina.connector.Request; import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response; import org.apache.catalina.connector.Response;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.adapters.as7.config.ManagedResourceConfig; import org.keycloak.adapters.config.ManagedResourceConfig;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>

View file

@ -22,8 +22,9 @@ import org.keycloak.RealmConfiguration;
import org.keycloak.ResourceMetadata; import org.keycloak.ResourceMetadata;
import org.keycloak.SkeletonKeyPrincipal; import org.keycloak.SkeletonKeyPrincipal;
import org.keycloak.SkeletonKeySession; import org.keycloak.SkeletonKeySession;
import org.keycloak.adapters.as7.config.ManagedResourceConfig; import org.keycloak.adapters.as7.config.CatalinaManagedResourceConfigLoader;
import org.keycloak.adapters.as7.config.ManagedResourceConfigLoader; import org.keycloak.adapters.config.ManagedResourceConfig;
import org.keycloak.adapters.config.ManagedResourceConfigLoader;
import org.keycloak.representations.SkeletonKeyToken; import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.representations.idm.admin.LogoutAction; import org.keycloak.representations.idm.admin.LogoutAction;
@ -66,32 +67,12 @@ public class OAuthManagedResourceValve extends FormAuthenticator implements Life
} }
protected void init() { protected void init() {
ManagedResourceConfigLoader managedResourceConfigLoader = new ManagedResourceConfigLoader(context); ManagedResourceConfigLoader managedResourceConfigLoader = new CatalinaManagedResourceConfigLoader(context);
managedResourceConfigLoader.init(true); managedResourceConfigLoader.init(true);
resourceMetadata = managedResourceConfigLoader.getResourceMetadata(); resourceMetadata = managedResourceConfigLoader.getResourceMetadata();
remoteSkeletonKeyConfig = managedResourceConfigLoader.getRemoteSkeletonKeyConfig(); remoteSkeletonKeyConfig = managedResourceConfigLoader.getRemoteSkeletonKeyConfig();
realmConfiguration = new RealmConfiguration(); realmConfiguration = managedResourceConfigLoader.getRealmConfiguration();
String authUrl = remoteSkeletonKeyConfig.getAuthUrl();
if (authUrl == null) {
throw new RuntimeException("You must specify auth-url");
}
String tokenUrl = remoteSkeletonKeyConfig.getCodeUrl();
if (tokenUrl == null) {
throw new RuntimeException("You mut specify code-url");
}
realmConfiguration.setMetadata(resourceMetadata);
realmConfiguration.setSslRequired(!remoteSkeletonKeyConfig.isSslNotRequired());
for (Map.Entry<String, String> entry : managedResourceConfigLoader.getRemoteSkeletonKeyConfig().getCredentials().entrySet()) {
realmConfiguration.getResourceCredentials().param(entry.getKey(), entry.getValue());
}
ResteasyClient client = managedResourceConfigLoader.getClient();
realmConfiguration.setClient(client);
realmConfiguration.setAuthUrl(UriBuilder.fromUri(authUrl).queryParam("client_id", resourceMetadata.getResourceName()));
realmConfiguration.setCodeUrl(client.target(tokenUrl));
AuthenticatedActionsValve actions = new AuthenticatedActionsValve(remoteSkeletonKeyConfig, getNext(), getContainer(), getController()); AuthenticatedActionsValve actions = new AuthenticatedActionsValve(remoteSkeletonKeyConfig, getNext(), getContainer(), getController());
setNext(actions); setNext(actions);
} }

View file

@ -1,237 +0,0 @@
package org.keycloak.adapters.as7.config;
import org.codehaus.jackson.annotate.JsonProperty;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthServerConfig {
@JsonProperty("realm")
protected String realm;
@JsonProperty("realm-private-key")
protected String realmPrivateKey;
@JsonProperty("realm-public-key")
protected String realmPublicKey;
@JsonProperty("realm-keystore")
protected String realmKeyStore;
@JsonProperty("realm-keystore-password")
protected String realmKeystorePassword;
@JsonProperty("realm-key-alias")
protected String realmKeyAlias;
@JsonProperty("realm-private-key-password")
protected String realmPrivateKeyPassword;
@JsonProperty("access-code-lifetime")
protected int accessCodeLifetime;
@JsonProperty("token-lifetime")
protected int tokenLifetime;
@JsonProperty("admin-role")
protected String adminRole;
@JsonProperty("login-role")
protected String loginRole;
@JsonProperty("oauth-client-role")
protected String clientRole;
@JsonProperty("wildcard-role")
protected String wildcardRole;
@JsonProperty("cancel-propagation")
protected boolean cancelPropagation;
@JsonProperty("sso-disabled")
protected boolean ssoDisabled;
// these properties are optional and used to provide connection metadata when the server wants to make
// remote SSL connections
protected String truststore;
@JsonProperty("truststore-password")
protected String truststorePassword;
@JsonProperty("client-keystore")
protected String clientKeystore;
@JsonProperty("client-keystore-password")
protected String clientKeystorePassword;
@JsonProperty("client-key-password")
protected String clientKeyPassword;
protected List<String> resources = new ArrayList<String>();
public String getRealm() {
return realm;
}
public void setRealm(String realm) {
this.realm = realm;
}
public String getRealmPrivateKey() {
return realmPrivateKey;
}
public void setRealmPrivateKey(String realmPrivateKey) {
this.realmPrivateKey = realmPrivateKey;
}
public String getRealmPublicKey() {
return realmPublicKey;
}
public void setRealmPublicKey(String realmPublicKey) {
this.realmPublicKey = realmPublicKey;
}
public int getAccessCodeLifetime() {
return accessCodeLifetime;
}
public void setAccessCodeLifetime(int accessCodeLifetime) {
this.accessCodeLifetime = accessCodeLifetime;
}
public String getTruststore() {
return truststore;
}
public void setTruststore(String truststore) {
this.truststore = truststore;
}
public String getTruststorePassword() {
return truststorePassword;
}
public void setTruststorePassword(String truststorePassword) {
this.truststorePassword = truststorePassword;
}
public String getClientKeystore() {
return clientKeystore;
}
public void setClientKeystore(String clientKeystore) {
this.clientKeystore = clientKeystore;
}
public String getClientKeystorePassword() {
return clientKeystorePassword;
}
public void setClientKeystorePassword(String clientKeystorePassword) {
this.clientKeystorePassword = clientKeystorePassword;
}
public String getClientKeyPassword() {
return clientKeyPassword;
}
public void setClientKeyPassword(String clientKeyPassword) {
this.clientKeyPassword = clientKeyPassword;
}
public boolean isCancelPropagation() {
return cancelPropagation;
}
public void setCancelPropagation(boolean cancelPropagation) {
this.cancelPropagation = cancelPropagation;
}
public boolean isSsoDisabled() {
return ssoDisabled;
}
public void setSsoDisabled(boolean ssoDisabled) {
this.ssoDisabled = ssoDisabled;
}
public List<String> getResources() {
return resources;
}
public String getAdminRole() {
return adminRole;
}
public void setAdminRole(String adminRole) {
this.adminRole = adminRole;
}
public String getLoginRole() {
return loginRole;
}
public void setLoginRole(String loginRole) {
this.loginRole = loginRole;
}
public String getClientRole() {
return clientRole;
}
public void setClientRole(String clientRole) {
this.clientRole = clientRole;
}
public String getWildcardRole() {
return wildcardRole;
}
public void setWildcardRole(String wildcardRole) {
this.wildcardRole = wildcardRole;
}
public String getRealmKeyStore() {
return realmKeyStore;
}
public void setRealmKeyStore(String realmKeyStore) {
this.realmKeyStore = realmKeyStore;
}
public String getRealmKeystorePassword() {
return realmKeystorePassword;
}
public void setRealmKeystorePassword(String realmKeystorePassword) {
this.realmKeystorePassword = realmKeystorePassword;
}
public String getRealmKeyAlias() {
return realmKeyAlias;
}
public void setRealmKeyAlias(String realmKeyAlias) {
this.realmKeyAlias = realmKeyAlias;
}
public String getRealmPrivateKeyPassword() {
return realmPrivateKeyPassword;
}
public void setRealmPrivateKeyPassword(String realmPrivateKeyPassword) {
this.realmPrivateKeyPassword = realmPrivateKeyPassword;
}
public int getTokenLifetime() {
return tokenLifetime;
}
public void setTokenLifetime(int tokenLifetime) {
this.tokenLifetime = tokenLifetime;
}
}

View file

@ -0,0 +1,27 @@
package org.keycloak.adapters.as7.config;
import org.apache.catalina.Context;
import org.keycloak.adapters.config.ManagedResourceConfigLoader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
public class CatalinaManagedResourceConfigLoader extends ManagedResourceConfigLoader {
public CatalinaManagedResourceConfigLoader(Context context) {
InputStream is = null;
String path = context.getServletContext().getInitParameter("keycloak.config.file");
if (path == null) {
is = context.getServletContext().getResourceAsStream("/WEB-INF/keycloak.json");
} else {
try {
is = new FileInputStream(path);
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
}
}
loadConfig(is);
}
}

View file

@ -16,6 +16,7 @@
<modules> <modules>
<module>as7-eap6/adapter</module> <module>as7-eap6/adapter</module>
<module>undertow</module>
<!-- <module>as7-eap6/jboss-modules</module> --> <!-- <module>as7-eap6/jboss-modules</module> -->
</modules> </modules>
</project> </project>

78
integration/undertow/pom.xml Executable file
View file

@ -0,0 +1,78 @@
<?xml version="1.0"?>
<project>
<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>
<artifactId>keycloak-undertow-adapter</artifactId>
<name>Keycloak Undertow Integration</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>3.1.2.GA</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jose-jwt</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-client</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-servlet</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<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,116 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.Headers;
import org.jboss.logging.Logger;
import org.keycloak.SkeletonKeySession;
import org.keycloak.adapters.config.ManagedResourceConfig;
import org.keycloak.representations.SkeletonKeyToken;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.Set;
/**
* Pre-installed actions that must be authenticated
*
* Actions include:
*
* CORS Origin Check and Response headers
* K_QUERY_BEARER_TOKEN: Get bearer token from server for Javascripts CORS requests
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticatedActionsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(AuthenticatedActionsHandler.class);
protected ManagedResourceConfig config;
protected HttpHandler next;
protected AuthenticatedActionsHandler(ManagedResourceConfig config, HttpHandler next) {
this.config = config;
this.next = next;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
log.debugv("AuthenticatedActionsValve.invoke {0}", exchange.getRequestURI());
SkeletonKeySession session = getSkeletonKeySession(exchange);
if (corsRequest(exchange, session)) return;
String requestUri = exchange.getRequestURI();
if (requestUri.endsWith("K_QUERY_BEARER_TOKEN")) {
queryBearerToken(exchange, session);
return;
}
next.handleRequest(exchange);
}
public SkeletonKeySession getSkeletonKeySession(HttpServerExchange exchange) {
SkeletonKeySession skSession = exchange.getAttachment(KeycloakAuthenticationMechanism.SKELETON_KEY_SESSION_ATTACHMENT_KEY);
if (skSession != null) return skSession;
return null;
}
protected void queryBearerToken(HttpServerExchange exchange, SkeletonKeySession session) throws IOException, ServletException {
log.debugv("queryBearerToken {0}",exchange.getRequestURI());
if (abortTokenResponse(exchange, session)) return;
exchange.setResponseCode(200);
exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/plain");
exchange.getResponseSender().send(session.getTokenString());
exchange.endExchange();
}
protected boolean abortTokenResponse(HttpServerExchange exchange, SkeletonKeySession session) throws IOException {
if (session == null) {
log.debugv("session was null, sending back 401: {0}",exchange.getRequestURI());
exchange.setResponseCode(200);
exchange.endExchange();
return true;
}
if (!config.isExposeToken()) {
exchange.setResponseCode(200);
exchange.endExchange();
return true;
}
if (!config.isCors() && exchange.getRequestHeaders().getFirst(Headers.ORIGIN) != null) {
exchange.setResponseCode(200);
exchange.endExchange();
return true;
}
return false;
}
protected boolean corsRequest(HttpServerExchange exchange, SkeletonKeySession session) throws IOException {
if (!config.isCors()) return false;
log.debugv("CORS enabled + request.getRequestURI()");
String origin = exchange.getRequestHeaders().getFirst("Origin");
log.debugv("Origin: {0} uri: {1}", origin, exchange.getRequestURI());
if (session != null && origin != null) {
SkeletonKeyToken token = session.getToken();
Set<String> allowedOrigins = token.getAllowedOrigins();
if (log.isDebugEnabled()) {
for (String a : allowedOrigins) log.debug(" " + a);
}
if (allowedOrigins == null || (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin))) {
if (allowedOrigins == null) {
log.debugv("allowedOrigins was null in token");
}
if (!allowedOrigins.contains("*") && !allowedOrigins.contains(origin)) {
log.debugv("allowedOrigins did not contain origin");
}
exchange.setResponseCode(403);
exchange.endExchange();
return true;
}
log.debugv("returning origin: {0}", origin);
exchange.setResponseCode(200);
exchange.getResponseHeaders().put(PreflightCorsHandler.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
exchange.getResponseHeaders().put(PreflightCorsHandler.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
} else {
log.debugv("session or origin was null: {0}", exchange.getRequestURI());
}
return false;
}
}

View file

@ -0,0 +1,149 @@
package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import org.jboss.logging.Logger;
import org.keycloak.RSATokenVerifier;
import org.keycloak.ResourceMetadata;
import org.keycloak.VerificationException;
import org.keycloak.representations.SkeletonKeyToken;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static io.undertow.util.Headers.AUTHORIZATION;
import static io.undertow.util.Headers.WWW_AUTHENTICATE;
import static io.undertow.util.StatusCodes.UNAUTHORIZED;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BearerTokenAuthenticator {
protected ResourceMetadata resourceMetadata;
protected Logger log = Logger.getLogger(BearerTokenAuthenticator.class);
protected String tokenString;
protected SkeletonKeyToken token;
protected boolean useResourceRoleMappings;
protected String surrogate;
protected KeycloakChallenge challenge;
public BearerTokenAuthenticator(ResourceMetadata resourceMetadata, boolean useResourceRoleMappings) {
this.resourceMetadata = resourceMetadata;
this.useResourceRoleMappings = useResourceRoleMappings;
}
public KeycloakChallenge getChallenge() {
return challenge;
}
public ResourceMetadata getResourceMetadata() {
return resourceMetadata;
}
public String getTokenString() {
return tokenString;
}
public SkeletonKeyToken getToken() {
return token;
}
public String getSurrogate() {
return surrogate;
}
public AuthenticationMechanism.AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange) {
List<String> authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION);
if (authHeaders == null || authHeaders.size() == 0) {
challenge = challengeResponse(exchange, null, null);
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
tokenString = null;
for (String authHeader : authHeaders) {
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) continue;
if (!split[0].equalsIgnoreCase("Bearer")) continue;
tokenString = split[1];
}
if (tokenString == null) {
challenge = challengeResponse(exchange, null, null);
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
try {
token = RSATokenVerifier.verifyToken(tokenString, resourceMetadata);
} catch (VerificationException e) {
log.error("Failed to verify token", e);
challenge = challengeResponse(exchange, "invalid_token", e.getMessage());
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
boolean verifyCaller = false;
Set<String> roles = new HashSet<String>();
if (useResourceRoleMappings) {
verifyCaller = token.isVerifyCaller(resourceMetadata.getResourceName());
} else {
verifyCaller = token.isVerifyCaller();
}
surrogate = null;
if (verifyCaller) {
if (token.getTrustedCertificates() == null || token.getTrustedCertificates().size() == 0) {
log.warn("No trusted certificates in token");
challenge = clientCertChallenge();
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
// for now, we just make sure Undertow did two-way SSL
// assume JBoss Web verifies the client cert
X509Certificate[] chain = new X509Certificate[0];
try {
chain = exchange.getConnection().getSslSessionInfo().getPeerCertificateChain();
} catch (SSLPeerUnverifiedException ignore) {
}
if (chain == null || chain.length == 0) {
log.warn("No certificates provided by undertow to verify the caller");
challenge = clientCertChallenge();
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
surrogate = chain[0].getSubjectDN().getName();
}
return AuthenticationMechanism.AuthenticationMechanismOutcome.AUTHENTICATED;
}
protected KeycloakChallenge clientCertChallenge() {
return new KeycloakChallenge() {
@Override
public AuthenticationMechanism.ChallengeResult sendChallenge(HttpServerExchange httpServerExchange, SecurityContext securityContext) {
// do the same thing as client cert auth
return new AuthenticationMechanism.ChallengeResult(false);
}
};
}
protected KeycloakChallenge challengeResponse(HttpServerExchange exchange, String error, String description) {
StringBuilder header = new StringBuilder("Bearer realm=\"");
header.append(resourceMetadata.getRealm()).append("\"");
if (error != null) {
header.append(", error=\"").append(error).append("\"");
}
if (description != null) {
header.append(", error_description=\"").append(description).append("\"");
}
String challenge = header.toString();
exchange.getResponseHeaders().add(WWW_AUTHENTICATE, challenge);
return new KeycloakChallenge() {
@Override
public AuthenticationMechanism.ChallengeResult sendChallenge(HttpServerExchange httpServerExchange, SecurityContext securityContext) {
return new AuthenticationMechanism.ChallengeResult(true, UNAUTHORIZED);
}
};
}
}

View file

@ -0,0 +1,142 @@
package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.SecurityContext;
import io.undertow.security.idm.Account;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;
import org.jboss.logging.Logger;
import org.keycloak.RealmConfiguration;
import org.keycloak.ResourceMetadata;
import org.keycloak.SkeletonKeyPrincipal;
import org.keycloak.SkeletonKeySession;
import org.keycloak.adapters.config.ManagedResourceConfig;
import org.keycloak.representations.SkeletonKeyToken;
import java.security.Principal;
import java.util.Collections;
import java.util.Set;
import static io.undertow.util.Headers.WWW_AUTHENTICATE;
import static io.undertow.util.StatusCodes.UNAUTHORIZED;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakAuthenticationMechanism implements AuthenticationMechanism {
protected Logger log = Logger.getLogger(KeycloakAuthenticationMechanism.class);
public static final AttachmentKey<KeycloakChallenge> KEYCLOAK_CHALLENGE_ATTACHMENT_KEY = AttachmentKey.create(KeycloakChallenge.class);
public static final AttachmentKey<SkeletonKeySession> SKELETON_KEY_SESSION_ATTACHMENT_KEY = AttachmentKey.create(SkeletonKeySession.class);
protected ResourceMetadata resourceMetadata;
protected ManagedResourceConfig config;
protected RealmConfiguration realmConfig;
protected int sslRedirectPort;
public KeycloakAuthenticationMechanism(ResourceMetadata resourceMetadata, ManagedResourceConfig config, RealmConfiguration realmConfig, int sslRedirectPort) {
this.resourceMetadata = resourceMetadata;
this.config = config;
this.realmConfig = realmConfig;
this.sslRedirectPort = sslRedirectPort;
}
public KeycloakAuthenticationMechanism(ResourceMetadata resourceMetadata, ManagedResourceConfig config, RealmConfiguration realmConfig) {
this.resourceMetadata = resourceMetadata;
this.config = config;
this.realmConfig = realmConfig;
}
@Override
public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) {
BearerTokenAuthenticator bearer = createBearerTokenAuthenticator();
AuthenticationMechanismOutcome outcome = bearer.authenticate(exchange);
if (outcome == AuthenticationMechanismOutcome.NOT_AUTHENTICATED) {
exchange.putAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY, bearer.getChallenge());
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
else if (outcome == AuthenticationMechanismOutcome.AUTHENTICATED) {
final SkeletonKeyToken token = bearer.getToken();
String surrogate = bearer.getSurrogate();
SkeletonKeySession session = new SkeletonKeySession(bearer.getTokenString(), token, resourceMetadata);
propagateBearer(exchange, session);
completeAuthentication(exchange, securityContext, token, surrogate);
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
else if (config.isBearerOnly()) {
exchange.putAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY, bearer.getChallenge());
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
OAuthAuthenticator oauth = createOAuthAuthenticator(exchange);
outcome = oauth.authenticate();
if (outcome == AuthenticationMechanismOutcome.NOT_AUTHENTICATED) {
exchange.putAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY, oauth.getChallenge());
return AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
else if (outcome == AuthenticationMechanismOutcome.NOT_ATTEMPTED) {
exchange.putAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY, oauth.getChallenge());
return AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
SkeletonKeySession session = new SkeletonKeySession(oauth.getTokenString(), oauth.getToken(), resourceMetadata);
propagateOauth(exchange, session);
completeAuthentication(exchange, securityContext, oauth.getToken(), null);
log.info("AUTHENTICATED");
return AuthenticationMechanismOutcome.AUTHENTICATED;
}
protected OAuthAuthenticator createOAuthAuthenticator(HttpServerExchange exchange) {
return new OAuthAuthenticator(exchange, realmConfig, sslRedirectPort);
}
protected BearerTokenAuthenticator createBearerTokenAuthenticator() {
return new BearerTokenAuthenticator(resourceMetadata, config.isUseResourceRoleMappings());
}
protected void completeAuthentication(HttpServerExchange exchange, SecurityContext securityContext, SkeletonKeyToken token, String surrogate) {
final SkeletonKeyPrincipal skeletonKeyPrincipal = new SkeletonKeyPrincipal(token.getPrincipal(), surrogate);
Set<String> roles = null;
if (config.isUseResourceRoleMappings()) {
SkeletonKeyToken.Access access = token.getResourceAccess(resourceMetadata.getResourceName());
if (access != null) roles = access.getRoles();
} else {
SkeletonKeyToken.Access access = token.getRealmAccess();
if (access != null) roles = access.getRoles();
}
if (roles == null) roles = Collections.emptySet();
final Set<String> accountRoles = roles;
Account account = new Account() {
@Override
public Principal getPrincipal() {
return skeletonKeyPrincipal;
}
@Override
public Set<String> getRoles() {
return accountRoles;
}
};
securityContext.authenticationComplete(account, "FORM");
}
protected void propagateBearer(HttpServerExchange exchange, SkeletonKeySession session) {
exchange.putAttachment(SKELETON_KEY_SESSION_ATTACHMENT_KEY, session);
}
protected void propagateOauth(HttpServerExchange exchange, SkeletonKeySession session) {
exchange.putAttachment(SKELETON_KEY_SESSION_ATTACHMENT_KEY, session);
}
@Override
public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
KeycloakChallenge challenge = exchange.getAttachment(KEYCLOAK_CHALLENGE_ATTACHMENT_KEY);
if (challenge != null) {
return challenge.sendChallenge(exchange, securityContext);
}
return new ChallengeResult(false);
}
}

View file

@ -0,0 +1,13 @@
package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface KeycloakChallenge {
public AuthenticationMechanism.ChallengeResult sendChallenge(HttpServerExchange httpServerExchange, SecurityContext securityContext);
}

View file

@ -0,0 +1,75 @@
package org.keycloak.adapters.undertow;
import io.undertow.security.idm.Account;
import io.undertow.security.idm.Credential;
import io.undertow.security.idm.IdentityManager;
import io.undertow.servlet.ServletExtension;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.ServletSessionConfig;
import org.jboss.logging.Logger;
import org.keycloak.adapters.config.ManagedResourceConfig;
import org.keycloak.adapters.config.ManagedResourceConfigLoader;
import javax.servlet.ServletContext;
import java.io.InputStream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakServletExtension implements ServletExtension {
protected Logger log = Logger.getLogger(KeycloakServletExtension.class);
@Override
public void handleDeployment(DeploymentInfo deploymentInfo, ServletContext servletContext) {
if (deploymentInfo.getLoginConfig() == null || !deploymentInfo.getLoginConfig().getAuthMethod().equalsIgnoreCase("keycloak")) {
log.info("auth-method is not keycloak!");
return;
}
log.info("KeycloakServletException initialization");
deploymentInfo.setIgnoreStandardAuthenticationMechanism(true);
InputStream is = servletContext.getResourceAsStream("/WEB-INF/keycloak.json");
if (is == null) throw new RuntimeException("Unable to find /WEB-INF/keycloak.json configuration file");
ManagedResourceConfigLoader loader = new ManagedResourceConfigLoader(is);
loader.init(true);
ManagedResourceConfig keycloakConfig = loader.getRemoteSkeletonKeyConfig();
PreflightCorsHandler.Wrapper preflight = new PreflightCorsHandler.Wrapper(keycloakConfig);
ServletKeycloakAuthenticationMechanism auth = new ServletKeycloakAuthenticationMechanism(loader.getResourceMetadata(),
keycloakConfig,
loader.getRealmConfiguration(),
deploymentInfo.getConfidentialPortManager());
ServletAuthenticatedActionsHandler.Wrapper actions = new ServletAuthenticatedActionsHandler.Wrapper(keycloakConfig);
// setup handlers
deploymentInfo.addInitialHandlerChainWrapper(preflight); // cors preflight
deploymentInfo.addAuthenticationMechanism(auth); // authentication
deploymentInfo.addInnerHandlerChainWrapper(ServletPropagateSessionHandler.WRAPPER); // propagates SkeletonKeySession
deploymentInfo.addInnerHandlerChainWrapper(actions); // handles authenticated actions and cors.
deploymentInfo.setIdentityManager(new IdentityManager() {
@Override
public Account verify(Account account) {
log.info("Verifying account in IdentityManager");
return account;
}
@Override
public Account verify(String id, Credential credential) {
log.warn("Shouldn't call verify!!!");
throw new IllegalStateException("Not allowed");
}
@Override
public Account verify(Credential credential) {
log.warn("Shouldn't call verify!!!");
throw new IllegalStateException("Not allowed");
}
});
log.info("Setting jsession cookie path to: " + deploymentInfo.getContextPath());
ServletSessionConfig cookieConfig = new ServletSessionConfig();
cookieConfig.setPath(deploymentInfo.getContextPath());
deploymentInfo.setServletSessionConfig(cookieConfig);
}
}

View file

@ -0,0 +1,294 @@
package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticationMechanism;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.Cookie;
import io.undertow.server.handlers.CookieImpl;
import io.undertow.util.Headers;
import org.jboss.logging.Logger;
import org.keycloak.RSATokenVerifier;
import org.keycloak.RealmConfiguration;
import org.keycloak.VerificationException;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.SkeletonKeyToken;
import org.keycloak.representations.idm.CredentialRepresentation;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.util.Deque;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OAuthAuthenticator {
private static final Logger log = Logger.getLogger(OAuthAuthenticator.class);
protected RealmConfiguration realmInfo;
protected int sslRedirectPort;
protected String tokenString;
protected SkeletonKeyToken token;
protected HttpServerExchange exchange;
protected String redirectUri;
protected KeycloakChallenge challenge;
public OAuthAuthenticator(HttpServerExchange exchange, RealmConfiguration realmInfo, int sslRedirectPort) {
this.exchange = exchange;
this.realmInfo = realmInfo;
this.sslRedirectPort = sslRedirectPort;
}
public KeycloakChallenge getChallenge() {
return challenge;
}
public String getTokenString() {
return tokenString;
}
public SkeletonKeyToken getToken() {
return token;
}
public String getRedirectUri() {
return redirectUri;
}
protected String getRequestUrl() {
UriBuilder uriBuilder = UriBuilder.fromUri(exchange.getRequestURI())
.replaceQuery(exchange.getQueryString());
if (!exchange.isHostIncludedInRequestURI()) uriBuilder.scheme(exchange.getRequestScheme()).host(exchange.getHostAndPort());
return uriBuilder.build().toString();
}
protected boolean isRequestSecure() {
return exchange.getProtocol().toString().equalsIgnoreCase("https");
}
protected Cookie getCookie(String cookieName) {
Map<String, Cookie> requestCookies = exchange.getRequestCookies();
if (requestCookies == null) return null;
return requestCookies.get(cookieName);
}
protected String getCookieValue(String cookieName) {
Cookie cookie = getCookie(cookieName);
if (cookie == null) return null;
return cookie.getValue();
}
protected String getQueryParamValue(String paramName) {
Map<String,Deque<String>> queryParameters = exchange.getQueryParameters();
if (queryParameters == null) return null;
Deque<String> strings = queryParameters.get(paramName);
if (strings == null) return null;
return strings.getFirst();
}
protected String getError() {
return getQueryParamValue("error");
}
protected String getCode() {
return getQueryParamValue("code");
}
protected String getRedirectUri(String state) {
String url = getRequestUrl();
log.info("sending redirect uri: " + url);
if (!isRequestSecure() && realmInfo.isSslRequired()) {
int port = sslRedirectPort();
if (port < 0) {
// disabled?
return null;
}
UriBuilder secureUrl = UriBuilder.fromUri(url).scheme("https").port(-1);
if (port != 443) secureUrl.port(port);
url = secureUrl.build().toString();
}
return realmInfo.getAuthUrl().clone()
.queryParam("client_id", realmInfo.getMetadata().getResourceName())
.queryParam("redirect_uri", url)
.queryParam("state", state)
.queryParam("login", "true")
.build().toString();
}
protected int sslRedirectPort() {
return sslRedirectPort;
}
protected static final AtomicLong counter = new AtomicLong();
protected String getStateCode() {
return counter.getAndIncrement() + "/" + UUID.randomUUID().toString();
}
protected KeycloakChallenge loginRedirect() {
final String state = getStateCode();
final String redirect = getRedirectUri(state);
return new KeycloakChallenge() {
@Override
public AuthenticationMechanism.ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) {
if (redirect == null) {
return new AuthenticationMechanism.ChallengeResult(true, 403);
}
CookieImpl cookie = new CookieImpl(realmInfo.getStateCookieName(), state);
//cookie.setPath(getDefaultCookiePath()); todo I don't think we need to set state cookie path as it will be the same redirect
cookie.setSecure(realmInfo.isSslRequired());
exchange.setResponseCookie(cookie);
exchange.getResponseHeaders().put(Headers.LOCATION, redirect);
return new AuthenticationMechanism.ChallengeResult(true, 302);
}
};
}
protected KeycloakChallenge checkStateCookie() {
Cookie stateCookie = getCookie(realmInfo.getStateCookieName());
if (stateCookie == null) {
log.warn("No state cookie");
return challenge(400);
}
// reset the cookie
log.info("** reseting application state cookie");
Cookie reset = new CookieImpl(realmInfo.getStateCookieName(), "");
reset.setPath(stateCookie.getPath());
reset.setMaxAge(0);
exchange.setResponseCookie(reset);
String stateCookieValue = getCookieValue(realmInfo.getStateCookieName());
String state = getQueryParamValue("state");
if (state == null) {
log.warn("state parameter was null");
return challenge(400);
}
if (!state.equals(stateCookieValue)) {
log.warn("state parameter invalid");
log.warn("cookie: " + stateCookieValue);
log.warn("queryParam: " + state);
return challenge(400);
}
return null;
}
public AuthenticationMechanism.AuthenticationMechanismOutcome authenticate() {
String code = getCode();
if (code == null) {
log.info("there was no code");
String error = getError();
if (error != null) {
// todo how do we send a response?
log.warn("There was an error: " + error);
challenge = challenge(400);
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
} else {
log.info("redirecting to auth server");
challenge = loginRedirect();
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_ATTEMPTED;
}
} else {
log.info("there was a code, resolving");
challenge = resolveCode(code);
if (challenge != null) {
return AuthenticationMechanism.AuthenticationMechanismOutcome.NOT_AUTHENTICATED;
}
return AuthenticationMechanism.AuthenticationMechanismOutcome.AUTHENTICATED;
}
}
protected KeycloakChallenge challenge(final int code) {
return new KeycloakChallenge() {
@Override
public AuthenticationMechanism.ChallengeResult sendChallenge(HttpServerExchange httpServerExchange, SecurityContext securityContext) {
return new AuthenticationMechanism.ChallengeResult(true, code);
}
};
}
/**
* Start or continue the oauth login process.
* <p/>
* if code query parameter is not present, then browser is redirected to authUrl. The redirect URL will be
* the URL of the current request.
* <p/>
* If code query parameter is present, then an access token is obtained by invoking a secure request to the codeUrl.
* If the access token is obtained, the browser is again redirected to the current request URL, but any OAuth
* protocol specific query parameters are removed.
*
* @return null if an access token was obtained, otherwise a challenge is returned
*/
protected KeycloakChallenge resolveCode(String code) {
// abort if not HTTPS
if (realmInfo.isSslRequired() && !isRequestSecure()) {
log.error("SSL is required");
return challenge(403);
}
log.info("checking state cookie for after code");
KeycloakChallenge challenge = checkStateCookie();
if (challenge != null) return challenge;
String client_id = realmInfo.getMetadata().getResourceName();
String password = realmInfo.getResourceCredentials().asMap().getFirst("password");
//String authHeader = BasicAuthHelper.createHeader(client_id, password);
redirectUri = stripOauthParametersFromRedirect();
Form form = new Form();
form.param("grant_type", "authorization_code")
.param("code", code)
.param("client_id", client_id)
.param(CredentialRepresentation.PASSWORD, password)
.param("redirect_uri", redirectUri);
Response res = realmInfo.getCodeUrl().request()
.post(Entity.form(form));
AccessTokenResponse tokenResponse;
try {
if (res.getStatus() != 200) {
log.error("failed to turn code into token");
log.error("status from server: " + res.getStatus());
if (res.getStatus() == 400 && res.getMediaType() != null) {
log.error(" " + res.readEntity(String.class));
}
return challenge(403);
}
log.debug("media type: " + res.getMediaType());
log.debug("Content-Type header: " + res.getHeaderString("Content-Type"));
tokenResponse = res.readEntity(AccessTokenResponse.class);
} finally {
res.close();
}
tokenString = tokenResponse.getToken();
try {
token = RSATokenVerifier.verifyToken(tokenString, realmInfo.getMetadata());
log.debug("Token Verification succeeded!");
} catch (VerificationException e) {
log.error("failed verification of token");
return challenge(403);
}
log.info("successful authenticated");
return null;
}
/**
* strip out unwanted query parameters and redirect so bookmarks don't retain oauth protocol bits
*/
protected String stripOauthParametersFromRedirect() {
UriBuilder builder = UriBuilder.fromUri(exchange.getRequestURI())
.replaceQuery(exchange.getQueryString())
.replaceQueryParam("code", null)
.replaceQueryParam("state", null);
return builder.build().toString();
}
}

View file

@ -0,0 +1,80 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.HttpString;
import org.jboss.logging.Logger;
import org.keycloak.adapters.config.ManagedResourceConfig;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class PreflightCorsHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(PreflightCorsHandler.class);
protected ManagedResourceConfig config;
protected HttpHandler next;
public static final HttpString ACCESS_CONTROL_ALLOW_ORIGIN = new HttpString("Access-Control-Allow-Origin");
public static final HttpString ACCESS_CONTROL_ALLOW_CREDENTIALS = new HttpString("Access-Control-Allow-Credentials");
public static final HttpString ACCESS_CONTROL_ALLOW_METHODS = new HttpString("Access-Control-Allow-Methods");
public static final HttpString ACCESS_CONTROL_ALLOW_HEADERS = new HttpString("Access-Control-Allow-Headers");
public static final HttpString ACCESS_CONTROL_MAX_AGE = new HttpString("Access-Control-Max-Age");
public static class Wrapper implements HandlerWrapper {
protected ManagedResourceConfig config;
public Wrapper(ManagedResourceConfig config) {
this.config = config;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
return new PreflightCorsHandler(config, handler);
}
}
protected PreflightCorsHandler(ManagedResourceConfig config, HttpHandler next) {
this.config = config;
this.next = next;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
log.debugv("checkCorsPreflight {0}", exchange.getRequestURI());
if (!exchange.getRequestMethod().toString().equalsIgnoreCase("OPTIONS")) {
log.debug("checkCorsPreflight: not options ");
next.handleRequest(exchange);
return;
}
if (exchange.getRequestHeaders().getFirst("Origin") == null) {
log.debug("checkCorsPreflight: no origin header");
next.handleRequest(exchange);
return;
}
log.debug("Preflight request returning");
exchange.setResponseCode(200);
String origin = exchange.getRequestHeaders().getFirst("Origin");
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
String requestMethods = exchange.getRequestHeaders().getFirst("Access-Control-Request-Method");
if (requestMethods != null) {
if (config.getCorsAllowedMethods() != null) {
requestMethods = config.getCorsAllowedMethods();
}
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_METHODS, requestMethods);
}
String allowHeaders = exchange.getRequestHeaders().getFirst("Access-Control-Request-Headers");
if (allowHeaders != null) {
if (config.getCorsAllowedHeaders() != null) {
allowHeaders = config.getCorsAllowedHeaders();
}
exchange.getResponseHeaders().put(ACCESS_CONTROL_ALLOW_HEADERS, allowHeaders);
}
if (config.getCorsMaxAge() > -1) {
exchange.getResponseHeaders().put(ACCESS_CONTROL_MAX_AGE, Integer.toString(config.getCorsMaxAge()));
}
exchange.endExchange();
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.keycloak.SkeletonKeySession;
import org.keycloak.adapters.config.ManagedResourceConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletAuthenticatedActionsHandler extends AuthenticatedActionsHandler {
protected ServletAuthenticatedActionsHandler(ManagedResourceConfig config, HttpHandler next) {
super(config, next);
}
public static class Wrapper implements HandlerWrapper {
protected ManagedResourceConfig config;
public Wrapper(ManagedResourceConfig config) {
this.config = config;
}
@Override
public HttpHandler wrap(HttpHandler handler) {
return new ServletAuthenticatedActionsHandler(config, handler);
}
}
@Override
public SkeletonKeySession getSkeletonKeySession(HttpServerExchange exchange) {
SkeletonKeySession skSession = super.getSkeletonKeySession(exchange);
if (skSession != null) return skSession;
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
HttpSession session = req.getSession(false);
if (session == null) return null;
return (SkeletonKeySession)session.getAttribute(SkeletonKeySession.class.getName());
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.ConfidentialPortManager;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.keycloak.RealmConfiguration;
import org.keycloak.ResourceMetadata;
import org.keycloak.SkeletonKeySession;
import org.keycloak.adapters.config.ManagedResourceConfig;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletKeycloakAuthenticationMechanism extends KeycloakAuthenticationMechanism {
protected ConfidentialPortManager portManager;
public ServletKeycloakAuthenticationMechanism(ResourceMetadata resourceMetadata, ManagedResourceConfig config, RealmConfiguration realmConfig, ConfidentialPortManager portManager) {
super(resourceMetadata, config, realmConfig);
this.portManager = portManager;
}
@Override
protected OAuthAuthenticator createOAuthAuthenticator(HttpServerExchange exchange) {
return new ServletOAuthAuthenticator(exchange, realmConfig, portManager);
}
@Override
protected void propagateBearer(HttpServerExchange exchange, SkeletonKeySession session) {
super.propagateBearer(exchange, session);
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
req.setAttribute(SkeletonKeySession.class.getName(), session);
}
@Override
protected void propagateOauth(HttpServerExchange exchange, SkeletonKeySession skSession) {
super.propagateOauth(exchange, skSession);
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
req.setAttribute(SkeletonKeySession.class.getName(), skSession);
HttpSession session = req.getSession(true);
session.setAttribute(SkeletonKeySession.class.getName(), skSession);
}
}

View file

@ -0,0 +1,23 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.api.ConfidentialPortManager;
import org.keycloak.RealmConfiguration;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletOAuthAuthenticator extends OAuthAuthenticator {
protected ConfidentialPortManager portManager;
public ServletOAuthAuthenticator(HttpServerExchange exchange, RealmConfiguration realmInfo, ConfidentialPortManager portManager) {
super(exchange, realmInfo, -1);
this.portManager = portManager;
}
@Override
protected int sslRedirectPort() {
return portManager.getConfidentialPort(exchange);
}
}

View file

@ -0,0 +1,63 @@
package org.keycloak.adapters.undertow;
import io.undertow.server.HandlerWrapper;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.jboss.logging.Logger;
import org.keycloak.SkeletonKeySession;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ServletPropagateSessionHandler implements HttpHandler {
private static final Logger log = Logger.getLogger(ServletPropagateSessionHandler.class);
protected HttpHandler next;
protected ServletPropagateSessionHandler(HttpHandler next) {
this.next = next;
}
public static final HandlerWrapper WRAPPER = new HandlerWrapper() {
@Override
public HttpHandler wrap(HttpHandler handler) {
return new ServletPropagateSessionHandler(handler);
}
};
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
log.info("handleRequest");
final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
HttpServletRequest req = (HttpServletRequest) servletRequestContext.getServletRequest();
SkeletonKeySession skSession = (SkeletonKeySession)req.getAttribute(SkeletonKeySession.class.getName());
if (skSession != null) {
log.info("skSession is in request");
next.handleRequest(exchange);
return;
}
HttpSession session = req.getSession(false);
if (session == null) {
log.info("http session was null, nothing to propagate");
next.handleRequest(exchange);
return;
}
skSession = (SkeletonKeySession)session.getAttribute(SkeletonKeySession.class.getName());
if (skSession == null) {
log.info("skSession not in http session, nothing to propagate");
next.handleRequest(exchange);
return;
}
log.info("propagating");
req.setAttribute(SkeletonKeySession.class.getName(), skSession);
exchange.putAttachment(KeycloakAuthenticationMechanism.SKELETON_KEY_SESSION_ATTACHMENT_KEY, skSession);
next.handleRequest(exchange);
}
}

View file

@ -0,0 +1 @@
org.keycloak.adapters.undertow.KeycloakServletExtension

View file

@ -10,7 +10,7 @@
<properties> <properties>
<resteasy.version>3.0.5.Final</resteasy.version> <resteasy.version>3.0.5.Final</resteasy.version>
<undertow.version>1.0.0.Beta12</undertow.version> <undertow.version>1.0.0.Beta21</undertow.version>
<picketlink.version>2.5.0.Beta6</picketlink.version> <picketlink.version>2.5.0.Beta6</picketlink.version>
<mongo.driver.version>2.11.2</mongo.driver.version> <mongo.driver.version>2.11.2</mongo.driver.version>
<jboss.logging.version>3.1.1.GA</jboss.logging.version> <jboss.logging.version>3.1.1.GA</jboss.logging.version>

View file

@ -26,6 +26,7 @@ import io.undertow.Undertow.Builder;
import io.undertow.server.handlers.resource.FileResource; import io.undertow.server.handlers.resource.FileResource;
import io.undertow.server.handlers.resource.FileResourceManager; import io.undertow.server.handlers.resource.FileResourceManager;
import io.undertow.server.handlers.resource.Resource; import io.undertow.server.handlers.resource.Resource;
import io.undertow.server.handlers.resource.ResourceChangeListener;
import io.undertow.server.handlers.resource.ResourceManager; import io.undertow.server.handlers.resource.ResourceManager;
import io.undertow.server.handlers.resource.URLResource; import io.undertow.server.handlers.resource.URLResource;
import io.undertow.servlet.Servlets; import io.undertow.servlet.Servlets;
@ -326,6 +327,26 @@ public class KeycloakServer {
return new FileResource(file, new FileResourceManager(file.getParentFile(), 1), path); return new FileResource(file, new FileResourceManager(file.getParentFile(), 1), path);
} }
} }
@Override
public boolean isResourceChangeListenerSupported() {
return false; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void registerResourceChangeListener(ResourceChangeListener resourceChangeListener) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void removeResourceChangeListener(ResourceChangeListener resourceChangeListener) {
//To change body of implemented methods use File | Settings | File Templates.
}
@Override
public void close() throws IOException {
//To change body of implemented methods use File | Settings | File Templates.
}
} }
private static File file(String... path) { private static File file(String... path) {