keycloak-aplcache
Changes
examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineAccessPortalServlet.java 226(+226 -0)
examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/OfflineExampleUris.java 27(+27 -0)
examples/demo-template/offline-access-app/src/main/java/org/keycloak/example/RefreshTokenDAO.java 47(+47 -0)
examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/jboss-deployment-structure.xml 10(+10 -0)
examples/demo-template/offline-access-app/src/main/webapp/WEB-INF/pages/loginCallback.jsp 35(+35 -0)
examples/demo-template/pom.xml 1(+1 -0)
examples/demo-template/README.md 14(+13 -1)
examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductSAClientSignedJWTServlet.java 3(+3 -0)
examples/demo-template/testrealm.json 21(+18 -3)
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
examples/demo-template/pom.xml 1(+1 -0)
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>
examples/demo-template/README.md 14(+13 -1)
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 {
examples/demo-template/testrealm.json 21(+18 -3)
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": {