keycloak-aplcache

Details

diff --git a/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java b/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
index 0a759a5..85c04a0 100644
--- a/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
+++ b/core/src/main/java/org/keycloak/util/RefreshTokenUtil.java
@@ -3,6 +3,7 @@ package org.keycloak.util;
 import java.io.IOException;
 
 import org.keycloak.OAuth2Constants;
+import org.keycloak.jose.jws.JWSInput;
 import org.keycloak.representations.RefreshToken;
 
 /**
@@ -30,7 +31,7 @@ public class RefreshTokenUtil {
 
 
     /**
-     * Return refresh token or offline otkne
+     * Return refresh token or offline token
      *
      * @param decodedToken
      * @return
@@ -39,9 +40,9 @@ public class RefreshTokenUtil {
         return JsonSerialization.readValue(decodedToken, RefreshToken.class);
     }
 
-    private static RefreshToken getRefreshToken(String refreshToken) throws IOException {
-        byte[] decodedToken = Base64Url.decode(refreshToken);
-        return getRefreshToken(decodedToken);
+    public static RefreshToken getRefreshToken(String refreshToken) throws IOException {
+        byte[] encodedContent = new JWSInput(refreshToken).getContent();
+        return getRefreshToken(encodedContent);
     }
 
     /**
diff --git a/examples/demo-template/customer-app/src/main/webapp/index.html b/examples/demo-template/customer-app/src/main/webapp/index.html
old mode 100755
new mode 100644
diff --git a/examples/demo-template/offline-access-app/pom.xml b/examples/demo-template/offline-access-app/pom.xml
new file mode 100644
index 0000000..f6a6020
--- /dev/null
+++ b/examples/demo-template/offline-access-app/pom.xml
@@ -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-examples-demo-parent</artifactId>
+        <groupId>org.keycloak</groupId>
+        <version>1.6.0.Final-SNAPSHOT</version>
+    </parent>
+
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>org.keycloak.example.demo</groupId>
+    <artifactId>offline-access-example</artifactId>
+    <packaging>war</packaging>
+    <name>Offline Access Portal</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.keycloak</groupId>
+            <artifactId>keycloak-core</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-adapter-spi</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>offline-access-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>
diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
new file mode 100644
index 0000000..b233574
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java
@@ -0,0 +1,226 @@
+package org.keycloak.example;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import javax.security.cert.X509Certificate;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.keycloak.KeycloakSecurityContext;
+import org.keycloak.adapters.AdapterDeploymentContext;
+import org.keycloak.adapters.HttpFacade;
+import org.keycloak.adapters.KeycloakDeployment;
+import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
+import org.keycloak.adapters.ServerRequest;
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.representations.AccessTokenResponse;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.KeycloakUriBuilder;
+import org.keycloak.util.RefreshTokenUtil;
+import org.keycloak.util.StreamUtil;
+import org.keycloak.util.Time;
+import org.keycloak.util.UriUtils;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineAccessPortalServlet extends HttpServlet {
+
+
+    @Override
+    public void init() throws ServletException {
+        getServletContext().setAttribute(HttpClient.class.getName(), new DefaultHttpClient());
+    }
+
+    @Override
+    public void destroy() {
+        getHttpClient().getConnectionManager().shutdown();
+    }
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
+
+        if (req.getRequestURI().endsWith("/login")) {
+            storeToken(req);
+            req.getRequestDispatcher("/WEB-INF/pages/loginCallback.jsp").forward(req, resp);
+            return;
+        }
+
+        String refreshToken = RefreshTokenDAO.loadToken();
+        String refreshTokenInfo;
+        boolean savedTokenAvailable;
+        if (refreshToken == null) {
+            refreshTokenInfo = "No token saved in database. Please login first";
+            savedTokenAvailable = false;
+        } else {
+            RefreshToken refreshTokenDecoded = RefreshTokenUtil.getRefreshToken(refreshToken);
+            String exp = (refreshTokenDecoded.getExpiration() == 0) ? "NEVER" : Time.toDate(refreshTokenDecoded.getExpiration()).toString();
+            refreshTokenInfo = String.format("<p>Type: %s</p><p>ID: %s</p><p>Expires: %s</p>", refreshTokenDecoded.getType(), refreshTokenDecoded.getId(), exp);
+            savedTokenAvailable = true;
+        }
+        req.setAttribute("tokenInfo", refreshTokenInfo);
+        req.setAttribute("savedTokenAvailable", savedTokenAvailable);
+
+        String customers;
+        if (req.getRequestURI().endsWith("/loadCustomers")) {
+            customers = loadCustomers(req, refreshToken);
+        } else {
+            customers = "";
+        }
+        req.setAttribute("customers", customers);
+
+        req.getRequestDispatcher("/WEB-INF/pages/view.jsp").forward(req, resp);
+    }
+
+    private void storeToken(HttpServletRequest req) throws IOException {
+        RefreshableKeycloakSecurityContext ctx = (RefreshableKeycloakSecurityContext) req.getAttribute(KeycloakSecurityContext.class.getName());
+        String refreshToken = ctx.getRefreshToken();
+
+        RefreshTokenDAO.saveToken(refreshToken);
+
+        RefreshToken refreshTokenDecoded = RefreshTokenUtil.getRefreshToken(refreshToken);
+        Boolean isOfflineToken = refreshTokenDecoded.getType().equals(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
+        req.setAttribute("isOfflineToken", isOfflineToken);
+    }
+
+    private String loadCustomers(HttpServletRequest req, String refreshToken) throws ServletException, IOException {
+        // Retrieve accessToken first with usage of refresh (offline) token from DB
+        String accessToken = null;
+        try {
+            KeycloakDeployment deployment = getDeployment(req);
+            AccessTokenResponse response = ServerRequest.invokeRefresh(deployment, refreshToken);
+            accessToken = response.getToken();
+        } catch (ServerRequest.HttpFailure failure) {
+            return "Failed to refresh token. Status from auth-server request: " + failure.getStatus() + ", Error: " + failure.getError();
+        }
+
+        // Load customers now
+        HttpGet get = new HttpGet(UriUtils.getOrigin(req.getRequestURL().toString()) + "/database/customers");
+        get.addHeader("Authorization", "Bearer " + accessToken);
+
+        HttpResponse response = getHttpClient().execute(get);
+        InputStream is = response.getEntity().getContent();
+        try {
+            if (response.getStatusLine().getStatusCode() != 200) {
+                return "Error when loading customer. Status: " + response.getStatusLine().getStatusCode() + ", error: " + StreamUtil.readString(is);
+            } else {
+                List<String> list = JsonSerialization.readValue(is, TypedList.class);
+                StringBuilder result = new StringBuilder();
+                for (String customer : list) {
+                    result.append(customer + "<br />");
+                }
+                return result.toString();
+            }
+        } finally {
+            is.close();
+        }
+    }
+
+
+    private KeycloakDeployment getDeployment(HttpServletRequest servletRequest) throws ServletException {
+        // The facade object is needed just if you have relative "auth-server-url" in keycloak.json. Otherwise you can call deploymentContext.resolveDeployment(null)
+        HttpFacade facade = getFacade(servletRequest);
+
+        AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) getServletContext().getAttribute(AdapterDeploymentContext.class.getName());
+        if (deploymentContext == null) {
+            throw new ServletException("AdapterDeploymentContext not set");
+        }
+        return deploymentContext.resolveDeployment(facade);
+    }
+
+    // TODO: Merge with facade in ServletOAuthClient and move to some common servlet adapter
+    private HttpFacade getFacade(final HttpServletRequest servletRequest) {
+        return new HttpFacade() {
+
+            @Override
+            public Request getRequest() {
+                return new Request() {
+
+                    @Override
+                    public String getMethod() {
+                        return servletRequest.getMethod();
+                    }
+
+                    @Override
+                    public String getURI() {
+                        return servletRequest.getRequestURL().toString();
+                    }
+
+                    @Override
+                    public boolean isSecure() {
+                        return servletRequest.isSecure();
+                    }
+
+                    @Override
+                    public String getQueryParamValue(String param) {
+                        return servletRequest.getParameter(param);
+                    }
+
+                    @Override
+                    public String getFirstParam(String param) {
+                        return servletRequest.getParameter(param);
+                    }
+
+                    @Override
+                    public Cookie getCookie(String cookieName) {
+                        // not needed
+                        return null;
+                    }
+
+                    @Override
+                    public String getHeader(String name) {
+                        return servletRequest.getHeader(name);
+                    }
+
+                    @Override
+                    public List<String> getHeaders(String name) {
+                        // not needed
+                        return null;
+                    }
+
+                    @Override
+                    public InputStream getInputStream() {
+                        try {
+                            return servletRequest.getInputStream();
+                        } catch (IOException ioe) {
+                            throw new RuntimeException(ioe);
+                        }
+                    }
+
+                    @Override
+                    public String getRemoteAddr() {
+                        return servletRequest.getRemoteAddr();
+                    }
+                };
+            }
+
+            @Override
+            public Response getResponse() {
+                throw new IllegalStateException("Not yet implemented");
+            }
+
+            @Override
+            public X509Certificate[] getCertificateChain() {
+                throw new IllegalStateException("Not yet implemented");
+            }
+        };
+    }
+
+    private HttpClient getHttpClient() {
+        return (HttpClient) getServletContext().getAttribute(HttpClient.class.getName());
+    }
+
+    static class TypedList extends ArrayList<String> {
+    }
+}
diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineExampleUris.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineExampleUris.java
new file mode 100644
index 0000000..0e56c71
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineExampleUris.java
@@ -0,0 +1,27 @@
+package org.keycloak.example;
+
+import org.keycloak.constants.ServiceUrlConstants;
+import org.keycloak.util.KeycloakUriBuilder;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineExampleUris {
+
+
+    public static final String LOGIN_CLASSIC = "/offline-access-portal/app/login";
+
+
+    public static final String LOGIN_WITH_OFFLINE_TOKEN = "/offline-access-portal/app/login?scope=offline_access";
+
+
+    public static final String LOAD_CUSTOMERS = "/offline-access-portal/app/loadCustomers";
+
+
+    public static final String ACCOUNT_MGMT = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH + "/applications")
+            .queryParam("referrer", "offline-access-portal").build("demo").toString();
+
+
+    public static final String LOGOUT = KeycloakUriBuilder.fromUri("/auth").path(ServiceUrlConstants.TOKEN_SERVICE_LOGOUT_PATH)
+            .queryParam("redirect_uri", "/offline-access-portal").build("demo").toString();
+}
diff --git a/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/RefreshTokenDAO.java b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/RefreshTokenDAO.java
new file mode 100644
index 0000000..2c94e19
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/RefreshTokenDAO.java
@@ -0,0 +1,47 @@
+package org.keycloak.example;
+
+import java.io.BufferedWriter;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+import org.keycloak.util.StreamUtil;
+
+/**
+ * Very simple DAO, which stores/loads just one token per whole application into file in tmp directory. Useful just for example purposes.
+ * In real environment, token should be stored in database.
+ *
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class RefreshTokenDAO {
+
+    public static final String FILE = System.getProperty("java.io.tmpdir") + "/offline-access-portal";
+
+    public static void saveToken(final String token) throws IOException {
+        PrintWriter writer = null;
+        try {
+            writer = new PrintWriter(new BufferedWriter(new FileWriter(FILE)));
+            writer.print(token);
+        } finally {
+            if (writer != null) {
+                writer.close();
+            }
+        }
+    }
+
+    public static String loadToken() throws IOException {
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(FILE);
+            return StreamUtil.readString(fis);
+        } catch (FileNotFoundException fnfe) {
+            return null;
+        } finally {
+            if (fis != null) {
+                fis.close();
+            }
+        }
+    }
+}
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/index.html b/examples/demo-template/offline-access-app/src/main/webapp/index.html
new file mode 100644
index 0000000..258d8f0
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/index.html
@@ -0,0 +1,3 @@
+<script>
+    window.location = "/offline-access-portal/app";
+</script>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
new file mode 100644
index 0000000..1abcf3f
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml
@@ -0,0 +1,10 @@
+<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"/>
+            <module name="org.keycloak.keycloak-adapter-spi"/>
+        </dependencies>
+    </deployment>
+</jboss-deployment-structure>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/keycloak.json
new file mode 100644
index 0000000..dff976c
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/keycloak.json
@@ -0,0 +1,10 @@
+{
+  "realm": "demo",
+  "resource": "offline-access-portal",
+  "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
+  "auth-server-url": "/auth",
+  "ssl-required" : "external",
+  "credentials": {
+    "secret": "password"
+  }
+}
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/loginCallback.jsp b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/loginCallback.jsp
new file mode 100644
index 0000000..31749f0
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/loginCallback.jsp
@@ -0,0 +1,35 @@
+<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
+         pageEncoding="ISO-8859-1" %>
+<%@ page session="false" %>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+    <head>
+        <title>Offline Access Example</title>
+    </head>
+    <body bgcolor="#ffffff">
+        <h1>Offline Access Example</h1>
+
+        <hr />
+
+        <p>
+            Login finished and refresh token saved successfully.
+        </p>
+
+        <p>
+            <div style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;">
+                <% if ((Boolean) request.getAttribute("isOfflineToken")) { %>
+                    Token type <b>is</b> offline token! You will be able to load customers even after logout or server restart. Offline token can be revoked in account management or by admin in admin console.
+                <% } else { %>
+                    Token <b>is not</b> offline token! Once you logout or restart server, token won't be valid anymore and you won't be able to load customers.
+                <% } %>
+            </div>
+        </p>
+
+        <p>
+            <a href="/offline-access-portal/app">Back to home page</a>
+        </p>
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/view.jsp b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/view.jsp
new file mode 100644
index 0000000..135a944
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/view.jsp
@@ -0,0 +1,45 @@
+<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
+         pageEncoding="ISO-8859-1" %>
+<%@ page import="org.keycloak.example.OfflineExampleUris" %>
+<%@ page session="false" %>
+
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+    <head>
+        <title>Offline Access Example</title>
+    </head>
+    <body bgcolor="#ffffff">
+        <h1>Offline Access Example</h1>
+
+        <hr />
+
+        <% if (request.getRemoteUser() == null) { %>
+            <a href="<%= OfflineExampleUris.LOGIN_CLASSIC %>">Login classic</a> |
+            <a href="<%= OfflineExampleUris.LOGIN_WITH_OFFLINE_TOKEN %>">Login with offline access</a> |
+        <% } else { %>
+            <a href='<%= OfflineExampleUris.LOGOUT %>'>Logout</a> |
+        <% } %>
+
+        <a href='<%= OfflineExampleUris.ACCOUNT_MGMT %>'>Account management</a> |
+
+        <% if ((Boolean) request.getAttribute("savedTokenAvailable")) { %>
+            <a href="<%= OfflineExampleUris.LOAD_CUSTOMERS %>">Load customers with saved token</a> |
+        <% } %>
+
+        <hr />
+
+        <h2>Saved Refresh Token Info</h2>
+        <div style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;">
+            <%= request.getAttribute("tokenInfo") %>
+        </div>
+
+        <hr />
+
+        <h2>Customers</h2>
+        <div style="background-color: #ddd; border: 1px solid #ccc; padding: 10px;">
+            <%= request.getAttribute("customers") %>
+        </div>
+
+    </body>
+</html>
\ No newline at end of file
diff --git a/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/web.xml b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000..2092b4e
--- /dev/null
+++ b/examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,50 @@
+<?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>offline-access-portal</module-name>
+
+    <servlet>
+        <servlet-name>OfflineAccessPortalServle</servlet-name>
+        <servlet-class>org.keycloak.example.OfflineAccessPortalServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>OfflineAccessPortalServle</servlet-name>
+        <url-pattern>/app/*</url-pattern>
+    </servlet-mapping>
+
+    <security-constraint>
+        <web-resource-collection>
+            <web-resource-name>User</web-resource-name>
+            <url-pattern>/app/login/*</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>demo</realm-name>
+    </login-config>
+
+    <security-role>
+        <role-name>admin</role-name>
+    </security-role>
+    <security-role>
+        <role-name>user</role-name>
+    </security-role>
+</web-app>
\ No newline at end of file
diff --git a/examples/demo-template/pom.xml b/examples/demo-template/pom.xml
index 347a483..bc0011d 100755
--- a/examples/demo-template/pom.xml
+++ b/examples/demo-template/pom.xml
@@ -37,6 +37,7 @@
         <module>third-party</module>
         <module>third-party-cdi</module>
         <module>service-account</module>
+        <module>offline-access-app</module>
     </modules>
 
     <profiles>
diff --git a/examples/demo-template/README.md b/examples/demo-template/README.md
index 2445e31..35b6db8 100755
--- a/examples/demo-template/README.md
+++ b/examples/demo-template/README.md
@@ -216,7 +216,19 @@ An example for retrieve service account dedicated to the Client Application itse
 
 [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) 
+Client authentication is done with OAuth2 Client Credentials Grant in out-of-bound request (Not Keycloak login screen displayed) .
+
+The example also shows different methods of client authentication. There is ProductSAClientSecretServlet using traditional authentication with clientId and client_secret, 
+but there is also ProductSAClientSignedJWTServlet using client authentication with JWT signed by client private key.  
+
+Step 11: Offline Access Example
+===============================
+An example for retrieve offline token, which is then saved to the database and can be used by application anytime later. Offline token
+is valid even if user is already logged out from SSO. Server restart also won't invalidate offline token. Offline token can be revoked by the user in 
+account management or by admin in admin console.
+
+[http://localhost:8080/offline-access-portal](http://localhost:8080/offline-access-portal)
+
 
 Admin Console
 ==========================
diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
index 58c5f9b..ce5ff79 100644
--- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
+++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java
@@ -1,6 +1,9 @@
 package org.keycloak.example;
 
 /**
+ * Client authentication based on JWT signed by client private key .
+ * See Keycloak documentation and <a href="https://tools.ietf.org/html/rfc7519">specs</a> for more details.
+ *
  * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
  */
 public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet {
diff --git a/examples/demo-template/testrealm.json b/examples/demo-template/testrealm.json
index 0ba235f..309afdd 100755
--- a/examples/demo-template/testrealm.json
+++ b/examples/demo-template/testrealm.json
@@ -22,7 +22,7 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": [ "user" ],
+            "realmRoles": [ "user", "offline_access"  ],
             "clientRoles": {
                 "account": [ "manage-account" ]
             }
@@ -37,7 +37,7 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": [ "user" ],
+            "realmRoles": [ "user", "offline_access"  ],
             "clientRoles": {
                 "account": [ "manage-account" ]
             }
@@ -52,7 +52,7 @@
                 { "type" : "password",
                     "value" : "password" }
             ],
