keycloak-aplcache

Details

diff --git a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
index d0c2a4a..9e58a2f 100755
--- a/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
+++ b/model/infinispan/src/main/java/org/keycloak/models/sessions/infinispan/UserSessionAdapter.java
@@ -225,6 +225,11 @@ public class UserSessionAdapter implements UserSessionModel {
     }
 
     @Override
+    public boolean isOffline() {
+        return offline;
+    }
+
+    @Override
     public String getNote(String name) {
         return entity.getNotes() != null ? entity.getNotes().get(name) : null;
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
index 64246e8..dd49136 100644
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/session/JpaUserSessionPersisterProvider.java
@@ -252,6 +252,7 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
         model.setUserSessionId(entity.getUserSessionId());
         model.setLastSessionRefresh(entity.getLastSessionRefresh());
         model.setData(entity.getData());
+        model.setOffline(offlineFromString(entity.getOffline()));
 
         Map<String, AuthenticatedClientSessionModel> clientSessions = new HashMap<>();
         return new PersistentUserSessionAdapter(model, realm, user, clientSessions);
@@ -287,4 +288,8 @@ public class JpaUserSessionPersisterProvider implements UserSessionPersisterProv
     private String offlineToString(boolean offline) {
         return offline ? "1" : "0";
     }
+
+    private boolean offlineFromString(String offlineStr) {
+        return "1".equals(offlineStr);
+    }
 }
