keycloak-memoizeit
Changes
examples/demo-template/pom.xml 1(+1 -0)
examples/demo-template/README.md 8(+8 -0)
examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java 234(+234 -0)
Details
examples/demo-template/pom.xml 1(+1 -0)
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index a0172c8..4ea8c39 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -36,6 +36,7 @@
<module>database-service</module>
<module>third-party</module>
<module>third-party-cdi</module>
+ <module>service-account</module>
</modules>
<profiles>
examples/demo-template/README.md 8(+8 -0)
diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md
index 1a896af..2445e31 100755
--- a/examples/demo-template/README.md
+++ b/examples/demo-template/README.md
@@ -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
==========================
diff --git a/examples/demo-template/service-account/pom.xml b/examples/demo-template/service-account/pom.xml
new file mode 100644
index 0000000..fde6966
--- /dev/null
+++ b/examples/demo-template/service-account/pom.xml
@@ -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>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
new file mode 100644
index 0000000..f9dc9f1
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java
@@ -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());
+ }
+}
diff --git a/examples/demo-template/service-account/src/main/webapp/index.html b/examples/demo-template/service-account/src/main/webapp/index.html
new file mode 100644
index 0000000..e2820d1
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/index.html
@@ -0,0 +1,5 @@
+<html>
+ <head>
+ <meta http-equiv="Refresh" content="0; URL=app">
+ </head>
+</html>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..9c1bac9
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
@@ -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>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json
new file mode 100644
index 0000000..7eec22a
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak.json
@@ -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"
+ }
+}
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp
new file mode 100644
index 0000000..e151f96
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/page.jsp
@@ -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>
\ No newline at end of file
diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..5dc7103
--- /dev/null
+++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/web.xml
@@ -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>
\ No newline at end of file
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index d592010..a26a058 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -162,6 +162,12 @@
"publicClient": true,
"directGrantsOnly": true,
"consentRequired": true
+ },
+ {
+ "clientId": "product-sa-client",
+ "enabled": true,
+ "secret": "password",
+ "serviceAccountsEnabled": true
}
],
"clientScopeMappings": {