keycloak-memoizeit

Details

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>
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": {