KEYCLOAK-401 Added service account demo

This commit is contained in:
mposolda 2015-07-22 10:53:41 +02:00
parent d8c1081578
commit 1a34feda65
10 changed files with 404 additions and 0 deletions

View file

@ -210,6 +210,14 @@ An pure HTML5/Javascript example using Keycloak to secure it.
If you are 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.
Step 10: Service Account Example
================================
An example for retrieve service account dedicated to the Client Application itself (not to any user).
[http://localhost:8080/service-account-portal](http://localhost:8080/service-account-portal)
Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed)
Admin Console
==========================

View file

@ -36,6 +36,7 @@
<module>database-service</module>
<module>third-party</module>
<module>third-party-cdi</module>
<module>service-account</module>
</modules>
<profiles>

View file

@ -0,0 +1,60 @@
<?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-examples-demo-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.4.0.Final-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>org.keycloak.example.demo</groupId>
<artifactId>service-account-example</artifactId>
<packaging>war</packaging>
<name>Service Account Example App</name>
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_3.0_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>service-account-portal</finalName>
<plugins>
<plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,234 @@
package org.keycloak.example;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.keycloak.OAuth2Constants;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.ServerRequest;
import org.keycloak.constants.ServiceAccountConstants;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.util.BasicAuthHelper;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ProductServiceAccountServlet extends HttpServlet {
public static final String ERROR = "error";
public static final String TOKEN = "token";
public static final String TOKEN_PARSED = "idTokenParsed";
public static final String REFRESH_TOKEN = "refreshToken";
public static final String PRODUCTS = "products";
@Override
public void init() throws ServletException {
InputStream config = getServletContext().getResourceAsStream("WEB-INF/keycloak.json");
KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config);
HttpClient client = new DefaultHttpClient();
getServletContext().setAttribute(KeycloakDeployment.class.getName(), deployment);
getServletContext().setAttribute(HttpClient.class.getName(), client);
}
@Override
public void destroy() {
getHttpClient().getConnectionManager().shutdown();
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String reqUri = req.getRequestURI();
if (reqUri.endsWith("/login")) {
serviceAccountLogin(req);
} else if (reqUri.endsWith("/refresh")) {
refreshToken(req);
} else if (reqUri.endsWith("/logout")){
logout(req);
}
// Don't load products if some error happened during login,refresh or logout
if (req.getAttribute(ERROR) == null) {
loadProducts(req);
}
req.getRequestDispatcher("/WEB-INF/page.jsp").forward(req, resp);
}
private void serviceAccountLogin(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
HttpClient client = getHttpClient();
String clientId = deployment.getResourceName();
String clientSecret = deployment.getResourceCredentials().get("secret");
try {
HttpPost post = new HttpPost(deployment.getTokenUrl());
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.CLIENT_CREDENTIALS));
String authHeader = BasicAuthHelper.createHeader(clientId, clientSecret);
post.addHeader("Authorization", authHeader);
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
post.setEntity(form);
HttpResponse response = client.execute(post);
int status = response.getStatusLine().getStatusCode();
HttpEntity entity = response.getEntity();
if (status != 200) {
String json = getContent(entity);
String error = "Service account login failed. Bad status: " + status + " response: " + json;
req.setAttribute(ERROR, error);
} else if (entity == null) {
req.setAttribute(ERROR, "No entity");
} else {
String json = getContent(entity);
AccessTokenResponse tokenResp = JsonSerialization.readValue(json, AccessTokenResponse.class);
setTokens(req, deployment, tokenResp);
}
} catch (IOException ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Service account login failed. IOException occured. See server.log for details. Message is: " + ioe.getMessage());
} catch (VerificationException vfe) {
req.setAttribute(ERROR, "Service account login failed. Failed to verify token Message is: " + vfe.getMessage());
}
}
private void setTokens(HttpServletRequest req, KeycloakDeployment deployment, AccessTokenResponse tokenResponse) throws IOException, VerificationException {
String token = tokenResponse.getToken();
String refreshToken = tokenResponse.getRefreshToken();
AccessToken tokenParsed = RSATokenVerifier.verifyToken(token, deployment.getRealmKey(), deployment.getRealmInfoUrl());
req.getSession().setAttribute(TOKEN, token);
req.getSession().setAttribute(REFRESH_TOKEN, refreshToken);
req.getSession().setAttribute(TOKEN_PARSED, tokenParsed);
}
private void loadProducts(HttpServletRequest req) {
HttpClient client = getHttpClient();
String token = (String) req.getSession().getAttribute(TOKEN);
HttpGet get = new HttpGet("http://localhost:8080/database/products");
if (token != null) {
get.addHeader("Authorization", "Bearer " + token);
}
try {
HttpResponse response = client.execute(get);
HttpEntity entity = response.getEntity();
int status = response.getStatusLine().getStatusCode();
if (status != 200) {
String json = getContent(entity);
String error = "Failed retrieve products.";
if (status == 401) {
error = error + " You need to login first with the service account.";
} else if (status == 403) {
error = error + " Maybe service account user doesn't have needed role? Assign role 'user' in Keycloak admin console to user '" +
ServiceAccountConstants.SERVICE_ACCOUNT_USER_PREFIX + getKeycloakDeployment().getResourceName() + "' and then logout and login again.";
}
error = error + " Status: " + status + ", Response: " + json;
req.setAttribute(ERROR, error);
} else if (entity == null) {
req.setAttribute(ERROR, "No entity");
} else {
String products = getContent(entity);
req.setAttribute(PRODUCTS, products);
}
} catch (IOException ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Failed retrieve products. IOException occured. See server.log for details. Message is: " + ioe.getMessage());
}
}
private void refreshToken(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
if (refreshToken == null) {
req.setAttribute(ERROR, "No refresh token available. Please login first");
} else {
try {
AccessTokenResponse tokenResponse = ServerRequest.invokeRefresh(deployment, refreshToken);
setTokens(req, deployment, tokenResponse);
} catch (ServerRequest.HttpFailure hfe) {
hfe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Status was: " + hfe.getStatus() + ", Error is: " + hfe.getError());
} catch (Exception ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage());
}
}
}
private void logout(HttpServletRequest req) {
KeycloakDeployment deployment = getKeycloakDeployment();
String refreshToken = (String) req.getSession().getAttribute(REFRESH_TOKEN);
if (refreshToken == null) {
req.setAttribute(ERROR, "No refresh token available. Please login first");
} else {
try {
ServerRequest.invokeLogout(deployment, refreshToken);
req.getSession().removeAttribute(TOKEN);
req.getSession().removeAttribute(REFRESH_TOKEN);
req.getSession().removeAttribute(TOKEN_PARSED);
} catch (IOException ioe) {
ioe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Message is: " + ioe.getMessage());
} catch (ServerRequest.HttpFailure hfe) {
hfe.printStackTrace();
req.setAttribute(ERROR, "Failed refresh token. See server.log for details. Status was: " + hfe.getStatus() + ", Error is: " + hfe.getError());
}
}
}
private String getContent(HttpEntity entity) throws IOException {
if (entity == null) return null;
InputStream is = entity.getContent();
try {
ByteArrayOutputStream os = new ByteArrayOutputStream();
int c;
while ((c = is.read()) != -1) {
os.write(c);
}
byte[] bytes = os.toByteArray();
String data = new String(bytes);
return data;
} finally {
try {
is.close();
} catch (IOException ignored) {
}
}
}
private KeycloakDeployment getKeycloakDeployment() {
return (KeycloakDeployment) getServletContext().getAttribute(KeycloakDeployment.class.getName());
}
private HttpClient getHttpClient() {
return (HttpClient) getServletContext().getAttribute(HttpClient.class.getName());
}
}

