keycloak-aplcache
Changes
core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java 35(+35 -0)
core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java 37(+37 -0)
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)
export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java 25(+25 -0)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java 2(+1 -1)
forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java 5(+3 -2)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 48(+48 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html 57(+57 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html 6(+6 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html 7(+7 -0)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java 105(+98 -7)
model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java 48(+0 -48)
model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java 7(+4 -3)
Details
diff --git a/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java
new file mode 100644
index 0000000..b2374bc
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/OfflineClientSessionRepresentation.java
@@ -0,0 +1,35 @@
+package org.keycloak.representations.idm;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineClientSessionRepresentation {
+
+ private String clientSessionId;
+ private String client; // clientId (not DB ID)
+ private String data;
+
+ public String getClientSessionId() {
+ return clientSessionId;
+ }
+
+ public void setClientSessionId(String clientSessionId) {
+ this.clientSessionId = clientSessionId;
+ }
+
+ public String getClient() {
+ return client;
+ }
+
+ public void setClient(String client) {
+ this.client = client;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java
new file mode 100644
index 0000000..e877c3e
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/OfflineUserSessionRepresentation.java
@@ -0,0 +1,37 @@
+package org.keycloak.representations.idm;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class OfflineUserSessionRepresentation {
+
+ private String userSessionId;
+ private String data;
+ private List<OfflineClientSessionRepresentation> offlineClientSessions;
+
+ public String getUserSessionId() {
+ return userSessionId;
+ }
+
+ public void setUserSessionId(String userSessionId) {
+ this.userSessionId = userSessionId;
+ }
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+
+ public List<OfflineClientSessionRepresentation> getOfflineClientSessions() {
+ return offlineClientSessions;
+ }
+
+ public void setOfflineClientSessions(List<OfflineClientSessionRepresentation> offlineClientSessions) {
+ this.offlineClientSessions = offlineClientSessions;
+ }
+}
diff --git a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
index d724fd7..99f8f3c 100755
--- a/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
+++ b/core/src/main/java/org/keycloak/representations/idm/UserRepresentation.java
@@ -36,6 +36,7 @@ public class UserRepresentation {
protected List<String> realmRoles;
protected Map<String, List<String>> clientRoles;
protected List<UserConsentRepresentation> clientConsents;
+ protected List<OfflineUserSessionRepresentation> offlineUserSessions;
@Deprecated
protected Map<String, List<String>> applicationRoles;
@@ -218,4 +219,12 @@ public class UserRepresentation {
public void setServiceAccountClientId(String serviceAccountClientId) {
this.serviceAccountClientId = serviceAccountClientId;
}
+
+ public List<OfflineUserSessionRepresentation> getOfflineUserSessions() {
+ return offlineUserSessions;
+ }
+
+ public void setOfflineUserSessions(List<OfflineUserSessionRepresentation> offlineUserSessions) {
+ this.offlineUserSessions = offlineUserSessions;
+ }
}
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/docbook/reference/en/en-US/modules/timeouts.xml b/docbook/reference/en/en-US/modules/timeouts.xml
index 08e0450..78418cb 100755
--- a/docbook/reference/en/en-US/modules/timeouts.xml
+++ b/docbook/reference/en/en-US/modules/timeouts.xml
@@ -1,4 +1,4 @@
- <chapter id="timeouts">
+<chapter id="timeouts">
<title>Cookie settings, Session Timeouts, and Token Lifespans</title>
<para>
Keycloak has a bunch of fine-grain settings to manage browser cookies, user login sessions, and token lifespans.
@@ -52,4 +52,38 @@
back to your application as an authentnicated user. This value is relatively short and is usually measured in minutes.
</para>
</section>
+ <section id="offline-access">
+ <title>Offline Access</title>
+ <para>
+ The Offline access is the feature described in <ulink url="http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess">OpenID Connect specification</ulink> .
+ The idea is that during login, your client application will request Offline token instead of classic Refresh token.
+ Then the application can save this offline token in the database and can use it anytime later even if user is logged out.
+ This is useful for example if your application needs to do some "offline" actions on behalf of user even if user is not online. For example
+ periodic backup of some data every night etc.
+ </para>
+ <para>
+ Your application is responsible for persist the offline token in some storage (usually database) and then use it to
+ manually retrieve new access token from Keycloak server.
+ </para>
+ <para>
+ The difference between classic Refresh token and Offline token is, that offline token will never expire and is not subject of <literal>SSO Session Idle timeout</literal> .
+ The offline token is valid even after user logout or server restart. User can revoke the offline tokens in Account management UI. The admin
+ user can revoke offline tokens for individual users in admin console (The <literal>Consent</literal> tab of particular user) and he can
+ see all the offline tokens of all users for particular client application in the settings of the client. Revoking of all offline tokens for particular
+ client is possible by set <literal>notBefore</literal> policy for the client.
+ </para>
+ <para>
+ For requesting the offline token, user needs to be in realm role <literal>offline_access</literal> and client needs to have
+ scope for this role. If client has <literal>Full scope allowed</literal>, the scope is granted by default. Also users are automatically
+ members of the role as it's the default role.
+ </para>
+ <para>
+ The client can request offline token by adding parameter <literal>scope=offline_access</literal>
+ when sending authorization request to Keycloak. The adapter automatically adds this parameter when you use it to access secured
+ URL of your application (ie. http://localhost:8080/customer-portal/secured?scope=offline_access ).
+ The <link linkend='direct-access-grants'>Direct Access Grant</link> or <link linkend="service-accounts">Service account</link> flows also support
+ offline tokens if you include <literal>scope=offline_access</literal> in the body of the authentication request. For more details,
+ see the <literal>offline-access-app</literal> example from Keycloak demo.
+ </para>
+ </section>
</chapter>
\ No newline at end of file
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": {
diff --git a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
index 5137491..88471d0 100755
--- a/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
+++ b/export-import/export-import-api/src/main/java/org/keycloak/exportimport/util/ExportUtils.java
@@ -1,5 +1,8 @@
package org.keycloak.exportimport.util;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
import org.keycloak.util.Base64;
import org.codehaus.jackson.JsonEncoding;
import org.codehaus.jackson.JsonFactory;
@@ -295,6 +298,28 @@ public class ExportUtils {
}
}
+ // Offline sessions
+ List<OfflineUserSessionRepresentation> offlineSessionReps = new LinkedList<>();
+ Collection<OfflineUserSessionModel> offlineSessions = session.users().getOfflineUserSessions(realm, user);
+ Collection<OfflineClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, user);
+
+ Map<String, List<OfflineClientSessionModel>> processed = new HashMap<>();
+ for (OfflineClientSessionModel clsm : offlineClientSessions) {
+ String userSessionId = clsm.getUserSessionId();
+ List<OfflineClientSessionModel> current = processed.get(userSessionId);
+ if (current == null) {
+ current = new LinkedList<>();
+ processed.put(userSessionId, current);
+ }
+ current.add(clsm);
+ }
+
+ for (OfflineUserSessionModel userSession : offlineSessions) {
+ OfflineUserSessionRepresentation sessionRep = ModelToRepresentation.toRepresentation(realm, userSession, processed.get(userSession.getUserSessionId()));
+ offlineSessionReps.add(sessionRep);
+ }
+ userRep.setOfflineUserSessions(offlineSessionReps);
+
return userRep;
}
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
index a4b3771..f782543 100755
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/FreeMarkerAccountProvider.java
@@ -202,7 +202,7 @@ public class FreeMarkerAccountProvider implements AccountProvider {
attributes.put("sessions", new SessionsBean(realm, sessions));
break;
case APPLICATIONS:
- attributes.put("applications", new ApplicationsBean(realm, user));
+ attributes.put("applications", new ApplicationsBean(session, realm, user));
attributes.put("advancedMsg", new AdvancedMessageFormatterMethod(locale, messagesBundle));
break;
case PASSWORD:
diff --git a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
index 371b5d0..71bf200 100644
--- a/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
+++ b/forms/account-freemarker/src/main/java/org/keycloak/account/freemarker/model/ApplicationsBean.java
@@ -6,6 +6,7 @@ import java.util.List;
import java.util.Set;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.UserConsentModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
@@ -22,9 +23,9 @@ public class ApplicationsBean {
private List<ApplicationEntry> applications = new LinkedList<ApplicationEntry>();
- public ApplicationsBean(RealmModel realm, UserModel user) {
+ public ApplicationsBean(KeycloakSession session, RealmModel realm, UserModel user) {
- Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(realm, user);
+ Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user);
List<ClientModel> realmClients = realm.getClients();
for (ClientModel client : realmClients) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index 350f0d0..a965325 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -637,6 +637,21 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'ClientSessionsCtrl'
})
+ .when('/realms/:realm/clients/:client/offline-access', {
+ templateUrl : resourceUrl + '/partials/client-offline-sessions.html',
+ resolve : {
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ },
+ client : function(ClientLoader) {
+ return ClientLoader();
+ },
+ offlineSessionCount : function(ClientOfflineSessionCountLoader) {
+ return ClientOfflineSessionCountLoader();
+ }
+ },
+ controller : 'ClientOfflineSessionsCtrl'
+ })
.when('/realms/:realm/clients/:client/credentials', {
templateUrl : resourceUrl + '/partials/client-credentials.html',
resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index 3cb3e60..27a9794 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -507,6 +507,54 @@ module.controller('ClientSessionsCtrl', function($scope, realm, sessionCount, cl
};
});
+module.controller('ClientOfflineSessionsCtrl', function($scope, realm, offlineSessionCount, client,
+ ClientOfflineSessions) {
+ $scope.realm = realm;
+ $scope.count = offlineSessionCount.count;
+ $scope.sessions = [];
+ $scope.client = client;
+
+ $scope.page = 0;
+
+ $scope.query = {
+ realm : realm.realm,
+ client: $scope.client.id,
+ max : 5,
+ first : 0
+ }
+
+ $scope.firstPage = function() {
+ $scope.query.first = 0;
+ if ($scope.query.first < 0) {
+ $scope.query.first = 0;
+ }
+ $scope.loadUsers();
+ }
+
+ $scope.previousPage = function() {
+ $scope.query.first -= parseInt($scope.query.max);
+ if ($scope.query.first < 0) {
+ $scope.query.first = 0;
+ }
+ $scope.loadUsers();
+ }
+
+ $scope.nextPage = function() {
+ $scope.query.first += parseInt($scope.query.max);
+ $scope.loadUsers();
+ }
+
+ $scope.toDate = function(val) {
+ return new Date(val);
+ };
+
+ $scope.loadUsers = function() {
+ ClientOfflineSessions.query($scope.query, function(updated) {
+ $scope.sessions = updated;
+ })
+ };
+});
+
module.controller('ClientRoleDetailCtrl', function($scope, realm, client, role, roles, clients,
Role, ClientRole, RoleById, RoleRealmComposites, RoleClientComposites,
$http, $location, Dialog, Notifications) {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
index b2a4870..3746740 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js
@@ -208,9 +208,9 @@ module.controller('UserConsentsCtrl', function($scope, realm, user, userConsents
UserConsents.query({realm: realm.realm, user: user.id}, function(updated) {
$scope.userConsents = updated;
})
- Notifications.success('Consent revoked successfully');
+ Notifications.success('Grant revoked successfully');
}, function() {
- Notifications.error("Consent couldn't be revoked");
+ Notifications.error("Grant couldn't be revoked");
});
console.log("Revoke consent " + clientId);
}
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
index 6b09bc1..9c05940 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/loaders.js
@@ -246,6 +246,15 @@ module.factory('ClientSessionCountLoader', function(Loader, ClientSessionCount,
});
});
+module.factory('ClientOfflineSessionCountLoader', function(Loader, ClientOfflineSessionCount, $route, $q) {
+ return Loader.get(ClientOfflineSessionCount, function() {
+ return {
+ realm : $route.current.params.realm,
+ client : $route.current.params.client
+ }
+ });
+});
+
module.factory('ClientClaimsLoader', function(Loader, ClientClaims, $route, $q) {
return Loader.get(ClientClaims, function() {
return {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
index 8d56c90..03f4b99 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js
@@ -841,6 +841,20 @@ module.factory('ClientUserSessions', function($resource) {
});
});
+module.factory('ClientOfflineSessionCount', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/offline-session-count', {
+ realm : '@realm',
+ client : "@client"
+ });
+});
+
+module.factory('ClientOfflineSessions', function($resource) {
+ return $resource(authUrl + '/admin/realms/:realm/clients/:client/offline-sessions', {
+ realm : '@realm',
+ client : "@client"
+ });
+});
+
module.factory('ClientLogoutAll', function($resource) {
return $resource(authUrl + '/admin/realms/:realm/clients/:client/logout-all', {
realm : '@realm',
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html
new file mode 100644
index 0000000..c8afb02
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/client-offline-sessions.html
@@ -0,0 +1,57 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <ol class="breadcrumb">
+ <li><a href="#/realms/{{realm.realm}}/clients">Clients</a></li>
+ <li>{{client.clientId}}</li>
+ </ol>
+
+ <kc-tabs-client></kc-tabs-client>
+
+ <form class="form-horizontal" name="sessionStats">
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label class="col-md-2 control-label" for="activeSessions">Offline Tokens</label>
+ <div class="col-md-6">
+ <input class="form-control" type="text" id="activeSessions" name="activeSessions" data-ng-model="count" ng-disabled="true">
+ </div>
+ <kc-tooltip>Total number of active offline tokens for this client.</kc-tooltip>
+ </div>
+ </fieldset>
+ </form>
+ <table class="table table-striped table-bordered" data-ng-show="count > 0">
+ <thead>
+ <tr>
+ <th class="kc-table-actions" colspan="3">
+ <div class="pull-right">
+ <a class="btn btn-default" ng-click="loadUsers()" tooltip-placement="left" tooltip-trigger="mouseover mouseout" tooltip="Warning, this is a potentially expensive operation depending on number of offline tokens.">Show Offline Tokens</a>
+ </div>
+ </th>
+ </tr>
+ <tr data-ng-show="sessions">
+ <th>User</th>
+ <th>From IP</th>
+ <th>Token Issued</th>
+ </tr>
+ </thead>
+ <tfoot data-ng-show="sessions && (sessions.length >= 5 || query.first != 0)">
+ <tr>
+ <td colspan="7">
+ <div class="table-nav">
+ <button data-ng-click="firstPage()" class="first" ng-disabled="query.first == 0">First page</button>
+ <button data-ng-click="previousPage()" class="prev" ng-disabled="query.first == 0">Previous page</button>
+ <button data-ng-click="nextPage()" class="next" ng-disabled="sessions.length < query.max">Next page</button>
+ </div>
+ </td>
+ </tr>
+ </tfoot>
+ <tbody>
+ <tr data-ng-repeat="session in sessions">
+ <td><a href="#/realms/{{realm.realm}}/users/{{session.userId}}">{{session.username}}</a></td>
+ <td>{{session.ipAddress}}</td>
+ <td>{{session.start | date:'medium'}}</td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
index 22db348..cf6db99 100644
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/user-consents.html
@@ -12,6 +12,7 @@
<th>Client</th>
<th>Granted Roles</th>
<th>Granted Protocol Mappers</th>
+ <th>Additional Grants</th>
<th>Action</th>
</tr>
</thead>
@@ -35,6 +36,11 @@
</span>
</span>
</td>
+ <td>
+ <span data-ng-repeat="additionalGrant in consent.additionalGrants">
+ <span ng-if="!$first">, </span>{{additionalGrant}}
+ </span>
+ </td>
<td class="kc-action-cell">
<button class="btn btn-default btn-block btn-sm" ng-click="revokeConsent(consent.clientId)">
<i class="pficon pficon-delete"></i> Revoke
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
index 2aad2d5..f033aac 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-tabs-client.html
@@ -26,6 +26,13 @@
<kc-tooltip>View active sessions for this client. Allows you to see which users are active and when they logged in.</kc-tooltip>
</li>
+ <li ng-class="{active: path[4] == 'offline-access'}" data-ng-show="!client.bearerOnly">
+ <a href="#/realms/{{realm.realm}}/clients/{{client.id}}/offline-access">Offline Access</a>
+ <kc-tooltip>View offline sessions for this client. Allows you to see which users retrieve offline token and when they retrieve it.
+ To revoke all tokens for the client, go to Revocation tab and set new not before value.
+ </kc-tooltip>
+ </li>
+
<li ng-class="{active: path[4] == 'clustering'}" data-ng-show="!client.publicClient"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/clustering">Clustering</a></li>
<li ng-class="{active: path[4] == 'installation'}" data-ng-show="client.protocol != 'saml'">
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
index 8010586..e2db902 100644
--- a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_6_0.java
@@ -3,8 +3,10 @@ package org.keycloak.migration.migrators;
import java.util.List;
import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
import org.keycloak.models.utils.KeycloakModelUtils;
/**
@@ -17,6 +19,16 @@ public class MigrateTo1_6_0 {
public void migrate(KeycloakSession session) {
List<RealmModel> realms = session.realms().getRealms();
for (RealmModel realm : realms) {
+
+ for (RoleModel realmRole : realm.getRoles()) {
+ realmRole.setScopeParamRequired(false);
+ }
+ for (ClientModel client : realm.getClients()) {
+ for (RoleModel clientRole : client.getRoles()) {
+ clientRole.setScopeParamRequired(false);
+ }
+ }
+
KeycloakModelUtils.setupOfflineTokens(realm);
}
diff --git a/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
index 47ae23a..59e325d 100644
--- a/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
+++ b/model/api/src/main/java/org/keycloak/models/OfflineClientSessionModel.java
@@ -8,6 +8,7 @@ public class OfflineClientSessionModel {
private String clientSessionId;
private String userSessionId;
private String clientId;
+ private String userId;
private String data;
public String getClientSessionId() {
@@ -34,6 +35,14 @@ public class OfflineClientSessionModel {
this.clientId = clientId;
}
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
public String getData() {
return data;
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
index 13c05f8..0500308 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java
@@ -4,6 +4,7 @@ import org.jboss.logging.Logger;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
@@ -475,6 +476,70 @@ public class UserFederationManager implements UserProvider {
}
@Override
+ public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
+ validateUser(realm, user);
+ if (user == null) throw new IllegalStateException("Federated user no longer valid");
+ session.userStorage().addOfflineUserSession(realm, user, offlineUserSession);
+ }
+
+ @Override
+ public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+ validateUser(realm, user);
+ if (user == null) throw new IllegalStateException("Federated user no longer valid");
+ return session.userStorage().getOfflineUserSession(realm, user, userSessionId);
+ }
+
+ @Override
+ public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
+ validateUser(realm, user);
+ if (user == null) throw new IllegalStateException("Federated user no longer valid");
+ return session.userStorage().getOfflineUserSessions(realm, user);
+ }
+
+ @Override
+ public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+ validateUser(realm, user);
+ if (user == null) throw new IllegalStateException("Federated user no longer valid");
+ return session.userStorage().removeOfflineUserSession(realm, user, userSessionId);
+ }
+
+ @Override
+ public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
+ session.userStorage().addOfflineClientSession(realm, offlineClientSession);
+ }
+
+ @Override
+ public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+ validateUser(realm, user);
+ if (user == null) throw new IllegalStateException("Federated user no longer valid");
+ return session.userStorage().getOfflineClientSession(realm, user, clientSessionId);
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+ validateUser(realm, user);
+ if (user == null) throw new IllegalStateException("Federated user no longer valid");
+ return session.userStorage().getOfflineClientSessions(realm, user);
+ }
+
+ @Override
+ public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+ validateUser(realm, user);
+ if (user == null) throw new IllegalStateException("Federated user no longer valid");
+ return session.userStorage().removeOfflineClientSession(realm, user, clientSessionId);
+ }
+
+ @Override
+ public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+ return session.userStorage().getOfflineClientSessionsCount(realm, client);
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+ return session.userStorage().getOfflineClientSessions(realm, client, firstResult, maxResults);
+ }
+
+ @Override
public void close() {
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java
index 51e8fc4..f57bb04 100755
--- a/model/api/src/main/java/org/keycloak/models/UserModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserModel.java
@@ -114,15 +114,6 @@ public interface UserModel {
void updateConsent(UserConsentModel consent);
boolean revokeConsentForClient(String clientInternalId);
- void addOfflineUserSession(OfflineUserSessionModel offlineUserSession);
- OfflineUserSessionModel getOfflineUserSession(String userSessionId);
- Collection<OfflineUserSessionModel> getOfflineUserSessions();
- boolean removeOfflineUserSession(String userSessionId);
- void addOfflineClientSession(OfflineClientSessionModel offlineClientSession);
- OfflineClientSessionModel getOfflineClientSession(String clientSessionId);
- Collection<OfflineClientSessionModel> getOfflineClientSessions();
- boolean removeOfflineClientSession(String clientSessionId);
-
public static enum RequiredAction {
VERIFY_EMAIL, UPDATE_PROFILE, CONFIGURE_TOTP, UPDATE_PASSWORD
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java
index 8712f2b..0012b24 100755
--- a/model/api/src/main/java/org/keycloak/models/UserProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java
@@ -2,6 +2,7 @@ package org.keycloak.models;
import org.keycloak.provider.Provider;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -55,5 +56,17 @@ public interface UserProvider extends Provider {
boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input);
CredentialValidationOutput validCredentials(RealmModel realm, UserCredentialModel... input);
+ void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession);
+ OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId);
+ Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user);
+ boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId);
+ void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession);
+ OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId);
+ Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user);
+ boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId);
+
+ int getOfflineClientSessionsCount(RealmModel realm, ClientModel client);
+ Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int first, int max);
+
void close();
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
index 9906ab2..6b19606 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java
@@ -10,6 +10,8 @@ import org.keycloak.models.IdentityProviderMapperModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.ModelException;
import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -30,6 +32,8 @@ import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.OfflineClientSessionRepresentation;
+import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
@@ -43,6 +47,7 @@ import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.util.Time;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
@@ -506,7 +511,31 @@ public class ModelToRepresentation {
return rep;
}
+ public static OfflineUserSessionRepresentation toRepresentation(RealmModel realm, OfflineUserSessionModel model, Collection<OfflineClientSessionModel> clientSessions) {
+ OfflineUserSessionRepresentation rep = new OfflineUserSessionRepresentation();
+ rep.setData(model.getData());
+ rep.setUserSessionId(model.getUserSessionId());
+ List<OfflineClientSessionRepresentation> clientSessionReps = new LinkedList<>();
+ for (OfflineClientSessionModel clsm : clientSessions) {
+ OfflineClientSessionRepresentation clrep = toRepresentation(realm, clsm);
+ clientSessionReps.add(clrep);
+ }
+ rep.setOfflineClientSessions(clientSessionReps);
+ return rep;
+ }
+
+ public static OfflineClientSessionRepresentation toRepresentation(RealmModel realm, OfflineClientSessionModel model) {
+ OfflineClientSessionRepresentation rep = new OfflineClientSessionRepresentation();
+
+ String clientInternalId = model.getClientId();
+ ClientModel client = realm.getClientById(clientInternalId);
+ rep.setClient(client.getClientId());
+
+ rep.setClientSessionId(model.getClientSessionId());
+ rep.setData(model.getData());
+ return rep;
+ }
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index ffb9673..4829182 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -1,5 +1,9 @@
package org.keycloak.models.utils;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
+import org.keycloak.representations.idm.OfflineClientSessionRepresentation;
+import org.keycloak.representations.idm.OfflineUserSessionRepresentation;
import org.keycloak.util.Base64;
import org.jboss.logging.Logger;
import org.keycloak.enums.SslRequired;
@@ -981,6 +985,11 @@ public class RepresentationToModel {
user.addConsent(consentModel);
}
}
+ if (userRep.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionRepresentation sessionRep : userRep.getOfflineUserSessions()) {
+ importOfflineSession(session, newRealm, user, sessionRep);
+ }
+ }
if (userRep.getServiceAccountClientId() != null) {
String clientId = userRep.getServiceAccountClientId();
ClientModel client = clientMap.get(clientId);
@@ -1151,6 +1160,29 @@ public class RepresentationToModel {
return consentModel;
}
+ public static void importOfflineSession(KeycloakSession session, RealmModel newRealm, UserModel user, OfflineUserSessionRepresentation sessionRep) {
+ OfflineUserSessionModel model = new OfflineUserSessionModel();
+ model.setUserSessionId(sessionRep.getUserSessionId());
+ model.setData(sessionRep.getData());
+ session.users().addOfflineUserSession(newRealm, user, model);
+
+ for (OfflineClientSessionRepresentation csRep : sessionRep.getOfflineClientSessions()) {
+ OfflineClientSessionModel csModel = new OfflineClientSessionModel();
+ String clientId = csRep.getClient();
+ ClientModel client = newRealm.getClientByClientId(clientId);
+ if (client == null) {
+ throw new RuntimeException("Unable to find client " + clientId + " referenced from offlineClientSession of user " + user.getUsername());
+ }
+ csModel.setClientId(client.getId());
+ csModel.setUserId(user.getId());
+ csModel.setClientSessionId(csRep.getClientSessionId());
+ csModel.setUserSessionId(sessionRep.getUserSessionId());
+ csModel.setData(csRep.getData());
+
+ session.users().addOfflineClientSession(newRealm, csModel);
+ }
+ }
+
public static AuthenticationFlowModel toModel(AuthenticationFlowRepresentation rep) {
AuthenticationFlowModel model = new AuthenticationFlowModel();
model.setBuiltIn(rep.isBuiltIn());
diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
index f727c75..5f73031 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java
@@ -258,44 +258,4 @@ public class UserModelDelegate implements UserModel {
public void setCreatedTimestamp(Long timestamp){
delegate.setCreatedTimestamp(timestamp);
}
-
- @Override
- public void addOfflineUserSession(OfflineUserSessionModel userSession) {
- delegate.addOfflineUserSession(userSession);
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
- return delegate.getOfflineUserSession(userSessionId);
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
- return delegate.getOfflineUserSessions();
- }
-
- @Override
- public boolean removeOfflineUserSession(String userSessionId) {
- return delegate.removeOfflineUserSession(userSessionId);
- }
-
- @Override
- public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
- delegate.addOfflineClientSession(clientSession);
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
- return delegate.getOfflineClientSession(clientSessionId);
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
- return delegate.getOfflineClientSessions();
- }
-
- @Override
- public boolean removeOfflineClientSession(String clientSessionId) {
- return delegate.removeOfflineClientSession(clientSessionId);
- }
}
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
index d89ce63..2c233d1 100755
--- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java
@@ -574,141 +574,6 @@ public class UserAdapter implements UserModel, Comparable {
return false;
}
- @Override
- public void addOfflineUserSession(OfflineUserSessionModel userSession) {
- if (user.getOfflineUserSessions() == null) {
- user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
- }
-
- if (getUserSessionEntityById(userSession.getUserSessionId()) != null) {
- throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + user.getUsername());
- }
-
- OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
- entity.setUserSessionId(userSession.getUserSessionId());
- entity.setData(userSession.getData());
- entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
- user.getOfflineUserSessions().add(entity);
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
- OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
- return entity==null ? null : toModel(entity);
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
- if (user.getOfflineUserSessions()==null) {
- return Collections.emptyList();
- } else {
- List<OfflineUserSessionModel> result = new ArrayList<>();
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
- }
-
- private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- @Override
- public boolean removeOfflineUserSession(String userSessionId) {
- OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
- if (entity != null) {
- user.getOfflineUserSessions().remove(entity);
- return true;
- } else {
- return false;
- }
- }
-
- private OfflineUserSessionEntity getUserSessionEntityById(String userSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- if (entity.getUserSessionId().equals(userSessionId)) {
- return entity;
- }
- }
- }
- return null;
- }
-
- @Override
- public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
- OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(clientSession.getUserSessionId());
- if (userSessionEntity == null) {
- throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
- }
-
- OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
- clEntity.setClientSessionId(clientSession.getClientSessionId());
- clEntity.setClientId(clientSession.getClientId());
- clEntity.setData(clientSession.getData());
-
- userSessionEntity.getOfflineClientSessions().add(clEntity);
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- return toModel(clSession, userSession.getUserSessionId());
- }
- }
- }
- }
-
- return null;
- }
-
- private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
- OfflineClientSessionModel model = new OfflineClientSessionModel();
- model.setClientSessionId(cls.getClientSessionId());
- model.setClientId(cls.getClientId());
- model.setData(cls.getData());
- model.setUserSessionId(userSessionId);
- return model;
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
- List<OfflineClientSessionModel> result = new ArrayList<>();
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- result.add(toModel(clSession, userSession.getUserSessionId()));
- }
- }
- }
-
- return result;
- }
-
- @Override
- public boolean removeOfflineClientSession(String clientSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- userSession.getOfflineClientSessions().remove(clSession);
- return true;
- }
- }
- }
- }
-
- return false;
- }
-
@Override
public boolean equals(Object o) {
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
index 40547a8..9f4080a 100755
--- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java
@@ -23,6 +23,9 @@ import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -32,6 +35,8 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.entities.FederatedIdentityEntity;
+import org.keycloak.models.entities.OfflineClientSessionEntity;
+import org.keycloak.models.entities.OfflineUserSessionEntity;
import org.keycloak.models.entities.UserEntity;
import org.keycloak.models.file.adapter.UserAdapter;
import org.keycloak.models.utils.CredentialValidation;
@@ -41,6 +46,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -489,4 +495,187 @@ public class FileUserProvider implements UserProvider {
return null; // not supported yet
}
+ @Override
+ public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) {
+ userModel = getUserById(userModel.getId(), realm);
+ UserEntity userEntity = ((UserAdapter) userModel).getUserEntity();
+
+ if (userEntity.getOfflineUserSessions() == null) {
+ userEntity.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
+ }
+
+ if (getUserSessionEntityById(userEntity, userSession.getUserSessionId()) != null) {
+ throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + userEntity.getUsername());
+ }
+
+ OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
+ entity.setUserSessionId(userSession.getUserSessionId());
+ entity.setData(userSession.getData());
+ entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
+ userEntity.getOfflineUserSessions().add(entity);
+ }
+
+ @Override
+ public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+ userModel = getUserById(userModel.getId(), realm);
+ UserEntity userEntity = ((UserAdapter) userModel).getUserEntity();
+
+ OfflineUserSessionEntity entity = getUserSessionEntityById(userEntity, userSessionId);
+ return entity==null ? null : toModel(entity);
+ }
+
+ @Override
+ public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel userModel) {
+ userModel = getUserById(userModel.getId(), realm);
+ UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+ if (user.getOfflineUserSessions()==null) {
+ return Collections.emptyList();
+ } else {
+ List<OfflineUserSessionModel> result = new ArrayList<>();
+ for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+ result.add(toModel(entity));
+ }
+ return result;
+ }
+ }
+
+ private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
+ OfflineUserSessionModel model = new OfflineUserSessionModel();
+ model.setUserSessionId(entity.getUserSessionId());
+ model.setData(entity.getData());
+ return model;
+ }
+
+ @Override
+ public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+ userModel = getUserById(userModel.getId(), realm);
+ UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+ OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
+ if (entity != null) {
+ user.getOfflineUserSessions().remove(entity);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private OfflineUserSessionEntity getUserSessionEntityById(UserEntity user, String userSessionId) {
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+ if (entity.getUserSessionId().equals(userSessionId)) {
+ return entity;
+ }
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) {
+ UserModel userModel = getUserById(clientSession.getUserId(), realm);
+ UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+ OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId());
+ if (userSessionEntity == null) {
+ throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
+ }
+
+ OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
+ clEntity.setClientSessionId(clientSession.getClientSessionId());
+ clEntity.setClientId(clientSession.getClientId());
+ clEntity.setData(clientSession.getData());
+
+ userSessionEntity.getOfflineClientSessions().add(clEntity);
+ }
+
+ @Override
+ public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+ userModel = getUserById(userModel.getId(), realm);
+ UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ if (clSession.getClientSessionId().equals(clientSessionId)) {
+ return toModel(clSession, userSession.getUserSessionId());
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
+ OfflineClientSessionModel model = new OfflineClientSessionModel();
+ model.setClientSessionId(cls.getClientSessionId());
+ model.setClientId(cls.getClientId());
+ model.setData(cls.getData());
+ model.setUserSessionId(userSessionId);
+ return model;
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel userModel) {
+ userModel = getUserById(userModel.getId(), realm);
+ UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+ List<OfflineClientSessionModel> result = new ArrayList<>();
+
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ result.add(toModel(clSession, userSession.getUserSessionId()));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+ userModel = getUserById(userModel.getId(), realm);
+ UserEntity user = ((UserAdapter) userModel).getUserEntity();
+
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ if (clSession.getClientSessionId().equals(clientSessionId)) {
+ userSession.getOfflineClientSessions().remove(clSession);
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+ return getOfflineClientSessions(realm, client, -1, -1).size();
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+ List<OfflineClientSessionModel> result = new LinkedList<>();
+
+ List<UserModel> users = new ArrayList<>(inMemoryModel.getUsers(realm.getId()));
+ users = sortedSubList(users, firstResult, maxResults);
+
+ for (UserModel userModel : users) {
+ UserEntity user = ((UserAdapter) userModel).getUserEntity();
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ if (clSession.getClientId().equals(client.getId())) {
+ result.add(toModel(clSession, userSession.getUserSessionId()));
+ }
+ }
+ }
+ }
+ return result;
+ }
}
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
index df7c144..584e91f 100644
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/DefaultCacheUserProvider.java
@@ -104,13 +104,14 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
};
}
+ private boolean isRegisteredForInvalidation(RealmModel realm, String userId) {
+ return realmInvalidations.contains(realm.getId()) || userInvalidations.containsKey(userId);
+ }
+
@Override
public UserModel getUserById(String id, RealmModel realm) {
if (!cache.isEnabled()) return getDelegate().getUserById(id, realm);
- if (realmInvalidations.contains(realm.getId())) {
- return getDelegate().getUserById(id, realm);
- }
- if (userInvalidations.containsKey(id)) {
+ if (isRegisteredForInvalidation(realm, id)) {
return getDelegate().getUserById(id, realm);
}
@@ -120,7 +121,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
if (model == null) return null;
if (managedUsers.containsKey(id)) return managedUsers.get(id);
if (userInvalidations.containsKey(id)) return model;
- cached = new CachedUser(realm, model);
+ cached = new CachedUser(this, realm, model);
cache.addCachedUser(realm.getId(), cached);
} else if (managedUsers.containsKey(id)) {
return managedUsers.get(id);
@@ -145,7 +146,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
if (model == null) return null;
if (managedUsers.containsKey(model.getId())) return managedUsers.get(model.getId());
if (userInvalidations.containsKey(model.getId())) return model;
- cached = new CachedUser(realm, model);
+ cached = new CachedUser(this, realm, model);
cache.addCachedUser(realm.getId(), cached);
} else if (userInvalidations.containsKey(cached.getId())) {
return getDelegate().getUserById(cached.getId(), realm);
@@ -172,7 +173,7 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
UserModel model = getDelegate().getUserByEmail(email, realm);
if (model == null) return null;
if (userInvalidations.containsKey(model.getId())) return model;
- cached = new CachedUser(realm, model);
+ cached = new CachedUser(this, realm, model);
cache.addCachedUser(realm.getId(), cached);
} else if (userInvalidations.containsKey(cached.getId())) {
return getDelegate().getUserByEmail(email, realm);
@@ -327,4 +328,94 @@ public class DefaultCacheUserProvider implements CacheUserProvider {
public void preRemove(ClientModel client, ProtocolMapperModel protocolMapper) {
getDelegate().preRemove(client, protocolMapper);
}
+
+ @Override
+ public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
+ registerUserInvalidation(realm, user.getId());
+ getDelegate().addOfflineUserSession(realm, user, offlineUserSession);
+ }
+
+ @Override
+ public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+ if (isRegisteredForInvalidation(realm, user.getId())) {
+ return getDelegate().getOfflineUserSession(realm, user, userSessionId);
+ }
+
+ CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+ if (cachedUser == null) {
+ return getDelegate().getOfflineUserSession(realm, user, userSessionId);
+ } else {
+ return cachedUser.getOfflineUserSessions().get(userSessionId);
+ }
+ }
+
+ @Override
+ public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
+ if (isRegisteredForInvalidation(realm, user.getId())) {
+ return getDelegate().getOfflineUserSessions(realm, user);
+ }
+
+ CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+ if (cachedUser == null) {
+ return getDelegate().getOfflineUserSessions(realm, user);
+ } else {
+ return cachedUser.getOfflineUserSessions().values();
+ }
+ }
+
+ @Override
+ public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+ registerUserInvalidation(realm, user.getId());
+ return getDelegate().removeOfflineUserSession(realm, user, userSessionId);
+ }
+
+ @Override
+ public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
+ registerUserInvalidation(realm, offlineClientSession.getUserId());
+ getDelegate().addOfflineClientSession(realm, offlineClientSession);
+ }
+
+ @Override
+ public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+ if (isRegisteredForInvalidation(realm, user.getId())) {
+ return getDelegate().getOfflineClientSession(realm, user, clientSessionId);
+ }
+
+ CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+ if (cachedUser == null) {
+ return getDelegate().getOfflineClientSession(realm, user, clientSessionId);
+ } else {
+ return cachedUser.getOfflineClientSessions().get(clientSessionId);
+ }
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+ if (isRegisteredForInvalidation(realm, user.getId())) {
+ return getDelegate().getOfflineClientSessions(realm, user);
+ }
+
+ CachedUser cachedUser = cache.getCachedUser(realm.getId(), user.getId());
+ if (cachedUser == null) {
+ return getDelegate().getOfflineClientSessions(realm, user);
+ } else {
+ return cachedUser.getOfflineClientSessions().values();
+ }
+ }
+
+ @Override
+ public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+ registerUserInvalidation(realm, user.getId());
+ return getDelegate().removeOfflineClientSession(realm, user, clientSessionId);
+ }
+
+ @Override
+ public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+ return getDelegate().getOfflineClientSessionsCount(realm, client);
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+ return getDelegate().getOfflineClientSessions(realm, client, firstResult, maxResults);
+ }
}
diff --git a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
index 769f1b4..5a74b01 100755
--- a/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
+++ b/model/invalidation-cache/infinispan/src/main/java/org/keycloak/models/cache/infinispan/UserAdapter.java
@@ -348,52 +348,4 @@ public class UserAdapter implements UserModel {
getDelegateForUpdate();
return updated.revokeConsentForClient(clientId);
}
-
- @Override
- public void addOfflineUserSession(OfflineUserSessionModel userSession) {
- getDelegateForUpdate();
- updated.addOfflineUserSession(userSession);
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
- if (updated != null) return updated.getOfflineUserSession(userSessionId);
- return cached.getOfflineUserSessions().get(userSessionId);
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
- if (updated != null) return updated.getOfflineUserSessions();
- return cached.getOfflineUserSessions().values();
- }
-
- @Override
- public boolean removeOfflineUserSession(String userSessionId) {
- getDelegateForUpdate();
- return updated.removeOfflineUserSession(userSessionId);
- }
-
- @Override
- public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
- getDelegateForUpdate();
- updated.addOfflineClientSession(clientSession);
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
- if (updated != null) return updated.getOfflineClientSession(clientSessionId);
- return cached.getOfflineClientSessions().get(clientSessionId);
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
- if (updated != null) return updated.getOfflineClientSessions();
- return cached.getOfflineClientSessions().values();
- }
-
- @Override
- public boolean removeOfflineClientSession(String clientSessionId) {
- getDelegateForUpdate();
- return updated.removeOfflineClientSession(clientSessionId);
- }
}
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
index d38b6f9..24e866a 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedUser.java
@@ -6,6 +6,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserCredentialValueModel;
import org.keycloak.models.UserModel;
+import org.keycloak.models.cache.CacheUserProvider;
import org.keycloak.util.MultivaluedHashMap;
import java.io.Serializable;
@@ -40,7 +41,7 @@ public class CachedUser implements Serializable {
private Map<String, OfflineUserSessionModel> offlineUserSessions = new HashMap<>();
private Map<String, OfflineClientSessionModel> offlineClientSessions = new HashMap<>();
- public CachedUser(RealmModel realm, UserModel user) {
+ public CachedUser(CacheUserProvider cacheUserProvider, RealmModel realm, UserModel user) {
this.id = user.getId();
this.realm = realm.getId();
this.username = user.getUsername();
@@ -59,10 +60,10 @@ public class CachedUser implements Serializable {
for (RoleModel role : user.getRoleMappings()) {
roleMappings.add(role.getId());
}
- for (OfflineUserSessionModel offlineSession : user.getOfflineUserSessions()) {
+ for (OfflineUserSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineUserSessions(realm, user)) {
offlineUserSessions.put(offlineSession.getUserSessionId(), offlineSession);
}
- for (OfflineClientSessionModel offlineSession : user.getOfflineClientSessions()) {
+ for (OfflineClientSessionModel offlineSession : cacheUserProvider.getDelegate().getOfflineClientSessions(realm, user)) {
offlineClientSessions.put(offlineSession.getClientSessionId(), offlineSession);
}
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java
index 23081c4..e1c636a 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/OfflineClientSessionEntity.java
@@ -16,7 +16,9 @@ import javax.persistence.Table;
@NamedQueries({
@NamedQuery(name="deleteOfflineClientSessionsByRealm", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId)"),
@NamedQuery(name="deleteOfflineClientSessionsByRealmAndLink", query="delete from OfflineClientSessionEntity sess where sess.user IN (select u from UserEntity u where u.realmId=:realmId and u.federationLink=:link)"),
- @NamedQuery(name="deleteOfflineClientSessionsByClient", query="delete from OfflineClientSessionEntity sess where sess.clientId=:clientId")
+ @NamedQuery(name="deleteOfflineClientSessionsByClient", query="delete from OfflineClientSessionEntity sess where sess.clientId=:clientId"),
+ @NamedQuery(name="findOfflineClientSessionsCountByClient", query="select count(sess) from OfflineClientSessionEntity sess where sess.clientId=:clientId"),
+ @NamedQuery(name="findOfflineClientSessionsByClient", query="select sess from OfflineClientSessionEntity sess where sess.clientId=:clientId order by sess.user.username")
})
@Table(name="OFFLINE_CLIENT_SESSION")
@Entity
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 8cee4ea..563e898 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -4,6 +4,8 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -13,6 +15,8 @@ import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserProvider;
import org.keycloak.models.jpa.entities.FederatedIdentityEntity;
+import org.keycloak.models.jpa.entities.OfflineClientSessionEntity;
+import org.keycloak.models.jpa.entities.OfflineUserSessionEntity;
import org.keycloak.models.jpa.entities.UserAttributeEntity;
import org.keycloak.models.jpa.entities.UserEntity;
import org.keycloak.models.utils.CredentialValidation;
@@ -22,8 +26,10 @@ import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -473,4 +479,167 @@ public class JpaUserProvider implements UserProvider {
// Not supported yet
return null;
}
+
+ @Override
+ public void addOfflineUserSession(RealmModel realm, UserModel user, OfflineUserSessionModel offlineUserSession) {
+ UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+ OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
+ entity.setUser(userEntity);
+ entity.setUserSessionId(offlineUserSession.getUserSessionId());
+ entity.setData(offlineUserSession.getData());
+ em.persist(entity);
+ userEntity.getOfflineUserSessions().add(entity);
+ em.flush();
+ }
+
+ @Override
+ public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+ UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+ for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) {
+ if (entity.getUserSessionId().equals(userSessionId)) {
+ return toModel(entity);
+ }
+ }
+ return null;
+ }
+
+ private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
+ OfflineUserSessionModel model = new OfflineUserSessionModel();
+ model.setUserSessionId(entity.getUserSessionId());
+ model.setData(entity.getData());
+ return model;
+ }
+
+ @Override
+ public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel user) {
+ UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+ List<OfflineUserSessionModel> result = new LinkedList<>();
+ for (OfflineUserSessionEntity entity : userEntity.getOfflineUserSessions()) {
+ result.add(toModel(entity));
+ }
+ return result;
+ }
+
+ @Override
+ public boolean removeOfflineUserSession(RealmModel realm, UserModel user, String userSessionId) {
+ UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+ OfflineUserSessionEntity found = null;
+ for (OfflineUserSessionEntity session : userEntity.getOfflineUserSessions()) {
+ if (session.getUserSessionId().equals(userSessionId)) {
+ found = session;
+ break;
+ }
+ }
+
+ if (found == null) {
+ return false;
+ } else {
+ userEntity.getOfflineUserSessions().remove(found);
+ em.remove(found);
+ em.flush();
+ return true;
+ }
+ }
+
+ @Override
+ public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel offlineClientSession) {
+ UserEntity userEntity = em.getReference(UserEntity.class, offlineClientSession.getUserId());
+
+ OfflineClientSessionEntity entity = new OfflineClientSessionEntity();
+ entity.setUser(userEntity);
+ entity.setClientSessionId(offlineClientSession.getClientSessionId());
+ entity.setUserSessionId(offlineClientSession.getUserSessionId());
+ entity.setClientId(offlineClientSession.getClientId());
+ entity.setData(offlineClientSession.getData());
+ em.persist(entity);
+ userEntity.getOfflineClientSessions().add(entity);
+ em.flush();
+ }
+
+ @Override
+ public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+ UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+ for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) {
+ if (entity.getClientSessionId().equals(clientSessionId)) {
+ return toModel(entity);
+ }
+ }
+ return null;
+ }
+
+ private OfflineClientSessionModel toModel(OfflineClientSessionEntity entity) {
+ OfflineClientSessionModel model = new OfflineClientSessionModel();
+ model.setClientSessionId(entity.getClientSessionId());
+ model.setClientId(entity.getClientId());
+ model.setUserId(entity.getUser().getId());
+ model.setUserSessionId(entity.getUserSessionId());
+ model.setData(entity.getData());
+ return model;
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel user) {
+ UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+ List<OfflineClientSessionModel> result = new LinkedList<>();
+ for (OfflineClientSessionEntity entity : userEntity.getOfflineClientSessions()) {
+ result.add(toModel(entity));
+ }
+ return result;
+ }
+
+ @Override
+ public boolean removeOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId) {
+ UserEntity userEntity = em.getReference(UserEntity.class, user.getId());
+
+ OfflineClientSessionEntity found = null;
+ for (OfflineClientSessionEntity session : userEntity.getOfflineClientSessions()) {
+ if (session.getClientSessionId().equals(clientSessionId)) {
+ found = session;
+ break;
+ }
+ }
+
+ if (found == null) {
+ return false;
+ } else {
+ userEntity.getOfflineClientSessions().remove(found);
+ em.remove(found);
+ em.flush();
+ return true;
+ }
+ }
+
+ @Override
+ public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+ Query query = em.createNamedQuery("findOfflineClientSessionsCountByClient");
+ query.setParameter("clientId", client.getId());
+ Number n = (Number) query.getSingleResult();
+ return n.intValue();
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+ TypedQuery<OfflineClientSessionEntity> query = em.createNamedQuery("findOfflineClientSessionsByClient", OfflineClientSessionEntity.class);
+ query.setParameter("clientId", client.getId());
+
+ if (firstResult != -1) {
+ query.setFirstResult(firstResult);
+ }
+ if (maxResults != -1) {
+ query.setMaxResults(maxResults);
+ }
+
+ List<OfflineClientSessionEntity> results = query.getResultList();
+ Set<OfflineClientSessionModel> set = new HashSet<>();
+ for (OfflineClientSessionEntity entity : results) {
+ set.add(toModel(entity));
+ }
+ return set;
+ }
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
index bdcf1f1..9c75057 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java
@@ -756,124 +756,6 @@ public class UserAdapter implements UserModel {
}
@Override
- public void addOfflineUserSession(OfflineUserSessionModel offlineSession) {
- OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
- entity.setUser(user);
- entity.setUserSessionId(offlineSession.getUserSessionId());
- entity.setData(offlineSession.getData());
- em.persist(entity);
- user.getOfflineUserSessions().add(entity);
- em.flush();
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- if (entity.getUserSessionId().equals(userSessionId)) {
- return toModel(entity);
- }
- }
- return null;
- }
-
- private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
- List<OfflineUserSessionModel> result = new LinkedList<>();
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
-
- @Override
- public boolean removeOfflineUserSession(String userSessionId) {
- OfflineUserSessionEntity found = null;
- for (OfflineUserSessionEntity session : user.getOfflineUserSessions()) {
- if (session.getUserSessionId().equals(userSessionId)) {
- found = session;
- break;
- }
- }
-
- if (found == null) {
- return false;
- } else {
- user.getOfflineUserSessions().remove(found);
- em.remove(found);
- em.flush();
- return true;
- }
- }
-
- @Override
- public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
- OfflineClientSessionEntity entity = new OfflineClientSessionEntity();
- entity.setUser(user);
- entity.setClientSessionId(clientSession.getClientSessionId());
- entity.setUserSessionId(clientSession.getUserSessionId());
- entity.setClientId(clientSession.getClientId());
- entity.setData(clientSession.getData());
- em.persist(entity);
- user.getOfflineClientSessions().add(entity);
- em.flush();
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
- for (OfflineClientSessionEntity entity : user.getOfflineClientSessions()) {
- if (entity.getClientSessionId().equals(clientSessionId)) {
- return toModel(entity);
- }
- }
- return null;
- }
-
- private OfflineClientSessionModel toModel(OfflineClientSessionEntity entity) {
- OfflineClientSessionModel model = new OfflineClientSessionModel();
- model.setClientSessionId(entity.getClientSessionId());
- model.setClientId(entity.getClientId());
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
- List<OfflineClientSessionModel> result = new LinkedList<>();
- for (OfflineClientSessionEntity entity : user.getOfflineClientSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
-
- @Override
- public boolean removeOfflineClientSession(String clientSessionId) {
- OfflineClientSessionEntity found = null;
- for (OfflineClientSessionEntity session : user.getOfflineClientSessions()) {
- if (session.getClientSessionId().equals(clientSessionId)) {
- found = session;
- break;
- }
- }
-
- if (found == null) {
- return false;
- } else {
- user.getOfflineClientSessions().remove(found);
- em.remove(found);
- em.flush();
- return true;
- }
- }
-
- @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
index 14081c1..214733a 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java
@@ -10,6 +10,10 @@ import org.keycloak.models.ClientModel;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.ModelException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
@@ -26,8 +30,10 @@ import org.keycloak.models.mongo.keycloak.entities.MongoUserEntity;
import org.keycloak.models.utils.CredentialValidation;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -51,7 +57,7 @@ public class MongoUserProvider implements UserProvider {
}
@Override
- public UserModel getUserById(String id, RealmModel realm) {
+ public UserAdapter getUserById(String id, RealmModel realm) {
MongoUserEntity user = getMongoStore().loadEntity(MongoUserEntity.class, id, invocationContext);
// Check that it's user from this realm
@@ -244,8 +250,8 @@ public class MongoUserProvider implements UserProvider {
@Override
public Set<FederatedIdentityModel> getFederatedIdentities(UserModel userModel, RealmModel realm) {
- UserModel user = getUserById(userModel.getId(), realm);
- MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+ UserAdapter user = getUserById(userModel.getId(), realm);
+ MongoUserEntity userEntity = user.getUser();
List<FederatedIdentityEntity> linkEntities = userEntity.getFederatedIdentities();
if (linkEntities == null) {
@@ -263,8 +269,8 @@ public class MongoUserProvider implements UserProvider {
@Override
public FederatedIdentityModel getFederatedIdentity(UserModel user, String socialProvider, RealmModel realm) {
- user = getUserById(user.getId(), realm);
- MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+ UserAdapter mongoUser = getUserById(user.getId(), realm);
+ MongoUserEntity userEntity = mongoUser.getUser();
FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, socialProvider);
return federatedIdentityEntity != null ? new FederatedIdentityModel(federatedIdentityEntity.getIdentityProvider(), federatedIdentityEntity.getUserId(),
@@ -320,8 +326,8 @@ public class MongoUserProvider implements UserProvider {
@Override
public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIdentityModel identity) {
- user = getUserById(user.getId(), realm);
- MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+ UserAdapter mongoUser = getUserById(user.getId(), realm);
+ MongoUserEntity userEntity = mongoUser.getUser();
FederatedIdentityEntity federatedIdentityEntity = new FederatedIdentityEntity();
federatedIdentityEntity.setIdentityProvider(identity.getIdentityProvider());
federatedIdentityEntity.setUserId(identity.getUserId());
@@ -333,8 +339,8 @@ public class MongoUserProvider implements UserProvider {
@Override
public void updateFederatedIdentity(RealmModel realm, UserModel federatedUser, FederatedIdentityModel federatedIdentityModel) {
- federatedUser = getUserById(federatedUser.getId(), realm);
- MongoUserEntity userEntity = ((UserAdapter) federatedUser).getUser();
+ UserAdapter mongoUser = getUserById(federatedUser.getId(), realm);
+ MongoUserEntity userEntity = mongoUser.getUser();
FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, federatedIdentityModel.getIdentityProvider());
federatedIdentityEntity.setToken(federatedIdentityModel.getToken());
@@ -342,8 +348,8 @@ public class MongoUserProvider implements UserProvider {
@Override
public boolean removeFederatedIdentity(RealmModel realm, UserModel userModel, String socialProvider) {
- UserModel user = getUserById(userModel.getId(), realm);
- MongoUserEntity userEntity = ((UserAdapter) user).getUser();
+ UserAdapter user = getUserById(userModel.getId(), realm);
+ MongoUserEntity userEntity = user.getUser();
FederatedIdentityEntity federatedIdentityEntity = findFederatedIdentityLink(userEntity, socialProvider);
if (federatedIdentityEntity == null) {
return false;
@@ -476,4 +482,205 @@ public class MongoUserProvider implements UserProvider {
// Not supported yet
return null;
}
+
+ @Override
+ public void addOfflineUserSession(RealmModel realm, UserModel userModel, OfflineUserSessionModel userSession) {
+ MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+ if (user.getOfflineUserSessions() == null) {
+ user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
+ }
+
+ if (getUserSessionEntityById(user, userSession.getUserSessionId()) != null) {
+ throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + user.getUsername());
+ }
+
+ OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
+ entity.setUserSessionId(userSession.getUserSessionId());
+ entity.setData(userSession.getData());
+ entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
+ user.getOfflineUserSessions().add(entity);
+
+ getMongoStore().updateEntity(user, invocationContext);
+ }
+
+ private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
+ OfflineUserSessionModel model = new OfflineUserSessionModel();
+ model.setUserSessionId(entity.getUserSessionId());
+ model.setData(entity.getData());
+ return model;
+ }
+
+ private OfflineUserSessionEntity getUserSessionEntityById(MongoUserEntity user, String userSessionId) {
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+ if (entity.getUserSessionId().equals(userSessionId)) {
+ return entity;
+ }
+ }
+ }
+ return null;
+ }
+
+
+ @Override
+ public OfflineUserSessionModel getOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+ MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+ OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
+ return entity==null ? null : toModel(entity);
+ }
+
+ @Override
+ public Collection<OfflineUserSessionModel> getOfflineUserSessions(RealmModel realm, UserModel userModel) {
+ MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+ if (user.getOfflineUserSessions()==null) {
+ return Collections.emptyList();
+ } else {
+ List<OfflineUserSessionModel> result = new ArrayList<>();
+ for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
+ result.add(toModel(entity));
+ }
+ return result;
+ }
+ }
+
+ @Override
+ public boolean removeOfflineUserSession(RealmModel realm, UserModel userModel, String userSessionId) {
+ MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+ OfflineUserSessionEntity entity = getUserSessionEntityById(user, userSessionId);
+ if (entity != null) {
+ user.getOfflineUserSessions().remove(entity);
+ getMongoStore().updateEntity(user, invocationContext);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public void addOfflineClientSession(RealmModel realm, OfflineClientSessionModel clientSession) {
+ MongoUserEntity user = getUserById(clientSession.getUserId(), realm).getUser();
+
+ OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(user, clientSession.getUserSessionId());
+ if (userSessionEntity == null) {
+ throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + user.getUsername());
+ }
+
+ OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
+ clEntity.setClientSessionId(clientSession.getClientSessionId());
+ clEntity.setClientId(clientSession.getClientId());
+ clEntity.setData(clientSession.getData());
+
+ userSessionEntity.getOfflineClientSessions().add(clEntity);
+ getMongoStore().updateEntity(user, invocationContext);
+ }
+
+ @Override
+ public OfflineClientSessionModel getOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+ MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ if (clSession.getClientSessionId().equals(clientSessionId)) {
+ return toModel(clSession, user.getId(), userSession.getUserSessionId());
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
+ private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userId, String userSessionId) {
+ OfflineClientSessionModel model = new OfflineClientSessionModel();
+ model.setClientSessionId(cls.getClientSessionId());
+ model.setClientId(cls.getClientId());
+ model.setUserId(userId);
+ model.setData(cls.getData());
+ model.setUserSessionId(userSessionId);
+ return model;
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, UserModel userModel) {
+ MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+
+ List<OfflineClientSessionModel> result = new ArrayList<>();
+
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ result.add(toModel(clSession, user.getId(), userSession.getUserSessionId()));
+ }
+ }
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean removeOfflineClientSession(RealmModel realm, UserModel userModel, String clientSessionId) {
+ MongoUserEntity user = getUserById(userModel.getId(), realm).getUser();
+ boolean updated = false;
+
+ if (user.getOfflineUserSessions() != null) {
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ if (clSession.getClientSessionId().equals(clientSessionId)) {
+ userSession.getOfflineClientSessions().remove(clSession);
+ updated = true;
+ break;
+ }
+ }
+
+ if (updated && userSession.getOfflineClientSessions().isEmpty()) {
+ user.getOfflineUserSessions().remove(userSession);
+ }
+
+ if (updated) {
+ getMongoStore().updateEntity(user, invocationContext);
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public int getOfflineClientSessionsCount(RealmModel realm, ClientModel client) {
+ DBObject query = new QueryBuilder()
+ .and("realmId").is(realm.getId())
+ .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
+ .get();
+ return getMongoStore().countEntities(MongoUserEntity.class, query, invocationContext);
+ }
+
+ @Override
+ public Collection<OfflineClientSessionModel> getOfflineClientSessions(RealmModel realm, ClientModel client, int firstResult, int maxResults) {
+ DBObject query = new QueryBuilder()
+ .and("realmId").is(realm.getId())
+ .and("offlineUserSessions.offlineClientSessions.clientId").is(client.getId())
+ .get();
+ DBObject sort = new BasicDBObject("username", 1);
+ List<MongoUserEntity> users = getMongoStore().loadEntities(MongoUserEntity.class, query, sort, firstResult, maxResults, invocationContext);
+
+ List<OfflineClientSessionModel> result = new LinkedList<>();
+ for (MongoUserEntity user : users) {
+ for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
+ for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
+ if (clSession.getClientId().equals(client.getId())) {
+ OfflineClientSessionModel model = toModel(clSession, user.getId(), userSession.getUserSessionId());
+ result.add(model);
+ }
+ }
+ }
+ }
+
+ return result;
+ }
}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
index a475bb6..9f13e63 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java
@@ -633,145 +633,6 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
}
@Override
- public void addOfflineUserSession(OfflineUserSessionModel userSession) {
- if (user.getOfflineUserSessions() == null) {
- user.setOfflineUserSessions(new ArrayList<OfflineUserSessionEntity>());
- }
-
- if (getUserSessionEntityById(userSession.getUserSessionId()) != null) {
- throw new ModelDuplicateException("User session already exists with id " + userSession.getUserSessionId() + " for user " + getMongoEntity().getUsername());
- }
-
- OfflineUserSessionEntity entity = new OfflineUserSessionEntity();
- entity.setUserSessionId(userSession.getUserSessionId());
- entity.setData(userSession.getData());
- entity.setOfflineClientSessions(new ArrayList<OfflineClientSessionEntity>());
- user.getOfflineUserSessions().add(entity);
- updateUser();
- }
-
- @Override
- public OfflineUserSessionModel getOfflineUserSession(String userSessionId) {
- OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
- return entity==null ? null : toModel(entity);
- }
-
- @Override
- public Collection<OfflineUserSessionModel> getOfflineUserSessions() {
- if (user.getOfflineUserSessions()==null) {
- return Collections.emptyList();
- } else {
- List<OfflineUserSessionModel> result = new ArrayList<>();
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- result.add(toModel(entity));
- }
- return result;
- }
- }
-
- private OfflineUserSessionModel toModel(OfflineUserSessionEntity entity) {
- OfflineUserSessionModel model = new OfflineUserSessionModel();
- model.setUserSessionId(entity.getUserSessionId());
- model.setData(entity.getData());
- return model;
- }
-
- @Override
- public boolean removeOfflineUserSession(String userSessionId) {
- OfflineUserSessionEntity entity = getUserSessionEntityById(userSessionId);
- if (entity != null) {
- user.getOfflineUserSessions().remove(entity);
- updateUser();
- return true;
- } else {
- return false;
- }
- }
-
- private OfflineUserSessionEntity getUserSessionEntityById(String userSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity entity : user.getOfflineUserSessions()) {
- if (entity.getUserSessionId().equals(userSessionId)) {
- return entity;
- }
- }
- }
- return null;
- }
-
- @Override
- public void addOfflineClientSession(OfflineClientSessionModel clientSession) {
- OfflineUserSessionEntity userSessionEntity = getUserSessionEntityById(clientSession.getUserSessionId());
- if (userSessionEntity == null) {
- throw new ModelException("OfflineUserSession with ID " + clientSession.getUserSessionId() + " doesn't exist for user " + getMongoEntity().getUsername());
- }
-
- OfflineClientSessionEntity clEntity = new OfflineClientSessionEntity();
- clEntity.setClientSessionId(clientSession.getClientSessionId());
- clEntity.setClientId(clientSession.getClientId());
- clEntity.setData(clientSession.getData());
-
- userSessionEntity.getOfflineClientSessions().add(clEntity);
- updateUser();
- }
-
- @Override
- public OfflineClientSessionModel getOfflineClientSession(String clientSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- return toModel(clSession, userSession.getUserSessionId());
- }
- }
- }
- }
-
- return null;
- }
-
- private OfflineClientSessionModel toModel(OfflineClientSessionEntity cls, String userSessionId) {
- OfflineClientSessionModel model = new OfflineClientSessionModel();
- model.setClientSessionId(cls.getClientSessionId());
- model.setClientId(cls.getClientId());
- model.setData(cls.getData());
- model.setUserSessionId(userSessionId);
- return model;
- }
-
- @Override
- public Collection<OfflineClientSessionModel> getOfflineClientSessions() {
- List<OfflineClientSessionModel> result = new ArrayList<>();
-
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- result.add(toModel(clSession, userSession.getUserSessionId()));
- }
- }
- }
-
- return result;
- }
-
- @Override
- public boolean removeOfflineClientSession(String clientSessionId) {
- if (user.getOfflineUserSessions() != null) {
- for (OfflineUserSessionEntity userSession : user.getOfflineUserSessions()) {
- for (OfflineClientSessionEntity clSession : userSession.getOfflineClientSessions()) {
- if (clSession.getClientSessionId().equals(clientSessionId)) {
- userSession.getOfflineClientSessions().remove(clSession);
- updateUser();
- return true;
- }
- }
- }
- }
-
- return false;
- }
-
- @Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !(o instanceof UserModel)) return false;
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
index c166565..ee8e9e8 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -98,7 +98,7 @@ public class TokenManager {
ClientSessionModel clientSession = null;
if (RefreshTokenUtil.TOKEN_TYPE_OFFLINE.equals(oldToken.getType())) {
- clientSession = OfflineTokenUtils.findOfflineClientSession(realm, user, oldToken.getClientSession(), oldToken.getSessionState());
+ clientSession = OfflineTokenUtils.findOfflineClientSession(session, realm, user, oldToken.getClientSession(), oldToken.getSessionState());
if (clientSession != null) {
userSession = clientSession.getUserSession();
}
@@ -496,7 +496,7 @@ public class TokenManager {
refreshToken = new RefreshToken(accessToken);
refreshToken.type(RefreshTokenUtil.TOKEN_TYPE_OFFLINE);
- OfflineTokenUtils.persistOfflineSession(clientSession, userSession);
+ OfflineTokenUtils.persistOfflineSession(session, realm, clientSession, userSession);
} else {
refreshToken = new RefreshToken(accessToken);
refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java b/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
index 50abfd4..519ae25 100644
--- a/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
+++ b/services/src/main/java/org/keycloak/services/offline/OfflineClientSessionAdapter.java
@@ -75,7 +75,7 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
@Override
public String getRedirectUri() {
- return data.getRedirectUri();
+ return getData().getRedirectUri();
}
@Override
@@ -85,7 +85,7 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
@Override
public int getTimestamp() {
- return 0;
+ return getData().getTimestamp();
}
@Override
@@ -238,6 +238,9 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
@JsonProperty("authenticatorStatus")
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
+ @JsonProperty("timestamp")
+ private int timestamp;
+
public String getAuthMethod() {
return authMethod;
}
@@ -285,5 +288,13 @@ public class OfflineClientSessionAdapter implements ClientSessionModel {
public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
this.authenticatorStatus = authenticatorStatus;
}
+
+ public int getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(int timestamp) {
+ this.timestamp = timestamp;
+ }
}
}
diff --git a/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java b/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java
index b4900c1..13ff55b 100644
--- a/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java
+++ b/services/src/main/java/org/keycloak/services/offline/OfflineTokenUtils.java
@@ -9,6 +9,7 @@ import org.jboss.logging.Logger;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.OfflineClientSessionModel;
import org.keycloak.models.OfflineUserSessionModel;
@@ -17,9 +18,9 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.Time;
/**
- * TODO: Change to utils?
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@@ -27,12 +28,12 @@ public class OfflineTokenUtils {
protected static Logger logger = Logger.getLogger(OfflineTokenUtils.class);
- public static void persistOfflineSession(ClientSessionModel clientSession, UserSessionModel userSession) {
+ public static void persistOfflineSession(KeycloakSession kcSession, RealmModel realm, ClientSessionModel clientSession, UserSessionModel userSession) {
UserModel user = userSession.getUser();
ClientModel client = clientSession.getClient();
// First verify if we already have offlineToken for this user+client . If yes, then invalidate it (This is to avoid leaks)
- Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+ Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
for (OfflineClientSessionModel existing : clientSessions) {
if (existing.getClientId().equals(client.getId())) {
if (logger.isTraceEnabled()) {
@@ -40,28 +41,28 @@ public class OfflineTokenUtils {
user.getUsername(), client.getClientId(), existing.getClientSessionId());
}
- user.removeOfflineClientSession(existing.getClientSessionId());
+ kcSession.users().removeOfflineClientSession(realm, user, existing.getClientSessionId());
// Check if userSession is ours. If not, then check if it has other clientSessions and remove it otherwise
if (!existing.getUserSessionId().equals(userSession.getId())) {
- checkUserSessionHasClientSessions(user, existing.getUserSessionId());
+ checkUserSessionHasClientSessions(kcSession, realm, user, existing.getUserSessionId());
}
}
}
// Verify if we already have UserSession with this ID. If yes, don't create another one
- OfflineUserSessionModel userSessionRep = user.getOfflineUserSession(userSession.getId());
+ OfflineUserSessionModel userSessionRep = kcSession.users().getOfflineUserSession(realm, user, userSession.getId());
if (userSessionRep == null) {
- createOfflineUserSession(user, userSession);
+ createOfflineUserSession(kcSession, realm, user, userSession);
}
// Create clientRep and save to DB.
- createOfflineClientSession(user, clientSession, userSession);
+ createOfflineClientSession(kcSession, realm, user, clientSession, userSession);
}
// userSessionId is provided from offline token. It's used just to verify if it match the ID from clientSession representation
- public static ClientSessionModel findOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
- OfflineClientSessionModel clientSession = user.getOfflineClientSession(clientSessionId);
+ public static ClientSessionModel findOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, String clientSessionId, String userSessionId) {
+ OfflineClientSessionModel clientSession = kcSession.users().getOfflineClientSession(realm, user, clientSessionId);
if (clientSession == null) {
return null;
}
@@ -71,7 +72,7 @@ public class OfflineTokenUtils {
" Wanted user session: " + userSessionId);
}
- OfflineUserSessionModel userSession = user.getOfflineUserSession(userSessionId);
+ OfflineUserSessionModel userSession = kcSession.users().getOfflineUserSession(realm, user, userSessionId);
if (userSession == null) {
throw new ModelException("Found clientSession " + clientSessionId + " but not userSession " + userSessionId);
}
@@ -79,13 +80,11 @@ public class OfflineTokenUtils {
OfflineUserSessionAdapter userSessionAdapter = new OfflineUserSessionAdapter(userSession, user);
ClientModel client = realm.getClientById(clientSession.getClientId());
- OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter);
-
- return clientSessionAdapter;
+ return new OfflineClientSessionAdapter(clientSession, realm, client, userSessionAdapter);
}
- public static Set<ClientModel> findClientsWithOfflineToken(RealmModel realm, UserModel user) {
- Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+ public static Set<ClientModel> findClientsWithOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user) {
+ Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
Set<ClientModel> clients = new HashSet<>();
for (OfflineClientSessionModel clientSession : clientSessions) {
ClientModel client = realm.getClientById(clientSession.getClientId());
@@ -94,8 +93,8 @@ public class OfflineTokenUtils {
return clients;
}
- public static boolean revokeOfflineToken(UserModel user, ClientModel client) {
- Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+ public static boolean revokeOfflineToken(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientModel client) {
+ Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
boolean anyRemoved = false;
for (OfflineClientSessionModel clientSession : clientSessions) {
if (clientSession.getClientId().equals(client.getId())) {
@@ -104,8 +103,8 @@ public class OfflineTokenUtils {
user.getUsername(), client.getClientId(), clientSession.getClientSessionId());
}
- user.removeOfflineClientSession(clientSession.getClientSessionId());
- checkUserSessionHasClientSessions(user, clientSession.getUserSessionId());
+ kcSession.users().removeOfflineClientSession(realm, user, clientSession.getClientSessionId());
+ checkUserSessionHasClientSessions(kcSession, realm, user, clientSession.getUserSessionId());
anyRemoved = true;
}
}
@@ -123,7 +122,7 @@ public class OfflineTokenUtils {
return clientSession.getRoles().contains(offlineAccessRole.getId());
}
- private static void createOfflineUserSession(UserModel user, UserSessionModel userSession) {
+ private static void createOfflineUserSession(KeycloakSession kcSession, RealmModel realm, UserModel user, UserSessionModel userSession) {
if (logger.isTraceEnabled()) {
logger.tracef("Creating new offline user session. UserSessionID: '%s' , Username: '%s'", userSession.getId(), user.getUsername());
}
@@ -141,13 +140,13 @@ public class OfflineTokenUtils {
OfflineUserSessionModel sessionModel = new OfflineUserSessionModel();
sessionModel.setUserSessionId(userSession.getId());
sessionModel.setData(stringRep);
- user.addOfflineUserSession(sessionModel);
+ kcSession.users().addOfflineUserSession(realm, user, sessionModel);
} catch (IOException ioe) {
throw new ModelException(ioe);
}
}
- private static void createOfflineClientSession(UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
+ private static void createOfflineClientSession(KeycloakSession kcSession, RealmModel realm, UserModel user, ClientSessionModel clientSession, UserSessionModel userSession) {
if (logger.isTraceEnabled()) {
logger.tracef("Creating new offline token client session. ClientSessionId: '%s', UserSessionID: '%s' , Username: '%s', Client: '%s'" ,
clientSession.getId(), userSession.getId(), user.getUsername(), clientSession.getClient().getClientId());
@@ -159,23 +158,25 @@ public class OfflineTokenUtils {
rep.setRoles(clientSession.getRoles());
rep.setNotes(clientSession.getNotes());
rep.setAuthenticatorStatus(clientSession.getExecutionStatus());
+ rep.setTimestamp(Time.currentTime());
try {
String stringRep = JsonSerialization.writeValueAsString(rep);
OfflineClientSessionModel clsModel = new OfflineClientSessionModel();
clsModel.setClientSessionId(clientSession.getId());
clsModel.setClientId(clientSession.getClient().getId());
+ clsModel.setUserId(user.getId());
clsModel.setUserSessionId(userSession.getId());
clsModel.setData(stringRep);
- user.addOfflineClientSession(clsModel);
+ kcSession.users().addOfflineClientSession(realm, clsModel);
} catch (IOException ioe) {
throw new ModelException(ioe);
}
}
// Check if userSession has any offline clientSessions attached to it. Remove userSession if not
- private static void checkUserSessionHasClientSessions(UserModel user, String userSessionId) {
- Collection<OfflineClientSessionModel> clientSessions = user.getOfflineClientSessions();
+ private static void checkUserSessionHasClientSessions(KeycloakSession kcSession, RealmModel realm, UserModel user, String userSessionId) {
+ Collection<OfflineClientSessionModel> clientSessions = kcSession.users().getOfflineClientSessions(realm, user);
for (OfflineClientSessionModel clientSession : clientSessions) {
if (clientSession.getUserSessionId().equals(userSessionId)) {
@@ -186,6 +187,6 @@ public class OfflineTokenUtils {
if (logger.isTraceEnabled()) {
logger.tracef("Removing offline userSession for user %s as it doesn't have any client sessions attached. UserSessionID: %s", user.getUsername(), userSessionId);
}
- user.removeOfflineUserSession(userSessionId);
+ kcSession.users().removeOfflineUserSession(realm, user, userSessionId);
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java
index 20e7b6e..17de6a2 100755
--- a/services/src/main/java/org/keycloak/services/resources/AccountService.java
+++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java
@@ -486,7 +486,7 @@ public class AccountService extends AbstractSecuredLocalService {
// Revoke grant in UserModel
UserModel user = auth.getUser();
user.revokeConsentForClient(client.getId());
- OfflineTokenUtils.revokeOfflineToken(user, client);
+ OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
// Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 2198333..71d8ac0 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -7,8 +7,11 @@ import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.ClientModel;
+import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
@@ -24,6 +27,8 @@ import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.managers.ClientManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
+import org.keycloak.services.offline.OfflineClientSessionAdapter;
+import org.keycloak.services.offline.OfflineUserSessionAdapter;
import org.keycloak.services.resources.KeycloakApplication;
import org.keycloak.services.ErrorResponse;
import org.keycloak.util.JsonSerialization;
@@ -391,6 +396,65 @@ public class ClientResource {
}
/**
+ * Get application offline session count
+ *
+ * Returns a number of offline user sessions associated with this client
+ *
+ * {
+ * "count": number
+ * }
+ *
+ * @return
+ */
+ @Path("offline-session-count")
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public Map<String, Integer> getOfflineSessionCount() {
+ auth.requireView();
+ Map<String, Integer> map = new HashMap<String, Integer>();
+ map.put("count", session.users().getOfflineClientSessionsCount(client.getRealm(), client));
+ return map;
+ }
+
+ /**
+ * Get offline sessions for client
+ *
+ * Returns a list of offline user sessions associated with this client
+ *
+ * @param firstResult Paging offset
+ * @param maxResults Paging size
+ * @return
+ */
+ @Path("offline-sessions")
+ @GET
+ @NoCache
+ @Produces(MediaType.APPLICATION_JSON)
+ public List<UserSessionRepresentation> getOfflineUserSessions(@QueryParam("first") Integer firstResult, @QueryParam("max") Integer maxResults) {
+ auth.requireView();
+ firstResult = firstResult != null ? firstResult : -1;
+ maxResults = maxResults != null ? maxResults : -1;
+ List<UserSessionRepresentation> sessions = new ArrayList<UserSessionRepresentation>();
+ for (OfflineClientSessionModel offlineClientSession : session.users().getOfflineClientSessions(client.getRealm(), client, firstResult, maxResults)) {
+ UserModel user = session.users().getUserById(offlineClientSession.getUserId(), client.getRealm());
+ OfflineUserSessionModel offlineUserSession = session.users().getOfflineUserSession(client.getRealm(), user, offlineClientSession.getUserSessionId());
+ OfflineUserSessionAdapter sessionAdapter = new OfflineUserSessionAdapter(offlineUserSession, user);
+ OfflineClientSessionAdapter clientSessionAdapter = new OfflineClientSessionAdapter(offlineClientSession, client.getRealm(), client, sessionAdapter);
+
+ UserSessionRepresentation rep = new UserSessionRepresentation();
+ rep.setId(sessionAdapter.getId());
+ rep.setStart(Time.toMillis(clientSessionAdapter.getTimestamp()));
+ rep.setUsername(user.getUsername());
+ rep.setUserId(user.getId());
+ rep.setIpAddress(sessionAdapter.getIpAddress());
+
+ sessions.add(rep);
+ }
+ return sessions;
+ }
+
+
+ /**
* Logout all sessions
*
* If the client has an admin URL, invalidate all sessions associated with that client directly.
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index e33e50e..bf535d9 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -66,15 +66,18 @@ import javax.ws.rs.WebApplicationException;
import java.net.URI;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.keycloak.models.UsernameLoginFailureModel;
import org.keycloak.services.managers.BruteForceProtector;
+import org.keycloak.services.offline.OfflineTokenUtils;
import org.keycloak.services.resources.AccountService;
/**
@@ -439,25 +442,44 @@ public class UsersResource {
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
- public List<UserConsentRepresentation> getConsents(final @PathParam("id") String id) {
+ public List<Map<String, Object>> getConsents(final @PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
- List<UserConsentModel> consents = user.getConsents();
- List<UserConsentRepresentation> result = new ArrayList<UserConsentRepresentation>();
+ List<Map<String, Object>> result = new LinkedList<>();
- for (UserConsentModel consent : consents) {
- UserConsentRepresentation rep = ModelToRepresentation.toRepresentation(consent);
- result.add(rep);
+ Set<ClientModel> offlineClients = OfflineTokenUtils.findClientsWithOfflineToken(session, realm, user);
+
+ for (ClientModel client : realm.getClients()) {
+ UserConsentModel consent = user.getConsentByClient(client.getId());
+ boolean hasOfflineToken = offlineClients.contains(client);
+
+ if (consent == null && !hasOfflineToken) {
+ continue;
+ }
+
+ UserConsentRepresentation rep = (consent == null) ? null : ModelToRepresentation.toRepresentation(consent);
+
+ Map<String, Object> currentRep = new HashMap<>();
+ currentRep.put("clientId", client.getClientId());
+ currentRep.put("grantedProtocolMappers", (rep==null ? Collections.emptyMap() : rep.getGrantedProtocolMappers()));
+ currentRep.put("grantedRealmRoles", (rep==null ? Collections.emptyList() : rep.getGrantedRealmRoles()));
+ currentRep.put("grantedClientRoles", (rep==null ? Collections.emptyMap() : rep.getGrantedClientRoles()));
+
+ List<String> additionalGrants = hasOfflineToken ? Arrays.asList("Offline Token") : Collections.<String>emptyList();
+ currentRep.put("additionalGrants", additionalGrants);
+
+ result.add(currentRep);
}
+
return result;
}
/**
- * Revoke consent for particular client from user
+ * Revoke consent and offline tokens for particular client from user
*
* @param id User id
* @param clientId Client id
@@ -473,12 +495,16 @@ public class UsersResource {
}
ClientModel client = realm.getClientByClientId(clientId);
- boolean revoked = user.revokeConsentForClient(client.getId());
- if (revoked) {
+ boolean revokedConsent = user.revokeConsentForClient(client.getId());
+ boolean revokedOfflineToken = OfflineTokenUtils.revokeOfflineToken(session, realm, user, client);
+
+ if (revokedConsent) {
// Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
- } else {
- throw new NotFoundException("Consent not found");
+ }
+
+ if (!revokedConsent && !revokedOfflineToken) {
+ throw new NotFoundException("Consent nor offline token not found");
}
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
index 6f42fd5..6040c72 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/ImportTest.java
@@ -15,6 +15,8 @@ import org.keycloak.models.FederatedIdentityModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.LDAPConstants;
+import org.keycloak.models.OfflineClientSessionModel;
+import org.keycloak.models.OfflineUserSessionModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredCredentialModel;
@@ -32,6 +34,7 @@ import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.managers.RealmManager;
+import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -327,6 +330,21 @@ public class ImportTest extends AbstractModelTest {
Assert.assertFalse(otherAppAdminConsent.isRoleGranted(application.getRole("app-admin")));
Assert.assertTrue(otherAppAdminConsent.isProtocolMapperGranted(gssCredentialMapper));
+ // Test offline sessions
+ Collection<OfflineUserSessionModel> offlineUserSessions = session.users().getOfflineUserSessions(realm, admin);
+ Collection<OfflineClientSessionModel> offlineClientSessions = session.users().getOfflineClientSessions(realm, admin);
+ Assert.assertEquals(offlineUserSessions.size(), 1);
+ Assert.assertEquals(offlineClientSessions.size(), 1);
+ OfflineUserSessionModel offlineSession = offlineUserSessions.iterator().next();
+ OfflineClientSessionModel offlineClSession = offlineClientSessions.iterator().next();
+ Assert.assertEquals(offlineSession.getData(), "something1");
+ Assert.assertEquals(offlineSession.getUserSessionId(), "123");
+ Assert.assertEquals(offlineClSession.getClientId(), otherApp.getId());
+ Assert.assertEquals(offlineClSession.getUserSessionId(), "123");
+ Assert.assertEquals(offlineClSession.getUserId(), admin.getId());
+ Assert.assertEquals(offlineClSession.getData(), "something2");
+
+
// Test service accounts
Assert.assertFalse(application.isServiceAccountsEnabled());
Assert.assertTrue(otherApp.isServiceAccountsEnabled());
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
index f0118c2..68fa2f8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/model/UserModelTest.java
@@ -15,6 +15,7 @@ import static org.junit.Assert.assertNotNull;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -292,12 +293,35 @@ public class UserModelTest extends AbstractModelTest {
ClientModel barClient = realm.addClient("bar");
UserModel user1 = session.users().addUser(realm, "user1");
- addOfflineUserSession(user1, "123", "something1");
- addOfflineClientSession(user1, "456", "123", fooClient.getId(), "something2");
- addOfflineClientSession(user1, "789", "123", barClient.getId(), "something3");
+ UserModel user2 = session.users().addUser(realm, "user2");
+
+ addOfflineUserSession(realm, user1, "123", "something1");
+ addOfflineClientSession(realm, user1, "456", "123", fooClient.getId(), "something2");
+ addOfflineClientSession(realm, user1, "789", "123", barClient.getId(), "something3");
+
+ addOfflineUserSession(realm, user2, "2123", "something4");
+ addOfflineClientSession(realm, user2, "2456", "2123", fooClient.getId(), "something5");
commit();
+ // Searching by clients
+ Assert.assertEquals(2, session.users().getOfflineClientSessionsCount(realm, fooClient));
+ Assert.assertEquals(1, session.users().getOfflineClientSessionsCount(realm, barClient));
+
+ Collection<OfflineClientSessionModel> clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 10);
+ Assert.assertEquals(2, clientSessions.size());
+ clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 0, 1);
+ OfflineClientSessionModel cls = clientSessions.iterator().next();
+ assertSessionEquals(cls, "456", "123", fooClient.getId(), user1.getId(), "something2");
+ clientSessions = session.users().getOfflineClientSessions(realm, fooClient, 1, 1);
+ cls = clientSessions.iterator().next();
+ assertSessionEquals(cls, "2456", "2123", fooClient.getId(), user2.getId(), "something5");
+
+ clientSessions = session.users().getOfflineClientSessions(realm, barClient, 0, 10);
+ Assert.assertEquals(1, clientSessions.size());
+ cls = clientSessions.iterator().next();
+ assertSessionEquals(cls, "789", "123", barClient.getId(), user1.getId(), "something3");
+
realm = realmManager.getRealmByName("original");
realm.removeClient(barClient.getId());
@@ -305,9 +329,9 @@ public class UserModelTest extends AbstractModelTest {
realm = realmManager.getRealmByName("original");
user1 = session.users().getUserByUsername("user1", realm);
- Assert.assertEquals("something1", user1.getOfflineUserSession("123").getData());
- Assert.assertEquals("something2", user1.getOfflineClientSession("456").getData());
- Assert.assertNull(user1.getOfflineClientSession("789"));
+ Assert.assertEquals("something1", session.users().getOfflineUserSession(realm, user1, "123").getData());
+ Assert.assertEquals("something2", session.users().getOfflineClientSession(realm, user1, "456").getData());
+ Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
realm.removeClient(fooClient.getId());
@@ -315,27 +339,28 @@ public class UserModelTest extends AbstractModelTest {
realm = realmManager.getRealmByName("original");
user1 = session.users().getUserByUsername("user1", realm);
- Assert.assertNull(user1.getOfflineClientSession("456"));
- Assert.assertNull(user1.getOfflineClientSession("789"));
- Assert.assertNull(user1.getOfflineUserSession("123"));
- Assert.assertEquals(0, user1.getOfflineUserSessions().size());
- Assert.assertEquals(0, user1.getOfflineClientSessions().size());
+ Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "456"));
+ Assert.assertNull(session.users().getOfflineClientSession(realm, user1, "789"));
+ Assert.assertNull(session.users().getOfflineUserSession(realm, user1, "123"));
+ Assert.assertEquals(0, session.users().getOfflineUserSessions(realm, user1).size());
+ Assert.assertEquals(0, session.users().getOfflineClientSessions(realm, user1).size());
}
- private void addOfflineUserSession(UserModel user, String userSessionId, String data) {
+ private void addOfflineUserSession(RealmModel realm, UserModel user, String userSessionId, String data) {
OfflineUserSessionModel model = new OfflineUserSessionModel();
model.setUserSessionId(userSessionId);
model.setData(data);
- user.addOfflineUserSession(model);
+ session.users().addOfflineUserSession(realm, user, model);
}
- private void addOfflineClientSession(UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
+ private void addOfflineClientSession(RealmModel realm, UserModel user, String clientSessionId, String userSessionId, String clientId, String data) {
OfflineClientSessionModel model = new OfflineClientSessionModel();
model.setClientSessionId(clientSessionId);
model.setUserSessionId(userSessionId);
+ model.setUserId(user.getId());
model.setClientId(clientId);
model.setData(data);
- user.addOfflineClientSession(model);
+ session.users().addOfflineClientSession(realm, model);
}
public static void assertEquals(UserModel expected, UserModel actual) {
@@ -352,5 +377,14 @@ public class UserModelTest extends AbstractModelTest {
Assert.assertArrayEquals(expectedRequiredActions, actualRequiredActions);
}
+ private static void assertSessionEquals(OfflineClientSessionModel cls, String expectedClientSessionId, String expectedUserSessionId,
+ String expectedClientId, String expectedUserId, String expectedData) {
+ Assert.assertEquals(cls.getData(), expectedData);
+ Assert.assertEquals(cls.getClientSessionId(), expectedClientSessionId);
+ Assert.assertEquals(cls.getUserSessionId(), expectedUserSessionId);
+ Assert.assertEquals(cls.getUserId(), expectedUserId);
+ Assert.assertEquals(cls.getClientId(), expectedClientId);
+ }
+
}
diff --git a/testsuite/integration/src/test/resources/model/testrealm.json b/testsuite/integration/src/test/resources/model/testrealm.json
index f7c8cdd..521ef83 100755
--- a/testsuite/integration/src/test/resources/model/testrealm.json
+++ b/testsuite/integration/src/test/resources/model/testrealm.json
@@ -119,6 +119,19 @@
"openid-connect": [ "gss delegation credential" ]
}
}
+ ],
+ "offlineUserSessions": [
+ {
+ "userSessionId": "123",
+ "data": "something1",
+ "offlineClientSessions": [
+ {
+ "clientSessionId": "456",
+ "client": "OtherApp",
+ "data": "something2"
+ }
+ ]
+ }
]
},
{
diff --git a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index 644435a..916d855 100755
--- a/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty81/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -82,7 +82,7 @@ public class JettySamlTest {
list.add(new WebAppContext(new File(base, "bad-client-signed-post").toString(), "/bad-client-sales-post-sig"));
list.add(new WebAppContext(new File(base, "bad-realm-signed-post").toString(), "/bad-realm-sales-post-sig"));
list.add(new WebAppContext(new File(base, "encrypted-post").toString(), "/sales-post-enc"));
- SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+ SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
diff --git a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index c9323c4..c6092b4 100755
--- a/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty91/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -81,7 +81,7 @@ public class JettySamlTest {
list.add(new WebAppContext(new File(base, "bad-client-signed-post").toString(), "/bad-client-sales-post-sig"));
list.add(new WebAppContext(new File(base, "bad-realm-signed-post").toString(), "/bad-realm-sales-post-sig"));
list.add(new WebAppContext(new File(base, "encrypted-post").toString(), "/sales-post-enc"));
- SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+ SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
diff --git a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
index ac09618..96709ad 100755
--- a/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
+++ b/testsuite/jetty/jetty92/src/test/java/org/keycloak/testsuite/JettySamlTest.java
@@ -81,7 +81,7 @@ public class JettySamlTest {
list.add(new WebAppContext(new File(base, "bad-client-signed-post").toString(), "/bad-client-sales-post-sig"));
list.add(new WebAppContext(new File(base, "bad-realm-signed-post").toString(), "/bad-realm-sales-post-sig"));
list.add(new WebAppContext(new File(base, "encrypted-post").toString(), "/sales-post-enc"));
- SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+ SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
diff --git a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
index 29a2e05..f9cb853 100755
--- a/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
+++ b/testsuite/tomcat7/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
@@ -78,7 +78,7 @@ public class TomcatSamlTest {
tomcat.addWebapp("/bad-client-sales-post-sig", new File(base, "bad-client-signed-post").toString());
tomcat.addWebapp("/bad-realm-sales-post-sig", new File(base, "bad-realm-signed-post").toString());
tomcat.addWebapp("/sales-post-enc", new File(base, "encrypted-post").toString());
- SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+ SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
tomcat.start();
diff --git a/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java b/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
index 6e314ee..405c6ee 100755
--- a/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
+++ b/testsuite/tomcat8/src/test/java/org/keycloak/testsuite/TomcatSamlTest.java
@@ -76,7 +76,7 @@ public class TomcatSamlTest {
tomcat.addWebapp("/bad-client-sales-post-sig", new File(base, "bad-client-signed-post").toString());
tomcat.addWebapp("/bad-realm-sales-post-sig", new File(base, "bad-realm-signed-post").toString());
tomcat.addWebapp("/sales-post-enc", new File(base, "encrypted-post").toString());
- SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth", keycloakRule);
+ SamlAdapterTestStrategy.uploadSP("http://localhost:8081/auth");
tomcat.start();