-            "realmRoles": [ "user" ],
+            "realmRoles": [ "user", "offline_access" ],
             "clientRoles": {
                 "account": [ "manage-account" ]
             }
@@ -103,6 +103,10 @@
         {
             "client": "third-party",
             "roles": ["user"]
+        },
+        {
+            "client": "offline-access-portal",
+            "roles": ["user", "offline_access"]
         }
     ],
     "clients": [
@@ -190,6 +194,17 @@
             "attributes": {
                 "jwt.credential.certificate": "MIICnTCCAYUCBgFPPLDaTzANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdjbGllbnQxMB4XDTE1MDgxNzE3MjI0N1oXDTI1MDgxNzE3MjQyN1owEjEQMA4GA1UEAwwHY2xpZW50MTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAIUjjgv+V3s96O+Za9002Lp/trtGuHBeaeVL9dFKMKzO2MPqdRmHB4PqNlDdd28Rwf5Xn6iWdFpyUKOnI/yXDLhdcuFpR0sMNK/C9Lt+hSpPFLuzDqgtPgDotlMxiHIWDOZ7g9/gPYNXbNvjv8nSiyqoguoCQiiafW90bPHsiVLdP7ZIUwCcfi1qQm7FhxRJ1NiW5dvUkuCnnWEf0XR+Wzc5eC9EgB0taLFiPsSEIlWMm5xlahYyXkPdNOqZjiRnrTWm5Y4uk8ZcsD/KbPTf/7t7cQXipVaswgjdYi1kK2/zRwOhg1QwWFX/qmvdd+fLxV0R6VqRDhn7Qep2cxwMxLsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAKE6OA46sf20bz8LZPoiNsqRwBUDkaMGXfnob7s/hJZIIwDEx0IAQ3uKsG7q9wb+aA6s+v7S340zb2k3IxuhFaHaZpAd4CyR5cn1FHylbzoZ7rI/3ASqHDqpljdJaFqPH+m7nZWtyDvtZf+gkZ8OjsndwsSBK1d/jMZPp29qYbl1+XfO7RCp/jDqro/R3saYFaIFiEZPeKn1hUJn6BO48vxH1xspSu9FmlvDOEAOz4AuM58z4zRMP49GcFdCWr1wkonJUHaSptJaQwmBwLFUkCbE5I1ixGMb7mjEud6Y5jhfzJiZMo2U8RfcjNbrN0diZl3jB6LQIwESnhYSghaTjNQ=="
             }
+        },
+        {
+            "clientId": "offline-access-portal",
+            "enabled": true,
+            "consentRequired": true,
+            "adminUrl": "/offline-access-portal",
+            "baseUrl": "/offline-access-portal",
+            "redirectUris": [
+                "/offline-access-portal/*"
+            ],
+            "secret": "password"
         }
     ],
     "clientScopeMappings": {