diff --git a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
index 40fdada..5cb9fd3 100755
--- a/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/UserSessionModel.java
@@ -53,6 +53,8 @@ public interface UserSessionModel {
 
     void setLastSessionRefresh(int seconds);
 
+    boolean isOffline();
+
     /**
      * Returns map where key is ID of the client (its UUID) and value is ID respective {@link AuthenticatedClientSessionModel} object.
      * @return 
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
index 095a857..87df79f 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionAdapter.java
@@ -157,6 +157,11 @@ public class PersistentUserSessionAdapter implements UserSessionModel {
     }
 
     @Override
+    public boolean isOffline() {
+        return model.isOffline();
+    }
+
+    @Override
     public Map<String, AuthenticatedClientSessionModel> getAuthenticatedClientSessions() {
         return authenticatedClientSessions;
     }
diff --git a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java
index d7e0a04..ced1768 100644
--- a/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/session/PersistentUserSessionModel.java
@@ -24,7 +24,7 @@ public class PersistentUserSessionModel {
 
     private String userSessionId;
     private int lastSessionRefresh;
-
+    private boolean offline;
     private String data;
 
     public String getUserSessionId() {
@@ -43,6 +43,13 @@ public class PersistentUserSessionModel {
         this.lastSessionRefresh = lastSessionRefresh;
     }
 
+    public boolean isOffline() {
+        return offline;
+    }
+
+    public void setOffline(boolean offline) {
+        this.offline = offline;
+    }
 
     public String getData() {
         return data;
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 47f44e5..dd94094 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -635,11 +635,8 @@ public class TokenManager {
 
 
         token.setSessionState(session.getId());
+        token.expiration(getTokenExpiration(realm, session, clientSession));
 
-        int tokenLifespan = getTokenLifespan(realm, clientSession);
-        if (tokenLifespan > 0) {
-            token.expiration(Time.currentTime() + tokenLifespan);
-        }
         Set<String> allowedOrigins = client.getWebOrigins();
         if (allowedOrigins != null) {
             token.setAllowedOrigins(WebOriginsUtils.resolveValidWebOrigins(uriInfo, client));
@@ -647,13 +644,22 @@ public class TokenManager {
         return token;
     }
 
-    private int getTokenLifespan(RealmModel realm, AuthenticatedClientSessionModel clientSession) {
+    private int getTokenExpiration(RealmModel realm, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
         boolean implicitFlow = false;
         String responseType = clientSession.getNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM);
         if (responseType != null) {
             implicitFlow = OIDCResponseType.parse(responseType).isImplicitFlow();
         }
-        return implicitFlow ? realm.getAccessTokenLifespanForImplicitFlow() : realm.getAccessTokenLifespan();
+        int tokenLifespan = implicitFlow ? realm.getAccessTokenLifespanForImplicitFlow() : realm.getAccessTokenLifespan();
+
+        int expiration = Time.currentTime() + tokenLifespan;
+
+        if (!userSession.isOffline()) {
+            int sessionExpires = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
+            expiration = expiration <= sessionExpires ? expiration : sessionExpires;
+        }
+
+        return expiration;
     }
 
     protected void addComposites(AccessToken token, RoleModel role) {
@@ -765,13 +771,19 @@ public class TokenManager {
                 sessionManager.createOrUpdateOfflineSession(clientSession, userSession);
             } else {
                 refreshToken = new RefreshToken(accessToken);
-                refreshToken.expiration(Time.currentTime() + realm.getSsoSessionIdleTimeout());
+                refreshToken.expiration(getRefreshExpiration());
             }
             refreshToken.id(KeycloakModelUtils.generateId());
             refreshToken.issuedNow();
             return this;
         }
 
+        private int getRefreshExpiration() {
+            int sessionExpires = userSession.getStarted() + realm.getSsoSessionMaxLifespan();
+            int expiration = Time.currentTime() + realm.getSsoSessionIdleTimeout();
+            return expiration <= sessionExpires ? expiration : sessionExpires;
+        }
+
         public AccessTokenResponseBuilder generateIDToken() {
             if (accessToken == null) {
                 throw new IllegalStateException("accessToken not set");
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/Assert.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/Assert.java
index a9c93f6..4fc045b 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/Assert.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/Assert.java
@@ -34,6 +34,10 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -133,4 +137,9 @@ public class Assert extends org.junit.Assert {
         Assert.assertEquals(helpText, property.getHelpText());
         Assert.assertEquals(type, property.getType());
     }
+
+    public static void assertExpiration(int actual, int expected) {
+        org.junit.Assert.assertThat(actual, allOf(greaterThanOrEqualTo(expected - 50), lessThanOrEqualTo(expected)));
+    }
+
 }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
index 961bf19..cfb97ce 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java
@@ -34,6 +34,7 @@ import org.keycloak.admin.client.resource.ClientTemplateResource;
 import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.admin.client.resource.UserResource;
 import org.keycloak.common.enums.SslRequired;
+import org.keycloak.common.util.Time;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.jose.jws.JWSHeader;
@@ -80,6 +81,7 @@ import java.io.IOException;
 import java.net.URI;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.TimeUnit;
 
 import static org.hamcrest.Matchers.allOf;
 import static org.hamcrest.Matchers.greaterThanOrEqualTo;
@@ -94,8 +96,8 @@ import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
 import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
 import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
 import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper;
+import static org.keycloak.testsuite.Assert.assertExpiration;
 
-import org.keycloak.util.TokenUtil;
 import org.openqa.selenium.By;
 
 /**
@@ -974,6 +976,46 @@ public class AccessTokenTest extends AbstractKeycloakTest {
         }
     }
 
+    // KEYCLOAK-4215
+    @Test
+    public void expiration() throws Exception {
+        int sessionMax = (int) TimeUnit.MINUTES.toSeconds(30);
+        int sessionIdle = (int) TimeUnit.MINUTES.toSeconds(30);
+        int tokenLifespan = (int) TimeUnit.MINUTES.toSeconds(5);
+
+        RealmResource realm = adminClient.realm("test");
+        RealmRepresentation rep = realm.toRepresentation();
+        Integer originalSessionMax = rep.getSsoSessionMaxLifespan();
+        rep.setSsoSessionMaxLifespan(sessionMax);
+        realm.update(rep);
+
+        try {
+            oauth.doLogin("test-user@localhost", "password");
+
+            String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+            OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+            assertEquals(200, response.getStatusCode());
+
+            // Assert refresh expiration equals session idle
+            assertExpiration(response.getRefreshExpiresIn(), sessionIdle);
+
+            // Assert token expiration equals token lifespan
+            assertExpiration(response.getExpiresIn(), tokenLifespan);
+
+            setTimeOffset(sessionMax - 60);
+
+            response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password");
+            assertEquals(200, response.getStatusCode());
+
+            // Assert expiration equals session expiration
+            assertExpiration(response.getRefreshExpiresIn(), 60);
+            assertExpiration(response.getExpiresIn(), 60);
+        } finally {
+            rep.setSsoSessionMaxLifespan(originalSessionMax);
+            realm.update(rep);
+        }
+    }
+
     private IDToken getIdToken(org.keycloak.representations.AccessTokenResponse tokenResponse) throws JWSInputException {
         JWSInput input = new JWSInput(tokenResponse.getIdToken());
         return input.readJsonContent(IDToken.class);