From 0bb2ff9e8a6cf8da2cfa3c0917be53c2d2e6d5fb Mon Sep 17 00:00:00 2001 From: stianst Date: Mon, 10 Sep 2018 21:52:13 +0200 Subject: [PATCH] KEYCLOAK-8264 Update OpenShift Token Review endpoint to support additional algorithms and to update session last refresh on token introspection --- .../AccessTokenIntrospectionProvider.java | 2 +- .../keycloak/protocol/oidc/TokenManager.java | 29 ++++-- .../OpenShiftTokenReviewEndpoint.java | 22 ++--- .../OpenShiftTokenReviewEndpointTest.java | 95 +++++++++++++++++-- 4 files changed, 121 insertions(+), 27 deletions(-) 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 1035355b46..12ebd69df8 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 f87e115fa0..9ff893c294 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 100207be30..efe9aa2a2f 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 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 48006cac5b..4ecfe5534e 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; @@ -114,6 +120,69 @@ public class OpenShiftTokenReviewEndpointTest extends AbstractTestRealmKeycloakT r.assertScope("openid", "email", "profile"); } + @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") @@ -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);