KEYCLOAK-904 Offline portal example added
This commit is contained in:
parent
df46a8ea0d
commit
018866aa81
16 changed files with 566 additions and 8 deletions
|
@ -3,6 +3,7 @@ package org.keycloak.util;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
import org.keycloak.representations.RefreshToken;
|
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
|
* @param decodedToken
|
||||||
* @return
|
* @return
|
||||||
|
@ -39,9 +40,9 @@ public class RefreshTokenUtil {
|
||||||
return JsonSerialization.readValue(decodedToken, RefreshToken.class);
|
return JsonSerialization.readValue(decodedToken, RefreshToken.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static RefreshToken getRefreshToken(String refreshToken) throws IOException {
|
public static RefreshToken getRefreshToken(String refreshToken) throws IOException {
|
||||||
byte[] decodedToken = Base64Url.decode(refreshToken);
|
byte[] encodedContent = new JWSInput(refreshToken).getContent();
|
||||||
return getRefreshToken(decodedToken);
|
return getRefreshToken(encodedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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)
|
[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
|
Admin Console
|
||||||
==========================
|
==========================
|
||||||
|
|
0
examples/demo-template/customer-app/src/main/webapp/index.html
Executable file → Normal file
0
examples/demo-template/customer-app/src/main/webapp/index.html
Executable file → Normal file
73
examples/demo-template/offline-access-app/pom.xml
Normal file
73
examples/demo-template/offline-access-app/pom.xml
Normal file
|
@ -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>
|
|
@ -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> {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -0,0 +1,3 @@
|
||||||
|
<script>
|
||||||
|
window.location = "/offline-access-portal/app";
|
||||||
|
</script>
|
|
@ -37,6 +37,7 @@
|
||||||
<module>third-party</module>
|
<module>third-party</module>
|
||||||
<module>third-party-cdi</module>
|
<module>third-party-cdi</module>
|
||||||
<module>service-account</module>
|
<module>service-account</module>
|
||||||
|
<module>offline-access-app</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<profiles>
|
<profiles>
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
package org.keycloak.example;
|
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>
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
*/
|
*/
|
||||||
public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet {
|
public class ProductSAClientSignedJWTServlet extends ProductServiceAccountServlet {
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
{ "type" : "password",
|
{ "type" : "password",
|
||||||
"value" : "password" }
|
"value" : "password" }
|
||||||
],
|
],
|
||||||
"realmRoles": [ "user" ],
|
"realmRoles": [ "user", "offline_access" ],
|
||||||
"clientRoles": {
|
"clientRoles": {
|
||||||
"account": [ "manage-account" ]
|
"account": [ "manage-account" ]
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
{ "type" : "password",
|
{ "type" : "password",
|
||||||
"value" : "password" }
|
"value" : "password" }
|
||||||
],
|
],
|
||||||
"realmRoles": [ "user" ],
|
"realmRoles": [ "user", "offline_access" ],
|
||||||
"clientRoles": {
|
"clientRoles": {
|
||||||
"account": [ "manage-account" ]
|
"account": [ "manage-account" ]
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
{ "type" : "password",
|
{ "type" : "password",
|
||||||
"value" : "password" }
|
"value" : "password" }
|
||||||
],
|
],
|
||||||
"realmRoles": [ "user" ],
|
"realmRoles": [ "user", "offline_access" ],
|
||||||
"clientRoles": {
|
"clientRoles": {
|
||||||
"account": [ "manage-account" ]
|
"account": [ "manage-account" ]
|
||||||
}
|
}
|
||||||
|
@ -103,6 +103,10 @@
|
||||||
{
|
{
|
||||||
"client": "third-party",
|
"client": "third-party",
|
||||||
"roles": ["user"]
|
"roles": ["user"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client": "offline-access-portal",
|
||||||
|
"roles": ["user", "offline_access"]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"clients": [
|
"clients": [
|
||||||
|
@ -190,6 +194,17 @@
|
||||||
"attributes": {
|
"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=="
|
"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": {
|
"clientScopeMappings": {
|
||||||
|
|
Loading…
Reference in a new issue