View file

@ -0,0 +1,9 @@
<jboss-deployment-structure>
<deployment>
<dependencies>
<!-- the Demo code uses classes in these modules. These are optional to import if you are not using
Apache Http Client or the HttpClientBuilder that comes with the adapter core -->
<module name="org.apache.httpcomponents"/>
</dependencies>
</deployment>
</jboss-deployment-structure>

View file

@ -0,0 +1,10 @@
{
"realm" : "demo",
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url" : "http://localhost:8080/auth",
"ssl-required" : "external",
"resource" : "product-sa-client",
"credentials": {
"secret": "password"
}
}

View file

@ -0,0 +1,52 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1" %>
<%@ page import="org.keycloak.example.ProductServiceAccountServlet" %>
<%@ page import="org.keycloak.representations.AccessToken" %>
<%@ page import="org.keycloak.constants.ServiceAccountConstants" %>
<%@ page import="org.keycloak.util.Time" %>
<html>
<head>
<title>Service account portal</title>
</head>
<body bgcolor="#FFFFFF">
<%
AccessToken token = (AccessToken) request.getSession().getAttribute(ProductServiceAccountServlet.TOKEN_PARSED);
String products = (String) request.getAttribute(ProductServiceAccountServlet.PRODUCTS);
String appError = (String) request.getAttribute(ProductServiceAccountServlet.ERROR);
%>
<h1>Service account portal</h1>
<p><a href="/service-account-portal/app/login">Login</a> | <a href="/service-account-portal/app/refresh">Refresh token</a> | <a
href="/service-account-portal/app/logout">Logout</a></p>
<hr />
<% if (appError != null) { %>
<p><font color="red">
<b>Error: </b> <%= appError %>
</font></p>
<hr />
<% } %>
<% if (token != null) { %>
<p>
<b>Service account available</b><br />
Client ID: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ID) %><br />
Client hostname: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_HOST) %><br />
Client address: <%= token.getOtherClaims().get(ServiceAccountConstants.CLIENT_ADDRESS) %><br />
Token expiration: <%= Time.toDate(token.getExpiration()) %><br />
<% if (token.isExpired()) { %>
<font color="red">Access token is expired. You may need to refresh</font><br />
<% } %>
</p>
<hr />
<% } %>
<% if (products != null) { %>
<p>
<b>Products retrieved successfully from REST endpoint</b><br />
Product list: <%= products %>
</p>
<hr />
<% } %>
</body>
</html>

View file

@ -0,0 +1,19 @@
<?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>service-account-portal</module-name>
<servlet>
<servlet-name>ServiceAccountExample</servlet-name>
<servlet-class>org.keycloak.example.ProductServiceAccountServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>ServiceAccountExample</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>

View file

@ -0,0 +1,5 @@
<html>
<head>
<meta http-equiv="Refresh" content="0; URL=app">
</head>
</html>

View file

@ -162,6 +162,12 @@
"publicClient": true,
"directGrantsOnly": true,
"consentRequired": true
},
{
"clientId": "product-sa-client",
"enabled": true,
"secret": "password",
"serviceAccountsEnabled": true
}
],
"clientScopeMappings": {