keycloak-memoizeit

KEYCLOAK-8264 Update OpenShift Token Review endpoint to support

9/10/2018 4:52:13 PM

Details

diff --git a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
index 1035355..12ebd69 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/AccessTokenIntrospectionProvider.java
@@ -86,7 +86,7 @@ public class AccessTokenIntrospectionProvider implements TokenIntrospectionProvi
 
         RealmModel realm = this.session.getContext().getRealm();
 
-        return tokenManager.isTokenValid(session, realm, accessToken) ? accessToken : null;
+        return tokenManager.checkTokenValidForIntrospection(session, realm, accessToken) ? accessToken : null;
     }
 
     @Override
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 f87e115..9ff893c 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java
@@ -202,7 +202,17 @@ public class TokenManager {
         return new TokenValidation(user, userSession, clientSessionCtx, newToken);
     }
 
-    public boolean isTokenValid(KeycloakSession session, RealmModel realm, AccessToken token) throws OAuthErrorException {
+    /**
+     * Checks if the token is valid. Intended usage is for token introspection endpoints as the session last refresh
+     * is updated if the token was valid. This is used to keep the session alive when long lived tokens are used.
+     *
+     * @param session
+     * @param realm
+     * @param token
+     * @return
+     * @throws OAuthErrorException
+     */
+    public boolean checkTokenValidForIntrospection(KeycloakSession session, RealmModel realm, AccessToken token) throws OAuthErrorException {
         if (!token.isActive()) {
             return false;
         }
@@ -216,17 +226,24 @@ public class TokenManager {
             return false;
         }
 
+        boolean valid = false;
+
         UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), false, client.getId());
+
         if (AuthenticationManager.isSessionValid(realm, userSession)) {
-            return isUserValid(session, realm, token, userSession);
+            valid = isUserValid(session, realm, token, userSession);
+        } else {
+            userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
+            if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
+                valid = isUserValid(session, realm, token, userSession);
+            }
         }
 
