diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java index d7985b0714..925a0dba15 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java @@ -17,26 +17,21 @@ package org.keycloak.adapters; -import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; import org.jboss.logging.Logger; import org.keycloak.adapters.authentication.ClientCredentialsProvider; +import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.rotation.PublicKeyLocator; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.common.enums.RelativeUrlsUsed; import org.keycloak.common.enums.SslRequired; import org.keycloak.enums.TokenStore; import org.keycloak.representations.adapters.config.AdapterConfig; -import org.keycloak.representations.idm.PublishedRealmRepresentation; -import org.keycloak.util.JsonSerialization; import org.keycloak.common.util.KeycloakUriBuilder; -import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.io.InputStream; import java.net.URI; -import java.security.PublicKey; import java.util.Map; /** @@ -57,7 +52,7 @@ public class AdapterDeploymentContext { * during the application deployment's life cycle. * * @param deployment A KeycloakConfigResolver, possibly missing the Auth - * Server URL and/or Realm Public Key + * Server URL */ public AdapterDeploymentContext(KeycloakDeployment deployment) { this.deployment = deployment; @@ -79,7 +74,6 @@ public class AdapterDeploymentContext { /** * For single-tenant deployments, it complements KeycloakDeployment * by resolving a relative Auth Server's URL based on the current request - * and, if needed, will lazily resolve the Realm's Public Key. * * For multi-tenant deployments, defers the resolution of KeycloakDeployment * to the KeycloakConfigResolver . @@ -98,8 +92,8 @@ public class AdapterDeploymentContext { if (deployment.getAuthServerBaseUrl() == null) return deployment; KeycloakDeployment resolvedDeployment = resolveUrls(deployment, facade); - if (resolvedDeployment.getRealmKey() == null) { - resolveRealmKey(resolvedDeployment); + if (resolvedDeployment.getPublicKeyLocator() == null) { + throw new RuntimeException("KeycloakDeployment was never initialized through appropriate SPIs"); } return resolvedDeployment; } @@ -115,45 +109,6 @@ public class AdapterDeploymentContext { } } - public void resolveRealmKey(KeycloakDeployment deployment) { - if (deployment.getClient() == null) { - throw new RuntimeException("KeycloakDeployment was never initialized through appropriate SPIs"); - } - HttpGet get = new HttpGet(deployment.getRealmInfoUrl()); - try { - HttpResponse response = deployment.getClient().execute(get); - int status = response.getStatusLine().getStatusCode(); - if (status != 200) { - close(response); - throw new RuntimeException("Unable to resolve realm public key remotely, status = " + status); - } - HttpEntity entity = response.getEntity(); - if (entity == null) { - throw new RuntimeException("Unable to resolve realm public key remotely. There was no entity."); - } - InputStream is = entity.getContent(); - try { - ByteArrayOutputStream os = new ByteArrayOutputStream(); - int c; - while ((c = is.read()) != -1) { - os.write(c); - } - byte[] bytes = os.toByteArray(); - String json = new String(bytes); - PublishedRealmRepresentation rep = JsonSerialization.readValue(json, PublishedRealmRepresentation.class); - deployment.setRealmKey(rep.getPublicKey()); - } finally { - try { - is.close(); - } catch (IOException ignored) { - - } - } - } catch (IOException e) { - throw new RuntimeException("Unable to resolve realm public key remotely", e); - } - } - /** * This delegate is used to store temporary, per-request metadata like request resolved URLs. * Ever method is delegated except URL get methods and isConfigured() @@ -207,6 +162,11 @@ public class AdapterDeploymentContext { return (this.unregisterNodeUrl != null) ? this.unregisterNodeUrl : delegate.getUnregisterNodeUrl(); } + @Override + public String getJwksUrl() { + return (this.jwksUrl != null) ? this.jwksUrl : delegate.getJwksUrl(); + } + @Override public String getResourceName() { return delegate.getResourceName(); @@ -223,13 +183,13 @@ public class AdapterDeploymentContext { } @Override - public PublicKey getRealmKey() { - return delegate.getRealmKey(); + public void setPublicKeyLocator(PublicKeyLocator publicKeyLocator) { + delegate.setPublicKeyLocator(publicKeyLocator); } @Override - public void setRealmKey(PublicKey realmKey) { - delegate.setRealmKey(realmKey); + public PublicKeyLocator getPublicKeyLocator() { + return delegate.getPublicKeyLocator(); } @Override @@ -466,6 +426,26 @@ public class AdapterDeploymentContext { public void setTokenMinimumTimeToLive(final int tokenMinimumTimeToLive) { delegate.setTokenMinimumTimeToLive(tokenMinimumTimeToLive); } + + @Override + public PolicyEnforcer getPolicyEnforcer() { + return delegate.getPolicyEnforcer(); + } + + @Override + public void setPolicyEnforcer(PolicyEnforcer policyEnforcer) { + delegate.setPolicyEnforcer(policyEnforcer); + } + + @Override + public void setMinTimeBetweenJwksRequests(int minTimeBetweenJwksRequests) { + delegate.setMinTimeBetweenJwksRequests(minTimeBetweenJwksRequests); + } + + @Override + public int getMinTimeBetweenJwksRequests() { + return delegate.getMinTimeBetweenJwksRequests(); + } } protected KeycloakUriBuilder getBaseBuilder(HttpFacade facade, String base) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java index 9af6214ba2..70b90b1044 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java @@ -19,6 +19,7 @@ package org.keycloak.adapters; import org.jboss.logging.Logger; import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; import org.keycloak.adapters.spi.HttpFacade; @@ -84,7 +85,7 @@ public class BearerTokenRequestAuthenticator { protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) { try { - token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl()); + token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); } catch (VerificationException e) { log.error("Failed to verify token", e); challenge = challengeResponse(exchange, OIDCAuthenticationError.Reason.INVALID_TOKEN, "invalid_token", e.getMessage()); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java index 7f66dbf2e6..2093645533 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/CookieTokenStore.java @@ -22,6 +22,7 @@ import java.io.IOException; import org.jboss.logging.Logger; import org.keycloak.KeycloakPrincipal; import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.common.VerificationException; import org.keycloak.constants.AdapterConstants; @@ -73,7 +74,7 @@ public class CookieTokenStore { try { // Skip check if token is active now. It's supposed to be done later by the caller - AccessToken accessToken = RSATokenVerifier.verifyToken(accessTokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl(), false, true); + AccessToken accessToken = AdapterRSATokenVerifier.verifyToken(accessTokenString, deployment, false, true); IDToken idToken; if (idTokenString != null && idTokenString.length() > 0) { try { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java new file mode 100644 index 0000000000..e01f7dc32e --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpAdapterUtils.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.client.methods.HttpRequestBase; +import org.keycloak.util.JsonSerialization; + +/** + * @author Marek Posolda + */ +public class HttpAdapterUtils { + + + public static T sendJsonHttpRequest(KeycloakDeployment deployment, HttpRequestBase httpRequest, Class clazz) throws HttpClientAdapterException { + try { + HttpResponse response = deployment.getClient().execute(httpRequest); + int status = response.getStatusLine().getStatusCode(); + if (status != 200) { + close(response); + throw new HttpClientAdapterException("Unexpected status = " + status); + } + HttpEntity entity = response.getEntity(); + if (entity == null) { + throw new HttpClientAdapterException("There was no entity."); + } + InputStream is = entity.getContent(); + try { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + int c; + while ((c = is.read()) != -1) { + os.write(c); + } + byte[] bytes = os.toByteArray(); + String json = new String(bytes); + return JsonSerialization.readValue(json, clazz); + } finally { + try { + is.close(); + } catch (IOException ignored) { + + } + } + } catch (IOException e) { + throw new HttpClientAdapterException("IO error", e); + } + } + + + private static void close(HttpResponse response) { + if (response.getEntity() != null) { + try { + response.getEntity().getContent().close(); + } catch (IOException e) { + + } + } + } +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientAdapterException.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientAdapterException.java new file mode 100644 index 0000000000..7d303e290d --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/HttpClientAdapterException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters; + +/** + * @author Marek Posolda + */ +public class HttpClientAdapterException extends Exception { + + public HttpClientAdapterException(String message) { + super(message); + } + + public HttpClientAdapterException(String message, Throwable t) { + super(message, t); + } +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index 901b3ea99c..30c40c37ca 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -21,6 +21,7 @@ import org.apache.http.client.HttpClient; import org.jboss.logging.Logger; import org.keycloak.adapters.authentication.ClientCredentialsProvider; import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.rotation.PublicKeyLocator; import org.keycloak.constants.ServiceUrlConstants; import org.keycloak.common.enums.RelativeUrlsUsed; import org.keycloak.common.enums.SslRequired; @@ -29,7 +30,6 @@ import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.common.util.KeycloakUriBuilder; import java.net.URI; -import java.security.PublicKey; import java.util.HashMap; import java.util.Map; @@ -43,7 +43,7 @@ public class KeycloakDeployment { protected RelativeUrlsUsed relativeUrls; protected String realm; - protected volatile PublicKey realmKey; + protected PublicKeyLocator publicKeyLocator; protected String authServerBaseUrl; protected String realmInfoUrl; protected KeycloakUriBuilder authUrl; @@ -52,6 +52,7 @@ public class KeycloakDeployment { protected String accountUrl; protected String registerNodeUrl; protected String unregisterNodeUrl; + protected String jwksUrl; protected String principalAttribute = "sub"; protected String resourceName; @@ -79,13 +80,14 @@ public class KeycloakDeployment { protected volatile int notBefore; protected int tokenMinimumTimeToLive; + protected int minTimeBetweenJwksRequests; private PolicyEnforcer policyEnforcer; public KeycloakDeployment() { } public boolean isConfigured() { - return getRealm() != null && getRealmKey() != null && (isBearerOnly() || getAuthServerBaseUrl() != null); + return getRealm() != null && getPublicKeyLocator() != null && (isBearerOnly() || getAuthServerBaseUrl() != null); } public String getResourceName() { @@ -100,12 +102,12 @@ public class KeycloakDeployment { this.realm = realm; } - public PublicKey getRealmKey() { - return realmKey; + public PublicKeyLocator getPublicKeyLocator() { + return publicKeyLocator; } - public void setRealmKey(PublicKey realmKey) { - this.realmKey = realmKey; + public void setPublicKeyLocator(PublicKeyLocator publicKeyLocator) { + this.publicKeyLocator = publicKeyLocator; } public String getAuthServerBaseUrl() { @@ -147,6 +149,7 @@ public class KeycloakDeployment { accountUrl = authUrlBuilder.clone().path(ServiceUrlConstants.ACCOUNT_SERVICE_PATH).build(getRealm()).toString(); registerNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_REGISTER_NODE_PATH).build(getRealm()).toString(); unregisterNodeUrl = authUrlBuilder.clone().path(ServiceUrlConstants.CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH).build(getRealm()).toString(); + jwksUrl = authUrlBuilder.clone().path(ServiceUrlConstants.JWKS_URL).build(getRealm()).toString(); } public RelativeUrlsUsed getRelativeUrls() { @@ -181,6 +184,10 @@ public class KeycloakDeployment { return unregisterNodeUrl; } + public String getJwksUrl() { + return jwksUrl; + } + public void setResourceName(String resourceName) { this.resourceName = resourceName; } @@ -369,6 +376,14 @@ public class KeycloakDeployment { this.tokenMinimumTimeToLive = tokenMinimumTimeToLive; } + public int getMinTimeBetweenJwksRequests() { + return minTimeBetweenJwksRequests; + } + + public void setMinTimeBetweenJwksRequests(int minTimeBetweenJwksRequests) { + this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests; + } + public void setPolicyEnforcer(PolicyEnforcer policyEnforcer) { this.policyEnforcer = policyEnforcer; } diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index 5d54df9bab..04e16f895a 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -22,6 +22,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.jboss.logging.Logger; import org.keycloak.adapters.authentication.ClientCredentialsProviderUtils; import org.keycloak.adapters.authorization.PolicyEnforcer; +import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator; +import org.keycloak.adapters.rotation.JWKPublicKeyLocator; import org.keycloak.common.enums.SslRequired; import org.keycloak.common.util.PemUtils; import org.keycloak.enums.TokenStore; @@ -59,11 +61,16 @@ public class KeycloakDeploymentBuilder { PublicKey realmKey; try { realmKey = PemUtils.decodePublicKey(realmKeyPem); + HardcodedPublicKeyLocator pkLocator = new HardcodedPublicKeyLocator(realmKey); + deployment.setPublicKeyLocator(pkLocator); } catch (Exception e) { throw new RuntimeException(e); } - deployment.setRealmKey(realmKey); + } else { + JWKPublicKeyLocator pkLocator = new JWKPublicKeyLocator(); + deployment.setPublicKeyLocator(pkLocator); } + if (adapterConfig.getSslRequired() != null) { deployment.setSslRequired(SslRequired.valueOf(adapterConfig.getSslRequired().toUpperCase())); } else { @@ -97,6 +104,7 @@ public class KeycloakDeploymentBuilder { deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup()); deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod()); deployment.setTokenMinimumTimeToLive(adapterConfig.getTokenMinimumTimeToLive()); + deployment.setMinTimeBetweenJwksRequests(adapterConfig.getMinTimeBetweenJwksRequests()); if (realmKeyPem == null && adapterConfig.isBearerOnly() && adapterConfig.getAuthServerUrl() == null) { throw new IllegalArgumentException("For bearer auth, you must set the realm-public-key or auth-server-url"); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java index 73aa0f52e7..02637c0810 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/OAuthRequestAuthenticator.java @@ -20,6 +20,7 @@ package org.keycloak.adapters; import org.jboss.logging.Logger; import org.keycloak.OAuth2Constants; import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.spi.AdapterSessionStore; import org.keycloak.adapters.spi.AuthChallenge; import org.keycloak.adapters.spi.AuthOutcome; @@ -342,7 +343,7 @@ public class OAuthRequestAuthenticator { refreshToken = tokenResponse.getRefreshToken(); idTokenString = tokenResponse.getIdToken(); try { - token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl()); + token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); if (idTokenString != null) { try { JWSInput input = new JWSInput(idTokenString); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java index 1a8c735afd..5a1df8c1b6 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/PreAuthActionsHandler.java @@ -17,7 +17,10 @@ package org.keycloak.adapters; +import java.security.PublicKey; + import org.jboss.logging.Logger; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.adapters.spi.UserSessionManagement; import org.keycloak.jose.jws.JWSInputException; @@ -198,7 +201,8 @@ public class PreAuthActionsHandler { try { JWSInput input = new JWSInput(token); - if (RSAProvider.verify(input, deployment.getRealmKey())) { + PublicKey publicKey = AdapterRSATokenVerifier.getPublicKey(input, deployment); + if (RSAProvider.verify(input, publicKey)) { return input; } } catch (JWSInputException ignore) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java index 7cfc7a698b..75f0cb8f2a 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/RefreshableKeycloakSecurityContext.java @@ -21,6 +21,7 @@ import org.jboss.logging.Logger; import org.keycloak.AuthorizationContext; import org.keycloak.KeycloakSecurityContext; import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.common.util.Time; import org.keycloak.representations.AccessToken; @@ -130,7 +131,7 @@ public class RefreshableKeycloakSecurityContext extends KeycloakSecurityContext String tokenString = response.getToken(); AccessToken token = null; try { - token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl()); + token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); log.debug("Token Verification succeeded!"); } catch (VerificationException e) { log.error("failed verification of token"); diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java index f1be944353..1c900b8657 100644 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/authorization/KeycloakAdapterPolicyEnforcer.java @@ -21,6 +21,7 @@ import org.jboss.logging.Logger; import org.keycloak.RSATokenVerifier; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.OIDCHttpFacade; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.adapters.spi.HttpFacade; import org.keycloak.authorization.client.AuthorizationDeniedException; import org.keycloak.authorization.client.AuthzClient; @@ -120,7 +121,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { AuthorizationResponse authzResponse = authzClient.authorization(accessToken).authorize(authzRequest); if (authzResponse != null) { - return RSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment.getRealmKey(), deployment.getRealmInfoUrl()); + return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment); } return null; @@ -130,7 +131,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { if (token.getAuthorization() == null) { EntitlementResponse authzResponse = authzClient.entitlement(accessToken).getAll(authzClient.getConfiguration().getClientId()); - return RSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment.getRealmKey(), deployment.getRealmInfoUrl()); + return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment); } else { EntitlementRequest request = new EntitlementRequest(); PermissionRequest permissionRequest = new PermissionRequest(); @@ -139,7 +140,7 @@ public class KeycloakAdapterPolicyEnforcer extends AbstractPolicyEnforcer { permissionRequest.setScopes(new HashSet<>(pathConfig.getScopes())); request.addPermission(permissionRequest); EntitlementResponse authzResponse = authzClient.entitlement(accessToken).get(authzClient.getConfiguration().getClientId(), request); - return RSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment.getRealmKey(), deployment.getRealmInfoUrl()); + return AdapterRSATokenVerifier.verifyToken(authzResponse.getRpt(), deployment); } } } catch (AuthorizationDeniedException e) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java index 51be55137f..75086e7804 100755 --- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/jaas/AbstractKeycloakLoginModule.java @@ -43,6 +43,7 @@ import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.RefreshableKeycloakSecurityContext; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.common.util.FindFile; import org.keycloak.representations.AccessToken; @@ -88,9 +89,6 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule { try { InputStream is = FindFile.findFile(keycloakConfigFile); KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is); - if (kd.getRealmKey() == null) { - new AdapterDeploymentContext().resolveRealmKey(kd); - } return kd; } catch (RuntimeException e) { getLogger().debug("Unable to find or parse file " + keycloakConfigFile + " due to " + e.getMessage(), e); @@ -190,7 +188,7 @@ public abstract class AbstractKeycloakLoginModule implements LoginModule { protected Auth bearerAuth(String tokenString) throws VerificationException { - AccessToken token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl()); + AccessToken token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); boolean verifyCaller; if (deployment.isUseResourceRoleMappings()) { diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterRSATokenVerifier.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterRSATokenVerifier.java new file mode 100644 index 0000000000..c69ee38d65 --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/AdapterRSATokenVerifier.java @@ -0,0 +1,64 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.rotation; + +import java.security.PublicKey; + +import org.jboss.logging.Logger; +import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.common.VerificationException; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.representations.AccessToken; + +/** + * @author Marek Posolda + */ +public class AdapterRSATokenVerifier { + + private static final Logger log = Logger.getLogger(AdapterRSATokenVerifier.class); + + public static AccessToken verifyToken(String tokenString, KeycloakDeployment deployment) throws VerificationException { + return verifyToken(tokenString, deployment, true, true); + } + + + public static PublicKey getPublicKey(JWSInput input, KeycloakDeployment deployment) throws VerificationException { + PublicKeyLocator pkLocator = deployment.getPublicKeyLocator(); + + PublicKey publicKey = pkLocator.getPublicKey(input, deployment); + if (publicKey == null) { + log.errorf("Didn't find publicKey for kid: %s", input.getHeader().getKeyId()); + throw new VerificationException("Didn't find publicKey for specified kid"); + } + + return publicKey; + } + + public static AccessToken verifyToken(String tokenString, KeycloakDeployment deployment, boolean checkActive, boolean checkTokenType) throws VerificationException { + JWSInput input; + try { + input = new JWSInput(tokenString); + } catch (Exception e) { + throw new VerificationException("Couldn't parse token", e); + } + + PublicKey publicKey = getPublicKey(input, deployment); + return RSATokenVerifier.verifyToken(input, publicKey, deployment.getRealmInfoUrl(), checkActive, checkTokenType); + } +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/HardcodedPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/HardcodedPublicKeyLocator.java new file mode 100644 index 0000000000..40fb71ad77 --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/HardcodedPublicKeyLocator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.rotation; + +import java.security.PublicKey; + +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.jose.jws.JWSInput; + +/** + * @author Marek Posolda + */ +public class HardcodedPublicKeyLocator implements PublicKeyLocator { + + private PublicKey publicKey; + + public HardcodedPublicKeyLocator(PublicKey publicKey) { + this.publicKey = publicKey; + } + + @Override + public PublicKey getPublicKey(JWSInput input, KeycloakDeployment deployment) { + return publicKey; + } +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java new file mode 100644 index 0000000000..500392338c --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/JWKPublicKeyLocator.java @@ -0,0 +1,107 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.rotation; + +import java.security.PublicKey; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.http.client.methods.HttpGet; +import org.jboss.logging.Logger; +import org.keycloak.adapters.HttpAdapterUtils; +import org.keycloak.adapters.HttpClientAdapterException; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.common.util.Time; +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jws.JWSInput; +import org.keycloak.util.JWKSUtils; + +/** + * When needed, publicKeys are downloaded by sending request to realm's jwks_url + * + * @author Marek Posolda + */ +public class JWKPublicKeyLocator implements PublicKeyLocator { + + private static final Logger log = Logger.getLogger(JWKPublicKeyLocator.class); + + private Map currentKeys = new ConcurrentHashMap<>(); + + private volatile int lastRequestTime = 0; + + @Override + public PublicKey getPublicKey(JWSInput input, KeycloakDeployment deployment) { + String kid = input.getHeader().getKeyId(); + return getPublicKey(kid, deployment); + } + + + private PublicKey getPublicKey(String kid, KeycloakDeployment deployment) { + int minTimeBetweenRequests = deployment.getMinTimeBetweenJwksRequests(); + + // Check if key is in cache. + PublicKey publicKey = currentKeys.get(kid); + if (publicKey != null) { + return publicKey; + } + + int currentTime = Time.currentTime(); + + // Check if we are allowed to send request + if (currentTime > lastRequestTime + minTimeBetweenRequests) { + synchronized (this) { + currentTime = Time.currentTime(); + if (currentTime > lastRequestTime + minTimeBetweenRequests) { + sendRequest(deployment); + lastRequestTime = currentTime; + } else { + // TODO: debug + log.infof("Won't send request to realm jwks url. Last request time was %d", lastRequestTime); + } + } + } + + return currentKeys.get(kid); + + } + + + private void sendRequest(KeycloakDeployment deployment) { + // Send the request + // TODO: trace or remove? + log.infof("Going to send request to retrieve new set of realm public keys for client %s", deployment.getResourceName()); + + HttpGet getMethod = new HttpGet(deployment.getJwksUrl()); + try { + JSONWebKeySet jwks = HttpAdapterUtils.sendJsonHttpRequest(deployment, getMethod, JSONWebKeySet.class); + + Map publicKeys = JWKSUtils.getKeysForUse(jwks, JWK.Use.SIG); + + // TODO: Debug with condition + log.infof("Realm public keys successfully retrieved for client %s. New kids: %s", deployment.getResourceName(), publicKeys.keySet().toString()); + + // Update current keys + currentKeys.clear(); + currentKeys.putAll(publicKeys); + + } catch (HttpClientAdapterException e) { + log.error("Error when sending request to retrieve realm keys", e); + } + } +} diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/PublicKeyLocator.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/PublicKeyLocator.java new file mode 100644 index 0000000000..bda80dc3a3 --- /dev/null +++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/rotation/PublicKeyLocator.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.adapters.rotation; + +import java.security.PublicKey; + +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.jose.jws.JWSInput; + +/** + * @author Marek Posolda + */ +public interface PublicKeyLocator { + + /** + * @param input + * @param deployment + * @return publicKey, which should be used for verify signature on given "input" + */ + PublicKey getPublicKey(JWSInput input, KeycloakDeployment deployment); + +} diff --git a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java index 4be511023c..d1ce748f63 100644 --- a/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java +++ b/adapters/oidc/adapter-core/src/test/java/org/keycloak/adapters/KeycloakDeploymentBuilderTest.java @@ -21,6 +21,8 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.junit.Test; import org.keycloak.adapters.authentication.ClientIdAndSecretCredentialsProvider; import org.keycloak.adapters.authentication.JWTClientCredentialsProvider; +import org.keycloak.adapters.rotation.HardcodedPublicKeyLocator; +import org.keycloak.adapters.rotation.JWKPublicKeyLocator; import org.keycloak.common.enums.RelativeUrlsUsed; import org.keycloak.common.enums.SslRequired; import org.keycloak.enums.TokenStore; @@ -39,7 +41,11 @@ public class KeycloakDeploymentBuilderTest { KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak.json")); assertEquals("demo", deployment.getRealm()); assertEquals("customer-portal", deployment.getResourceName()); - assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"), deployment.getRealmKey()); + + assertTrue(deployment.getPublicKeyLocator() instanceof HardcodedPublicKeyLocator); + assertEquals(PemUtils.decodePublicKey("MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"), + deployment.getPublicKeyLocator().getPublicKey(null, deployment)); + assertEquals("https://localhost:8443/auth/realms/demo/protocol/openid-connect/auth", deployment.getAuthUrl().build().toString()); assertEquals(SslRequired.EXTERNAL, deployment.getSslRequired()); assertTrue(deployment.isUseResourceRoleMappings()); @@ -62,12 +68,16 @@ public class KeycloakDeploymentBuilderTest { assertEquals(TokenStore.COOKIE, deployment.getTokenStore()); assertEquals("email", deployment.getPrincipalAttribute()); assertEquals(10, deployment.getTokenMinimumTimeToLive()); + assertEquals(20, deployment.getMinTimeBetweenJwksRequests()); } @Test public void loadNoClientCredentials() throws Exception { KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(getClass().getResourceAsStream("/keycloak-no-credentials.json")); assertEquals(ClientIdAndSecretCredentialsProvider.PROVIDER_ID, deployment.getClientAuthenticator().getId()); + + assertTrue(deployment.getPublicKeyLocator() instanceof JWKPublicKeyLocator); + assertEquals(10, deployment.getMinTimeBetweenJwksRequests()); } @Test diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak-no-credentials.json b/adapters/oidc/adapter-core/src/test/resources/keycloak-no-credentials.json index 5f223ac75f..a3c4026c34 100644 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak-no-credentials.json +++ b/adapters/oidc/adapter-core/src/test/resources/keycloak-no-credentials.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "customer-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "https://localhost:8443/auth", "public-client": true, "expose-token": true diff --git a/adapters/oidc/adapter-core/src/test/resources/keycloak.json b/adapters/oidc/adapter-core/src/test/resources/keycloak.json index 7bf269f2c1..a8afd22cf2 100644 --- a/adapters/oidc/adapter-core/src/test/resources/keycloak.json +++ b/adapters/oidc/adapter-core/src/test/resources/keycloak.json @@ -29,5 +29,6 @@ "register-node-period": 1000, "token-store": "cookie", "principal-attribute": "email", - "token-minimum-time-to-live": 10 + "token-minimum-time-to-live": 10, + "min-time-between-jwks-requests": 20 } \ No newline at end of file diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java index fc0e47fa50..c52a44af65 100644 --- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java +++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java @@ -20,6 +20,7 @@ package org.keycloak.adapters.installed; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; @@ -213,7 +214,7 @@ public class KeycloakInstalled { refreshToken = tokenResponse.getRefreshToken(); idTokenString = tokenResponse.getIdToken(); - token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealmInfoUrl()); + token = AdapterRSATokenVerifier.verifyToken(tokenString, deployment); if (idTokenString != null) { try { JWSInput input = new JWSInput(idTokenString); diff --git a/core/src/main/java/org/keycloak/RSATokenVerifier.java b/core/src/main/java/org/keycloak/RSATokenVerifier.java index 27a430102b..562261de5e 100755 --- a/core/src/main/java/org/keycloak/RSATokenVerifier.java +++ b/core/src/main/java/org/keycloak/RSATokenVerifier.java @@ -38,6 +38,12 @@ public class RSATokenVerifier { public static AccessToken verifyToken(String tokenString, PublicKey realmKey, String realmUrl, boolean checkActive, boolean checkTokenType) throws VerificationException { AccessToken token = toAccessToken(tokenString, realmKey); + tokenVerifications(token, realmUrl, checkActive, checkTokenType); + + return token; + } + + private static void tokenVerifications(AccessToken token, String realmUrl, boolean checkActive, boolean checkTokenType) throws VerificationException { String user = token.getSubject(); if (user == null) { throw new VerificationException("Token user was null."); @@ -60,9 +66,9 @@ public class RSATokenVerifier { throw new VerificationException("Token is not active."); } - return token; } + public static AccessToken toAccessToken(String tokenString, PublicKey realmKey) throws VerificationException { JWSInput input; try { @@ -81,6 +87,23 @@ public class RSATokenVerifier { return token; } + + public static AccessToken verifyToken(JWSInput input, PublicKey realmKey, String realmUrl, boolean checkActive, boolean checkTokenType) throws VerificationException { + if (!isPublicKeyValid(input, realmKey)) throw new VerificationException("Invalid token signature."); + + AccessToken token; + try { + token = input.readJsonContent(AccessToken.class); + } catch (JWSInputException e) { + throw new VerificationException("Couldn't parse token signature", e); + } + + tokenVerifications(token, realmUrl, checkActive, checkTokenType); + + return token; + } + + private static boolean isPublicKeyValid(JWSInput input, PublicKey realmKey) throws VerificationException { try { return RSAProvider.verify(input, realmKey); diff --git a/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java b/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java index a21f26284b..36b4f1d2d6 100755 --- a/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java +++ b/core/src/main/java/org/keycloak/constants/ServiceUrlConstants.java @@ -30,5 +30,6 @@ public interface ServiceUrlConstants { public static final String REALM_INFO_PATH = "/realms/{realm-name}"; public static final String CLIENTS_MANAGEMENT_REGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/register-node"; public static final String CLIENTS_MANAGEMENT_UNREGISTER_NODE_PATH = "/realms/{realm-name}/clients-managements/unregister-node"; + public static final String JWKS_URL = "/realms/{realm-name}/protocol/openid-connect/certs"; } diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java index 7a31f72b6a..a20d253241 100755 --- a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java +++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java @@ -21,7 +21,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import org.keycloak.common.util.Base64Url; import org.keycloak.util.JsonSerialization; -import java.io.InputStream; import java.math.BigInteger; import java.security.KeyFactory; import java.security.PublicKey; @@ -66,8 +65,8 @@ public class JWKParser { } public PublicKey toPublicKey() { - String algorithm = jwk.getKeyType(); - if (isAlgorithmSupported(algorithm)) { + String keyType = jwk.getKeyType(); + if (isKeyTypeSupported(keyType)) { BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString())); BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString())); @@ -77,12 +76,12 @@ public class JWKParser { throw new RuntimeException(e); } } else { - throw new RuntimeException("Unsupported algorithm " + algorithm); + throw new RuntimeException("Unsupported keyType " + keyType); } } - public boolean isAlgorithmSupported(String algorithm) { - return RSAPublicJWK.RSA.equals(algorithm); + public boolean isKeyTypeSupported(String keyType) { + return RSAPublicJWK.RSA.equals(keyType); } } diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java index 91fb5f0e9c..c4818b45c7 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java @@ -36,7 +36,7 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder; "client-keystore", "client-keystore-password", "client-key-password", "always-refresh-token", "register-node-at-startup", "register-node-period", "token-store", "principal-attribute", - "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", + "proxy-url", "turn-off-change-session-id-on-login", "token-minimum-time-to-live", "min-time-between-jwks-requests", "policy-enforcer" }) public class AdapterConfig extends BaseAdapterConfig { @@ -71,6 +71,8 @@ public class AdapterConfig extends BaseAdapterConfig { protected Boolean turnOffChangeSessionIdOnLogin; @JsonProperty("token-minimum-time-to-live") protected int tokenMinimumTimeToLive = 0; + @JsonProperty("min-time-between-jwks-requests") + protected int minTimeBetweenJwksRequests = 10; @JsonProperty("policy-enforcer") protected PolicyEnforcerConfig policyEnforcerConfig; @@ -216,4 +218,11 @@ public class AdapterConfig extends BaseAdapterConfig { this.tokenMinimumTimeToLive = tokenMinimumTimeToLive; } + public int getMinTimeBetweenJwksRequests() { + return minTimeBetweenJwksRequests; + } + + public void setMinTimeBetweenJwksRequests(int minTimeBetweenJwksRequests) { + this.minTimeBetweenJwksRequests = minTimeBetweenJwksRequests; + } } diff --git a/core/src/main/java/org/keycloak/util/JWKSUtils.java b/core/src/main/java/org/keycloak/util/JWKSUtils.java new file mode 100644 index 0000000000..72ffe91c86 --- /dev/null +++ b/core/src/main/java/org/keycloak/util/JWKSUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.util; + +import java.security.PublicKey; +import java.util.HashMap; +import java.util.Map; + +import org.keycloak.jose.jwk.JSONWebKeySet; +import org.keycloak.jose.jwk.JWK; +import org.keycloak.jose.jwk.JWKParser; + +/** + * @author Marek Posolda + */ +public class JWKSUtils { + + public static Map getKeysForUse(JSONWebKeySet keySet, JWK.Use requestedUse) { + Map result = new HashMap<>(); + + for (JWK jwk : keySet.getKeys()) { + JWKParser parser = JWKParser.create(jwk); + if (jwk.getPublicKeyUse().equals(requestedUse.asString()) && parser.isKeyTypeSupported(jwk.getKeyType())) { + result.put(jwk.getKeyId(), parser.toPublicKey()); + } + } + + return result; + } +} diff --git a/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json index a492837299..b5a1bbf797 100644 --- a/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/authz/hello-world-authz-service/src/main/webapp/WEB-INF/keycloak.json @@ -1,6 +1,5 @@ { "realm": "hello-world-authz", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8080/auth", "ssl-required": "external", "resource": "hello-world-authz-service", diff --git a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json index c1dee24574..affafdd82c 100644 --- a/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json +++ b/examples/authz/photoz/photoz-html5-client/src/main/webapp/keycloak.json @@ -1,6 +1,5 @@ { "realm": "photoz", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "resource" : "photoz-html5-client", diff --git a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json index 6849d0771b..9e06730fe3 100644 --- a/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/authz/photoz/photoz-restful-api/src/main/webapp/WEB-INF/keycloak.json @@ -1,6 +1,5 @@ { "realm": "photoz", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8080/auth", "ssl-required": "external", "resource": "photoz-restful-api", diff --git a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json index eaffea8d19..f6b9c90927 100644 --- a/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/authz/servlet-authz/src/main/webapp/WEB-INF/keycloak.json @@ -1,6 +1,5 @@ { "realm": "servlet-authz", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "resource" : "servlet-authz-app", diff --git a/examples/basic-auth/src/main/webapp/WEB-INF/keycloak.json b/examples/basic-auth/src/main/webapp/WEB-INF/keycloak.json index 4502199d70..9da7ed42e5 100644 --- a/examples/basic-auth/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/basic-auth/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "basic-auth", "resource" : "basic-auth-service", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "enable-basic-auth" : "true", diff --git a/examples/broker/facebook-authentication/src/main/webapp/keycloak.json b/examples/broker/facebook-authentication/src/main/webapp/keycloak.json index 55446affdb..17743f663e 100644 --- a/examples/broker/facebook-authentication/src/main/webapp/keycloak.json +++ b/examples/broker/facebook-authentication/src/main/webapp/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "facebook-identity-provider-realm", "resource" : "facebook-authentication", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "public-client" : true diff --git a/examples/broker/google-authentication/src/main/webapp/keycloak.json b/examples/broker/google-authentication/src/main/webapp/keycloak.json index 93c25b4497..f05da2ff5e 100644 --- a/examples/broker/google-authentication/src/main/webapp/keycloak.json +++ b/examples/broker/google-authentication/src/main/webapp/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "google-identity-provider-realm", "resource" : "google-authentication", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "public-client" : true diff --git a/examples/broker/twitter-authentication/src/main/webapp/keycloak.json b/examples/broker/twitter-authentication/src/main/webapp/keycloak.json index 7243636390..2f745149d9 100644 --- a/examples/broker/twitter-authentication/src/main/webapp/keycloak.json +++ b/examples/broker/twitter-authentication/src/main/webapp/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "twitter-identity-provider-realm", "resource" : "twitter-authentication", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "public-client" : true diff --git a/examples/cors/angular-product-app/src/main/webapp/keycloak.json b/examples/cors/angular-product-app/src/main/webapp/keycloak.json index 40b35a1953..d68545945d 100755 --- a/examples/cors/angular-product-app/src/main/webapp/keycloak.json +++ b/examples/cors/angular-product-app/src/main/webapp/keycloak.json @@ -1,6 +1,5 @@ { "realm" : "cors", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost-auth:8080/auth", "ssl-required" : "external", "resource" : "angular-cors-product", diff --git a/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json b/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json index 265049f22c..61da4087a3 100755 --- a/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/cors/database-service/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "cors", "resource" : "cors-database-service", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost-auth:8080/auth", "bearer-only" : true, "ssl-required": "external", diff --git a/examples/demo-template/angular-product-app/src/main/webapp/keycloak.json b/examples/demo-template/angular-product-app/src/main/webapp/keycloak.json index 72ecb5be56..174053e4a1 100755 --- a/examples/demo-template/angular-product-app/src/main/webapp/keycloak.json +++ b/examples/demo-template/angular-product-app/src/main/webapp/keycloak.json @@ -1,6 +1,5 @@ { "realm" : "demo", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "/auth", "ssl-required" : "external", "resource" : "angular-product", diff --git a/examples/demo-template/angular2-product-app/src/main/webapp/keycloak.json b/examples/demo-template/angular2-product-app/src/main/webapp/keycloak.json index ddd1f2ec81..87a2ad64ef 100644 --- a/examples/demo-template/angular2-product-app/src/main/webapp/keycloak.json +++ b/examples/demo-template/angular2-product-app/src/main/webapp/keycloak.json @@ -1,6 +1,5 @@ { "realm": "demo", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required": "external", "resource": "angular2-product", diff --git a/examples/demo-template/customer-app-cli/src/main/resources/META-INF/keycloak.json b/examples/demo-template/customer-app-cli/src/main/resources/META-INF/keycloak.json index 51c8775c99..1c61433813 100644 --- a/examples/demo-template/customer-app-cli/src/main/resources/META-INF/keycloak.json +++ b/examples/demo-template/customer-app-cli/src/main/resources/META-INF/keycloak.json @@ -1,6 +1,5 @@ { "realm" : "demo", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "resource" : "customer-portal-cli", diff --git a/examples/demo-template/customer-app-filter/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/customer-app-filter/src/main/webapp/WEB-INF/keycloak.json index 14e56f543b..3766330ece 100755 --- a/examples/demo-template/customer-app-filter/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/demo-template/customer-app-filter/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "customer-portal-filter", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "expose-token": true, diff --git a/examples/demo-template/customer-app-js/src/main/webapp/keycloak.json b/examples/demo-template/customer-app-js/src/main/webapp/keycloak.json index 224c70b76f..4a60ffacd0 100644 --- a/examples/demo-template/customer-app-js/src/main/webapp/keycloak.json +++ b/examples/demo-template/customer-app-js/src/main/webapp/keycloak.json @@ -1,6 +1,5 @@ { "realm" : "demo", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "/auth", "ssl-required" : "external", "resource" : "customer-portal-js", diff --git a/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json index c2241b3e91..20619097b5 100755 --- a/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/demo-template/customer-app/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "customer-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "expose-token": true, diff --git a/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json index cb938548dc..72e4903bc6 100755 --- a/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/demo-template/database-service/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "demo", "resource" : "database-service", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "bearer-only" : true, "ssl-required" : "external" 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 index dff976cf0e..600dcfa8a8 100644 --- 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 @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "offline-access-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "credentials": { diff --git a/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json index 0a86c041c7..109270101f 100755 --- a/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/demo-template/product-app/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "demo", "resource" : "product-portal", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "/auth", "ssl-required" : "external", "credentials" : { diff --git a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java index e4d6a4033a..72ffd4959b 100644 --- a/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java +++ b/examples/demo-template/service-account/src/main/java/org/keycloak/example/ProductServiceAccountServlet.java @@ -41,6 +41,7 @@ import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import org.keycloak.OAuth2Constants; import org.keycloak.RSATokenVerifier; +import org.keycloak.adapters.rotation.AdapterRSATokenVerifier; import org.keycloak.common.VerificationException; import org.keycloak.adapters.KeycloakDeployment; import org.keycloak.adapters.KeycloakDeploymentBuilder; @@ -163,7 +164,7 @@ public abstract class ProductServiceAccountServlet extends HttpServlet { private void setTokens(HttpServletRequest req, KeycloakDeployment deployment, AccessTokenResponse tokenResponse) throws IOException, VerificationException { String token = tokenResponse.getToken(); String refreshToken = tokenResponse.getRefreshToken(); - AccessToken tokenParsed = RSATokenVerifier.verifyToken(token, deployment.getRealmKey(), deployment.getRealmInfoUrl()); + AccessToken tokenParsed = AdapterRSATokenVerifier.verifyToken(token, deployment); req.getSession().setAttribute(TOKEN, token); req.getSession().setAttribute(REFRESH_TOKEN, refreshToken); req.getSession().setAttribute(TOKEN_PARSED, tokenParsed); diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-secret.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-secret.json index 7eec22a6c3..1a9322d96a 100644 --- a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-secret.json +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-secret.json @@ -1,6 +1,5 @@ { "realm" : "demo", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "resource" : "product-sa-client", diff --git a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json index 3e90c34a30..3c99da7b42 100644 --- a/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json +++ b/examples/demo-template/service-account/src/main/webapp/WEB-INF/keycloak-client-signed-jwt.json @@ -1,6 +1,5 @@ { "realm" : "demo", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "resource" : "product-sa-client-jwt-auth", diff --git a/examples/fuse/camel/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/examples/fuse/camel/src/main/resources/OSGI-INF/blueprint/blueprint.xml index a4796cc152..56550d681a 100644 --- a/examples/fuse/camel/src/main/resources/OSGI-INF/blueprint/blueprint.xml +++ b/examples/fuse/camel/src/main/resources/OSGI-INF/blueprint/blueprint.xml @@ -26,7 +26,6 @@ - diff --git a/examples/fuse/customer-app-fuse/src/main/webapp/WEB-INF/keycloak.json b/examples/fuse/customer-app-fuse/src/main/webapp/WEB-INF/keycloak.json index b5d6b30d24..c7f61f68e3 100755 --- a/examples/fuse/customer-app-fuse/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/fuse/customer-app-fuse/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "customer-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8080/auth", "ssl-required" : "external", "credentials": { diff --git a/examples/fuse/cxf-jaxrs/src/main/resources/WEB-INF/keycloak.json b/examples/fuse/cxf-jaxrs/src/main/resources/WEB-INF/keycloak.json index f5d7e1a989..a8c4b752e1 100644 --- a/examples/fuse/cxf-jaxrs/src/main/resources/WEB-INF/keycloak.json +++ b/examples/fuse/cxf-jaxrs/src/main/resources/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "builtin-cxf-app", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8080/auth", "ssl-required" : "external", "credentials": { diff --git a/examples/fuse/cxf-jaxws/src/main/resources/META-INF/spring/beans.xml b/examples/fuse/cxf-jaxws/src/main/resources/META-INF/spring/beans.xml index 2d15ae017b..8a808c623c 100644 --- a/examples/fuse/cxf-jaxws/src/main/resources/META-INF/spring/beans.xml +++ b/examples/fuse/cxf-jaxws/src/main/resources/META-INF/spring/beans.xml @@ -32,7 +32,6 @@ - diff --git a/examples/fuse/external-config/external-config-keycloak.json b/examples/fuse/external-config/external-config-keycloak.json index 920e99a677..469da82a17 100644 --- a/examples/fuse/external-config/external-config-keycloak.json +++ b/examples/fuse/external-config/external-config-keycloak.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "external-config", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8080/auth", "ssl-required" : "external", "credentials": { diff --git a/examples/fuse/fuse-admin/keycloak-direct-access.json b/examples/fuse/fuse-admin/keycloak-direct-access.json index 8e5ac0ff24..2441134231 100644 --- a/examples/fuse/fuse-admin/keycloak-direct-access.json +++ b/examples/fuse/fuse-admin/keycloak-direct-access.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "ssh-jmx-admin-client", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "ssl-required" : "external", "auth-server-url" : "http://localhost:8080/auth", "credentials": { diff --git a/examples/fuse/product-app-fuse/src/main/resources/WEB-INF/keycloak.json b/examples/fuse/product-app-fuse/src/main/resources/WEB-INF/keycloak.json index 2a52d247f7..e90433ac51 100644 --- a/examples/fuse/product-app-fuse/src/main/resources/WEB-INF/keycloak.json +++ b/examples/fuse/product-app-fuse/src/main/resources/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm": "demo", "resource": "product-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8080/auth", "ssl-required" : "external", "credentials": { diff --git a/examples/js-console/src/main/webapp/keycloak.json b/examples/js-console/src/main/webapp/keycloak.json index c0c04d5aee..cc4bab3394 100644 --- a/examples/js-console/src/main/webapp/keycloak.json +++ b/examples/js-console/src/main/webapp/keycloak.json @@ -1,6 +1,5 @@ { "realm" : "example", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "/auth", "ssl-required" : "external", "resource" : "js-console", diff --git a/examples/kerberos/src/main/webapp/WEB-INF/keycloak.json b/examples/kerberos/src/main/webapp/WEB-INF/keycloak.json index db1223c6b2..7e9d91a7da 100644 --- a/examples/kerberos/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/kerberos/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "kerberos-demo", "resource" : "kerberos-app", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "credentials": { diff --git a/examples/ldap/src/main/webapp/WEB-INF/keycloak.json b/examples/ldap/src/main/webapp/WEB-INF/keycloak.json index 84e1129a88..f43107b054 100644 --- a/examples/ldap/src/main/webapp/WEB-INF/keycloak.json +++ b/examples/ldap/src/main/webapp/WEB-INF/keycloak.json @@ -1,7 +1,6 @@ { "realm" : "ldap-demo", "resource" : "ldap-app", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "/auth", "ssl-required" : "external", "credentials": { diff --git a/examples/multi-tenant/src/main/resources/tenant1-keycloak.json b/examples/multi-tenant/src/main/resources/tenant1-keycloak.json index 57be2774e7..34c17737b3 100644 --- a/examples/multi-tenant/src/main/resources/tenant1-keycloak.json +++ b/examples/multi-tenant/src/main/resources/tenant1-keycloak.json @@ -1,7 +1,6 @@ { "realm" : "tenant1", "resource" : "multi-tenant", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "credentials" : { diff --git a/examples/multi-tenant/src/main/resources/tenant2-keycloak.json b/examples/multi-tenant/src/main/resources/tenant2-keycloak.json index 4f221dc66d..5877082e35 100644 --- a/examples/multi-tenant/src/main/resources/tenant2-keycloak.json +++ b/examples/multi-tenant/src/main/resources/tenant2-keycloak.json @@ -1,7 +1,6 @@ { "realm" : "tenant2", "resource" : "multi-tenant", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDA0oJjgPQJhnVhOo51KauQGfLLreMFu64OJdKXRnfvAQJQTuKNwc5JrR63l/byyW1B6FgclABF818TtLvMCAkn4EuFwQZCZhg3x3+lFGiB/IzC6UAt4Bi0JQrTbdh83/U97GIPegvaDqiqEiQESEkbCZWxM6sh/34hQaAhCaFpMwIDAQAB", "auth-server-url" : "http://localhost:8080/auth", "ssl-required" : "external", "credentials" : { diff --git a/server-spi/src/main/java/org/keycloak/models/Constants.java b/server-spi/src/main/java/org/keycloak/models/Constants.java index 42982f6ace..edc9567e37 100755 --- a/server-spi/src/main/java/org/keycloak/models/Constants.java +++ b/server-spi/src/main/java/org/keycloak/models/Constants.java @@ -51,4 +51,7 @@ public interface Constants { // Prefix for user attributes used in various "context"data maps String USER_ATTRIBUTES_PREFIX = "user.attributes."; + + // Indication to admin-rest-endpoint that realm keys should be re-generated + String GENERATE = "GENERATE"; } diff --git a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index ac285a8fe1..51b1f4e40f 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -820,7 +820,7 @@ public class RepresentationToModel { realm.setUserFederationProviders(providerModels); } - if ("GENERATE".equals(rep.getPublicKey())) { + if (Constants.GENERATE.equals(rep.getPublicKey())) { KeycloakModelUtils.generateRealmKeys(realm); } else { if (rep.getPrivateKey() != null && rep.getPublicKey() != null) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java index 6fda3f0394..79a3919be5 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCClientInstallation.java @@ -51,7 +51,6 @@ public class KeycloakOIDCClientInstallation implements ClientInstallationProvide ClientManager.InstallationAdapterConfig rep = new ClientManager.InstallationAdapterConfig(); rep.setAuthServerUrl(baseUri.toString()); rep.setRealm(realm.getName()); - rep.setRealmKey(realm.getPublicKeyPem()); rep.setSslRequired(realm.getSslRequired().name().toLowerCase()); if (client.isPublicClient() && !client.isBearerOnly()) rep.setPublicClient(true); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java index 33ab67787d..d0bc939eb2 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/installation/KeycloakOIDCJbossSubsystemClientInstallation.java @@ -40,7 +40,6 @@ public class KeycloakOIDCJbossSubsystemClientInstallation implements ClientInsta StringBuffer buffer = new StringBuffer(); buffer.append("\n"); buffer.append(" ").append(realm.getName()).append("\n"); - buffer.append(" ").append(realm.getPublicKeyPem()).append("\n"); buffer.append(" ").append(baseUri.toString()).append("\n"); if (client.isBearerOnly()){ buffer.append(" true\n"); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java index d8f7fe72de..62d2c58039 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/JWKSUtils.java @@ -30,6 +30,8 @@ import org.keycloak.models.KeycloakSession; import org.keycloak.util.JsonSerialization; /** + * TODO: Merge with JWKSUtils from keycloak-core? + * * @author Marek Posolda */ public class JWKSUtils { @@ -44,7 +46,7 @@ public class JWKSUtils { public static PublicKey getKeyForUse(JSONWebKeySet keySet, JWK.Use requestedUse) { for (JWK jwk : keySet.getKeys()) { JWKParser parser = JWKParser.create(jwk); - if (parser.getJwk().getPublicKeyUse().equals(requestedUse.asString()) && parser.isAlgorithmSupported(jwk.getKeyType())) { + if (parser.getJwk().getPublicKeyUse().equals(requestedUse.asString()) && parser.isKeyTypeSupported(jwk.getKeyType())) { return parser.toPublicKey(); } } diff --git a/services/src/main/java/org/keycloak/services/managers/ClientManager.java b/services/src/main/java/org/keycloak/services/managers/ClientManager.java index 7bdf5b1889..3de62db240 100644 --- a/services/src/main/java/org/keycloak/services/managers/ClientManager.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientManager.java @@ -265,7 +265,6 @@ public class ClientManager { InstallationAdapterConfig rep = new InstallationAdapterConfig(); rep.setAuthServerUrl(baseUri.toString()); rep.setRealm(realmModel.getName()); - rep.setRealmKey(realmModel.getPublicKeyPem()); rep.setSslRequired(realmModel.getSslRequired().name().toLowerCase()); if (clientModel.isPublicClient() && !clientModel.isBearerOnly()) rep.setPublicClient(true); @@ -286,7 +285,6 @@ public class ClientManager { StringBuffer buffer = new StringBuffer(); buffer.append("\n"); buffer.append(" ").append(realmModel.getName()).append("\n"); - buffer.append(" ").append(realmModel.getPublicKeyPem()).append("\n"); buffer.append(" ").append(baseUri.toString()).append("\n"); if (clientModel.isBearerOnly()){ buffer.append(" true\n"); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 7d4922340d..1caa5fb207 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -35,6 +35,7 @@ import org.keycloak.events.admin.ResourceType; import org.keycloak.exportimport.ClientDescriptionConverter; import org.keycloak.exportimport.ClientDescriptionConverterFactory; import org.keycloak.models.ClientModel; +import org.keycloak.models.Constants; import org.keycloak.models.GroupModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.ModelDuplicateException; @@ -281,7 +282,7 @@ public class RealmAdminResource { logger.debug("updating realm: " + realm.getName()); try { - if (!"GENERATE".equals(rep.getPublicKey()) && (rep.getPrivateKey() != null && rep.getPublicKey() != null)) { + if (!Constants.GENERATE.equals(rep.getPublicKey()) && (rep.getPrivateKey() != null && rep.getPublicKey() != null)) { try { KeyPairVerifier.verify(rep.getPrivateKey(), rep.getPublicKey()); } catch (VerificationException e) { @@ -289,7 +290,7 @@ public class RealmAdminResource { } } - if (!"GENERATE".equals(rep.getPublicKey()) && (rep.getCertificate() != null)) { + if (!Constants.GENERATE.equals(rep.getPublicKey()) && (rep.getCertificate() != null)) { try { X509Certificate cert = PemUtils.decodeCertificate(rep.getCertificate()); if (cert == null) { diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java index c282dff966..aad5bb889d 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/filter/AdapterActionsFilter.java @@ -27,8 +27,13 @@ import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.jboss.logging.Logger; +import org.keycloak.adapters.AdapterDeploymentContext; +import org.keycloak.adapters.KeycloakDeployment; +import org.keycloak.adapters.rotation.JWKPublicKeyLocator; import org.keycloak.common.util.Time; /** @@ -38,6 +43,11 @@ import org.keycloak.common.util.Time; */ public class AdapterActionsFilter implements Filter { + public static final String TIME_OFFSET_PARAM = "timeOffset"; + public static final String RESET_PUBLIC_KEY_PARAM = "resetPublicKey"; + + private static final Logger log = Logger.getLogger(AdapterActionsFilter.class); + @Override public void init(FilterConfig filterConfig) throws ServletException { @@ -45,16 +55,28 @@ public class AdapterActionsFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + HttpServletRequest servletReq = (HttpServletRequest) request; HttpServletResponse servletResp = (HttpServletResponse) response; //Accept timeOffset as argument to enforce timeouts - String timeOffsetParam = request.getParameter("timeOffset"); - if (timeOffsetParam != null && !timeOffsetParam.isEmpty()) { - Time.setOffset(Integer.parseInt(timeOffsetParam)); - } + String timeOffsetParam = request.getParameter(TIME_OFFSET_PARAM); + String resetPublicKey = request.getParameter(RESET_PUBLIC_KEY_PARAM); - // Continue request - chain.doFilter(request, response); + if (timeOffsetParam != null && !timeOffsetParam.isEmpty()) { + int timeOffset = Integer.parseInt(timeOffsetParam); + log.infof("Time offset updated to %d for application %s", timeOffset, servletReq.getRequestURI()); + Time.setOffset(timeOffset); + writeResponse(servletResp, "Offset set successfully"); + } else if (resetPublicKey != null && !resetPublicKey.isEmpty()) { + AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext) request.getServletContext().getAttribute(AdapterDeploymentContext.class.getName()); + KeycloakDeployment deployment = deploymentContext.resolveDeployment(null); + deployment.setPublicKeyLocator(new JWKPublicKeyLocator()); + log.infof("Restarted publicKey locator for application %s", servletReq.getRequestURI()); + writeResponse(servletResp, "PublicKeyLocator restarted successfully"); + } else { + // Continue request + chain.doFilter(request, response); + } } @@ -64,8 +86,9 @@ public class AdapterActionsFilter implements Filter { } private void writeResponse(HttpServletResponse response, String responseText) throws IOException { + response.setContentType("text/html"); PrintWriter writer = response.getWriter(); - writer.println(responseText); + writer.println("" + responseText + ""); writer.flush(); } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java index d370dd049b..6cb1f37d5b 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/adapter/page/AbstractShowTokensPage.java @@ -23,6 +23,7 @@ import org.keycloak.representations.AccessToken; import org.keycloak.representations.RefreshToken; import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl; import org.keycloak.util.JsonSerialization; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.WebElement; import org.openqa.selenium.support.FindBy; @@ -43,6 +44,8 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl return JsonSerialization.readValue(accessToken.getText(), AccessToken.class); } catch (IOException e) { e.printStackTrace(); + } catch (NoSuchElementException nsee) { + log.warn("No accessToken element found on the page"); } return null; @@ -53,7 +56,10 @@ public abstract class AbstractShowTokensPage extends AbstractPageWithInjectedUrl return JsonSerialization.readValue(refreshToken.getText(), RefreshToken.class); } catch (IOException e) { e.printStackTrace(); + } catch (NoSuchElementException nsee) { + log.warn("No idToken element found on the page"); } + return null; } } diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java index 9cd7625cc1..4af2988d1b 100644 --- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java +++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/arquillian/DeploymentArchiveProcessor.java @@ -49,8 +49,6 @@ import static org.keycloak.testsuite.util.IOUtil.*; */ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { - public static final String REALM_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"; - protected final Logger log = org.jboss.logging.Logger.getLogger(this.getClass()); private final boolean authServerSslRequired = Boolean.parseBoolean(System.getProperty("auth.server.ssl.required")); @@ -129,7 +127,6 @@ public class DeploymentArchiveProcessor implements ApplicationArchiveProcessor { // ac.setRealmKey(null); // TODO verify if realm key is required for relative scneario } else { adapterConfig.setAuthServerUrl(getAuthServerContextRoot() + "/auth"); - adapterConfig.setRealmKey(REALM_KEY); } if ("true".equals(System.getProperty("app.server.ssl.required"))) { diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java index 127c863fa8..a40275f727 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/AbstractServletsAdapterTest.java @@ -22,6 +22,7 @@ import org.jboss.shrinkwrap.api.ShrinkWrap; import org.jboss.shrinkwrap.api.asset.StringAsset; import org.jboss.shrinkwrap.api.spec.WebArchive; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter; import org.keycloak.testsuite.util.WaitUtils; import org.openqa.selenium.By; @@ -113,7 +114,7 @@ public abstract class AbstractServletsAdapterTest extends AbstractAdapterTest { protected void setAdapterAndServerTimeOffset(int timeOffset, String servletUri) { setTimeOffset(timeOffset); String timeOffsetUri = UriBuilder.fromUri(servletUri) - .queryParam("timeOffset", timeOffset) + .queryParam(AdapterActionsFilter.TIME_OFFSET_PARAM, timeOffset) .build().toString(); driver.navigate().to(timeOffsetUri); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java index 083867f514..02adb84429 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractDemoServletsAdapterTest.java @@ -28,6 +28,7 @@ import org.keycloak.OAuth2Constants; import org.keycloak.common.Version; import org.keycloak.common.util.Time; import org.keycloak.constants.AdapterConstants; +import org.keycloak.models.Constants; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.representations.AccessToken; @@ -36,8 +37,11 @@ import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest; import org.keycloak.testsuite.adapter.filter.AdapterActionsFilter; import org.keycloak.testsuite.adapter.page.*; +import org.keycloak.testsuite.util.URLAssert; import org.keycloak.testsuite.util.URLUtils; +import org.keycloak.testsuite.util.WaitUtils; import org.keycloak.util.BasicAuthHelper; +import org.openqa.selenium.By; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; @@ -46,6 +50,8 @@ import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Form; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + import java.net.URI; import java.util.List; import java.util.Map; @@ -166,6 +172,59 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd client.close(); } + @Test + public void testRealmKeyRotationWithNewKeyDownload() throws Exception { + // Login success first + tokenMinTTLPage.navigateTo(); + testRealmLoginPage.form().waitForUsernameInputPresent(); + assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); + testRealmLoginPage.form().login("bburke@redhat.com", "password"); + assertCurrentUrlEquals(tokenMinTTLPage); + + AccessToken token = tokenMinTTLPage.getAccessToken(); + Assert.assertEquals("bburke@redhat.com", token.getPreferredUsername()); + + // Logout + String logoutUri = OIDCLoginProtocolService.logoutUrl(authServerPage.createUriBuilder()) + .queryParam(OAuth2Constants.REDIRECT_URI, tokenMinTTLPage.toString()) + .build("demo").toString(); + driver.navigate().to(logoutUri); + + // Generate new realm key + RealmRepresentation realmRep = testRealmResource().toRepresentation(); + String oldPublicKey = realmRep.getPublicKey(); + String oldPrivateKey = realmRep.getPrivateKey(); + realmRep.setPublicKey(Constants.GENERATE); + testRealmResource().update(realmRep); + + // Try to login again. It should fail now + tokenMinTTLPage.navigateTo(); + testRealmLoginPage.form().waitForUsernameInputPresent(); + assertCurrentUrlStartsWithLoginUrlOf(testRealmPage); + testRealmLoginPage.form().login("bburke@redhat.com", "password"); + URLAssert.assertCurrentUrlStartsWith(driver, tokenMinTTLPage.getInjectedUrl().toString()); + assertNull(tokenMinTTLPage.getAccessToken()); + + String adapterActionsUrl = tokenMinTTLPage.toString() + "/unsecured/foo"; + setAdapterAndServerTimeOffset(300, adapterActionsUrl); + + // Try to login. Should work now due to realm key change + tokenMinTTLPage.navigateTo(); + assertCurrentUrlEquals(tokenMinTTLPage); + token = tokenMinTTLPage.getAccessToken(); + Assert.assertEquals("bburke@redhat.com", token.getPreferredUsername()); + driver.navigate().to(logoutUri); + + // Revert public keys change + String timeOffsetUri = UriBuilder.fromUri(adapterActionsUrl) + .queryParam(AdapterActionsFilter.RESET_PUBLIC_KEY_PARAM, "true") + .build().toString(); + driver.navigate().to(timeOffsetUri); + WaitUtils.waitUntilElement(By.tagName("body")).is().visible(); + + setAdapterAndServerTimeOffset(0, adapterActionsUrl); + } + @Test public void testLoginSSOAndLogout() { // test login to customer-portal which does a bearer request to customer-db @@ -444,6 +503,7 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd // Sets 5 minutes offset and assert access token will be still the same setAdapterAndServerTimeOffset(300, tokenMinTTLPage.toString()); + tokenMinTTLPage.navigateTo(); token = tokenMinTTLPage.getAccessToken(); int tokenIssued2 = token.getIssuedAt(); Assert.assertEquals(tokenIssued1, tokenIssued2); @@ -451,6 +511,7 @@ public abstract class AbstractDemoServletsAdapterTest extends AbstractServletsAd // Sets 9 minutes offset and assert access token will be refreshed (accessTokenTimeout is 10 minutes, token-min-ttl is 2 minutes. Hence 8 minutes or more should be sufficient) setAdapterAndServerTimeOffset(540, tokenMinTTLPage.toString()); + tokenMinTTLPage.navigateTo(); token = tokenMinTTLPage.getAccessToken(); int tokenIssued3 = token.getIssuedAt(); Assert.assertTrue(tokenIssued3 > tokenIssued1); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java index fa6d0a8aed..b8c10b6dac 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/servlet/AbstractOfflineServletsAdapterTest.java @@ -85,7 +85,7 @@ public abstract class AbstractOfflineServletsAdapterTest extends AbstractServlet String refreshTokenId = offlineTokenPage.getRefreshToken().getId(); setAdapterAndServerTimeOffset(9999); - + offlineTokenPage.navigateTo(); assertCurrentUrlStartsWith(offlineTokenPage); Assert.assertNotEquals(offlineTokenPage.getRefreshToken().getId(), refreshTokenId); Assert.assertNotEquals(offlineTokenPage.getAccessToken().getId(), accessTokenId); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java index fb03891e15..c7339ed12d 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/client/InstallationTest.java @@ -21,11 +21,11 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.keycloak.admin.client.resource.ClientResource; -import org.keycloak.events.admin.OperationType; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.testsuite.arquillian.AuthServerTestEnricher; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.assertFalse; /** * Test getting the installation/configuration files for OIDC and SAML. @@ -81,7 +81,7 @@ public class InstallationTest extends AbstractClientTest { private void assertOidcInstallationConfig(String config) { RealmRepresentation realmRep = realmRep(); assertTrue(config.contains(realmRep.getId())); - assertTrue(config.contains(realmRep.getPublicKey())); + assertFalse(config.contains(realmRep.getPublicKey())); assertTrue(config.contains(authServerUrl())); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java index 0a4fe4bea7..818a84a992 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/client/AdapterInstallationConfigTest.java @@ -35,14 +35,11 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes private ClientRepresentation client; private ClientRepresentation client2; private ClientRepresentation clientPublic; - private String publicKey; @Before public void before() throws Exception { super.before(); - publicKey = adminClient.realm(REALM_NAME).toRepresentation().getPublicKey(); - client = new ClientRepresentation(); client.setEnabled(true); client.setClientId("RegistrationAccessTokenTest"); @@ -92,7 +89,6 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes assertEquals(1, config.getCredentials().size()); assertEquals(client.getSecret(), config.getCredentials().get("secret")); - assertEquals(publicKey, config.getRealmKey()); assertEquals(client.getClientId(), config.getResource()); assertEquals(SslRequired.EXTERNAL.name().toLowerCase(), config.getSslRequired()); } @@ -132,7 +128,6 @@ public class AdapterInstallationConfigTest extends AbstractClientRegistrationTes assertEquals(0, config.getCredentials().size()); - assertEquals(publicKey, config.getRealmKey()); assertEquals(clientPublic.getClientId(), config.getResource()); assertEquals(SslRequired.EXTERNAL.name().toLowerCase(), config.getSslRequired()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json index b75c1d5e65..32703f9a12 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/customer-portal/WEB-INF/keycloak.json @@ -1,10 +1,10 @@ { "realm": "demo", "resource": "customer-portal", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", - "auth-server-url": "http://localhost:8180/auth", + "auth-server-url": "http://localhostt:8180/auth", "ssl-required" : "external", "expose-token": true, + "min-time-between-jwks-requests": 120, "credentials": { "secret": "password" } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/input-portal/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/input-portal/WEB-INF/keycloak.json index a934e97ec2..eecc24bac9 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/input-portal/WEB-INF/keycloak.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/input-portal/WEB-INF/keycloak.json @@ -1,9 +1,9 @@ { "realm" : "demo", "resource" : "input-portal", - "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url" : "http://${my.host.name}:8180/auth", "ssl-required" : "external", + "min-time-between-jwks-requests": 120, "credentials" : { "secret": "password" } diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/keycloak.json b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/keycloak.json index 85eb9a72ab..b86e018279 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/keycloak.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/keycloak.json @@ -1,11 +1,11 @@ { "realm": "demo", "resource": "token-min-ttl", - "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8180/auth", "ssl-required" : "external", "credentials": { "secret": "password" }, - "token-minimum-time-to-live": 120 + "token-minimum-time-to-live": 120, + "min-time-between-jwks-requests": 120 } \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/web.xml b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/web.xml index f8110bc3cc..86d07f5ee6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/web.xml +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/adapter-test/token-min-ttl/WEB-INF/web.xml @@ -68,6 +68,12 @@ /error.html + + + Unsecured + /unsecured/* + + KEYCLOAK