-        userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true, client.getId());
-        if (AuthenticationManager.isOfflineSessionValid(realm, userSession)) {
-            return isUserValid(session, realm, token, userSession);
+        if (valid) {
+            userSession.setLastSessionRefresh(Time.currentTime());
         }
 
-        return false;
+        return valid;
     }
 
     private boolean isUserValid(KeycloakSession session, RealmModel realm, AccessToken token, UserSessionModel userSession) {
diff --git a/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java
index 100207b..efe9aa2 100644
--- a/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/openshift/OpenShiftTokenReviewEndpoint.java
@@ -16,8 +16,10 @@
  */
 package org.keycloak.protocol.openshift;
 
-import org.keycloak.RSATokenVerifier;
+import org.keycloak.TokenVerifier;
 import org.keycloak.common.VerificationException;
+import org.keycloak.crypto.SignatureProvider;
+import org.keycloak.crypto.SignatureVerifierContext;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
 import org.keycloak.events.EventBuilder;
@@ -39,7 +41,6 @@ import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
-import java.security.PublicKey;
 import java.util.List;
 
 /**
@@ -89,22 +90,19 @@ public class OpenShiftTokenReviewEndpoint implements OIDCExtProvider {
 
         AccessToken token = null;
         try {
-            RSATokenVerifier verifier = RSATokenVerifier.create(reviewRequest.getSpec().getToken())
+            TokenVerifier<AccessToken> verifier = TokenVerifier.create(reviewRequest.getSpec().getToken(), AccessToken.class)
                     .realmUrl(Urls.realmIssuer(session.getContext().getUri().getBaseUri(), realm.getName()));
 
-            PublicKey publicKey = session.keys().getRsaPublicKey(realm, verifier.getHeader().getKeyId());
-            if (publicKey == null) {
-                error(401, Errors.INVALID_TOKEN, "Invalid public key");
-            } else {
-                verifier.publicKey(publicKey);
-                verifier.verify();
-                token = verifier.getToken();
-            }
+            SignatureVerifierContext verifierContext = session.getProvider(SignatureProvider.class, verifier.getHeader().getAlgorithm().name()).verifier(verifier.getHeader().getKeyId());
+            verifier.verifierContext(verifierContext);
+
+            verifier.verify();
+            token = verifier.getToken();
         } catch (VerificationException e) {
             error(401, Errors.INVALID_TOKEN, "Token verification failure");
         }
 
-        if (!tokenManager.isTokenValid(session, realm, token)) {
+        if (!tokenManager.checkTokenValidForIntrospection(session, realm, token)) {
             error(401, Errors.INVALID_TOKEN, "Token verification failure");
         }
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java
index 48006ca..4ecfe55 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/openshift/OpenShiftTokenReviewEndpointTest.java
@@ -6,9 +6,14 @@ import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.keycloak.OAuth2Constants;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.RealmResource;
 import org.keycloak.broker.provider.util.SimpleHttp;
 import org.keycloak.common.util.Base64Url;
+import org.keycloak.crypto.Algorithm;
 import org.keycloak.events.Details;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.protocol.oidc.OIDCConfigAttributes;
 import org.keycloak.protocol.oidc.OIDCLoginProtocol;
 import org.keycloak.protocol.oidc.mappers.GroupMembershipMapper;
 import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
@@ -22,6 +27,7 @@ import org.keycloak.representations.idm.ProtocolMapperRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
 import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
 import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.admin.ApiUtil;
 import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
 import org.keycloak.testsuite.util.OAuthClient;
 import org.keycloak.testsuite.util.UserBuilder;
@@ -115,6 +121,69 @@ public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakT
     }
 
     @Test
+    public void longExpiration() {
+        ClientResource client = ApiUtil.findClientByClientId(adminClient.realm("test"), "test-app");
+        ClientRepresentation clientRep = client.toRepresentation();
+
+        try {
+            clientRep.getAttributes().put(OIDCConfigAttributes.ACCESS_TOKEN_LIFESPAN, "-1");
+            client.update(clientRep);
+
+            // Set time offset just before SSO idle, to get session last refresh updated
+
+            setTimeOffset(1500);
+
+            Review review = new Review();
+
+            review.invoke().assertSuccess();
+
+            // Bump last refresh updated again
+
+            setTimeOffset(3000);
+
+            review.invoke().assertSuccess();
+
+            // And, again
+
+            setTimeOffset(4500);
+
+            // Token should still be valid as session last refresh should have been updated
+
+            review.invoke().assertSuccess();
+        } finally {
+            clientRep.getAttributes().put(OIDCConfigAttributes.ACCESS_TOKEN_LIFESPAN, null);
+            client.update(clientRep);
+        }
+    }
+
+    @Test
+    public void hs256() {
+        RealmResource realm = adminClient.realm("test");
+        RealmRepresentation rep = realm.toRepresentation();
+
+        try {
+            rep.setDefaultSignatureAlgorithm(Algorithm.HS256);
+            realm.update(rep);
+
+            Review r = new Review().algorithm(Algorithm.HS256).invoke()
+                    .assertSuccess();
+
+            String userId = testRealm().users().search(r.username).get(0).getId();
+
+            OpenShiftTokenReviewResponseRepresentation.User user = r.response.getStatus().getUser();
+
+            assertEquals(userId, user.getUid());
+            assertEquals("test-user@localhost", user.getUsername());
+            assertNotNull(user.getExtra());
+
+            r.assertScope("openid", "email", "profile");
+        } finally {
+            rep.setDefaultSignatureAlgorithm(null);
+            realm.update(rep);
+        }
+    }
+
+    @Test
     public void groups() {
         new Review().username("groups-user")
                 .invoke()
@@ -194,7 +263,7 @@ public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakT
                     i.token = i.token.replaceFirst(header, newHeader);
                 })
                 .invoke()
-                .assertError(401, "Invalid public key");
+                .assertError(401, "Token verification failure");
     }
 
     @Test
@@ -251,6 +320,7 @@ public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakT
         private String clientId = "test-app";
         private String username = "test-user@localhost";
         private String password = "password";
+        private String algorithm = Algorithm.RS256;
         private InvokeRunnable runAfterTokenRequest;
 
         private String token;
@@ -262,6 +332,11 @@ public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakT
             return this;
         }
 
+        public Review algorithm(String algorithm) {
+            this.algorithm = algorithm;
+            return this;
+        }
+
         public Review runAfterTokenRequest(InvokeRunnable runnable) {
             this.runAfterTokenRequest = runnable;
             return this;
@@ -269,16 +344,20 @@ public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakT
 
         public Review invoke() {
             try {
-                String userId = testRealm().users().search(username).get(0).getId();
-                oauth.doLogin(username, password);
-                EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
+                if (token == null) {
+                    String userId = testRealm().users().search(username).get(0).getId();
+                    oauth.doLogin(username, password);
+                    EventRepresentation loginEvent = events.expectLogin().user(userId).assertEvent();
 
-                String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
-                OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password");
+                    String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+                    OAuthClient.AccessTokenResponse accessTokenResponse = oauth.doAccessTokenRequest(code, "password");
 
-                events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()).detail("client_auth_method", "testsuite-client-dummy").user(userId).assertEvent();
+                    events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID), loginEvent.getSessionId()).detail("client_auth_method", "testsuite-client-dummy").user(userId).assertEvent();
+
+                    token = accessTokenResponse.getAccessToken();
+                }
 
-                token = accessTokenResponse.getAccessToken();
+                assertEquals(algorithm, new JWSInput(token).getHeader().getAlgorithm().name());
 
                 if (runAfterTokenRequest != null) {
                     runAfterTokenRequest.run(this);