Merge pull request #3228 from mposolda/master
KEYCLOAK-905 Realm keys rotation support on adapters
This commit is contained in:
commit
a88e975a3a
78 changed files with 668 additions and 152 deletions
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class HttpAdapterUtils {
|
||||
|
||||
|
||||
public static <T> T sendJsonHttpRequest(KeycloakDeployment deployment, HttpRequestBase httpRequest, Class<T> 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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class HttpClientAdapterException extends Exception {
|
||||
|
||||
public HttpClientAdapterException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpClientAdapterException(String message, Throwable t) {
|
||||
super(message, t);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class JWKPublicKeyLocator implements PublicKeyLocator {
|
||||
|
||||
private static final Logger log = Logger.getLogger(JWKPublicKeyLocator.class);
|
||||
|
||||
private Map<String, PublicKey> 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<String, PublicKey> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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);
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
45
core/src/main/java/org/keycloak/util/JWKSUtils.java
Normal file
45
core/src/main/java/org/keycloak/util/JWKSUtils.java
Normal file
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class JWKSUtils {
|
||||
|
||||
public static Map<String, PublicKey> getKeysForUse(JSONWebKeySet keySet, JWK.Use requestedUse) {
|
||||
Map<String, PublicKey> 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;
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"realm" : "demo",
|
||||
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url" : "/auth",
|
||||
"ssl-required" : "external",
|
||||
"resource" : "angular-product",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"realm": "demo",
|
||||
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url": "/auth",
|
||||
"ssl-required": "external",
|
||||
"resource": "angular2-product",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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" : {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -26,7 +26,6 @@
|
|||
<bean id="kcAdapterConfig" class="org.keycloak.representations.adapters.config.AdapterConfig">
|
||||
<property name="realm" value="demo"/>
|
||||
<property name="resource" value="admin-camel-endpoint"/>
|
||||
<property name="realmKey" value="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"/>
|
||||
<property name="bearerOnly" value="true"/>
|
||||
<property name="authServerUrl" value="http://localhost:8080/auth" />
|
||||
<property name="sslRequired" value="EXTERNAL"/>
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
<bean id="kcAdapterConfig" class="org.keycloak.representations.adapters.config.AdapterConfig">
|
||||
<property name="realm" value="demo"/>
|
||||
<property name="resource" value="custom-cxf-endpoint"/>
|
||||
<property name="realmKey" value="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB"/>
|
||||
<property name="bearerOnly" value="true"/>
|
||||
<property name="authServerUrl" value="http://localhost:8080/auth" />
|
||||
<property name="sslRequired" value="EXTERNAL"/>
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
{
|
||||
"realm" : "example",
|
||||
"realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
|
||||
"auth-server-url" : "/auth",
|
||||
"ssl-required" : "external",
|
||||
"resource" : "js-console",
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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" : {
|
||||
|
|
|
@ -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" : {
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -40,7 +40,6 @@ public class KeycloakOIDCJbossSubsystemClientInstallation implements ClientInsta
|
|||
StringBuffer buffer = new StringBuffer();
|
||||
buffer.append("<secure-deployment name=\"WAR MODULE NAME.war\">\n");
|
||||
buffer.append(" <realm>").append(realm.getName()).append("</realm>\n");
|
||||
buffer.append(" <realm-public-key>").append(realm.getPublicKeyPem()).append("</realm-public-key>\n");
|
||||
buffer.append(" <auth-server-url>").append(baseUri.toString()).append("</auth-server-url>\n");
|
||||
if (client.isBearerOnly()){
|
||||
buffer.append(" <bearer-only>true</bearer-only>\n");
|
||||
|
|
|
@ -30,6 +30,8 @@ import org.keycloak.models.KeycloakSession;
|
|||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
/**
|
||||
* TODO: Merge with JWKSUtils from keycloak-core?
|
||||
*
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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("<secure-deployment name=\"WAR MODULE NAME.war\">\n");
|
||||
buffer.append(" <realm>").append(realmModel.getName()).append("</realm>\n");
|
||||
buffer.append(" <realm-public-key>").append(realmModel.getPublicKeyPem()).append("</realm-public-key>\n");
|
||||
buffer.append(" <auth-server-url>").append(baseUri.toString()).append("</auth-server-url>\n");
|
||||
if (clientModel.isBearerOnly()){
|
||||
buffer.append(" <bearer-only>true</bearer-only>\n");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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("<html><body>" + responseText + "</body></html>");
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"))) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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()));
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -68,6 +68,12 @@
|
|||
<url-pattern>/error.html</url-pattern>
|
||||
</web-resource-collection>
|
||||
</security-constraint>
|
||||
<security-constraint>
|
||||
<web-resource-collection>
|
||||
<web-resource-name>Unsecured</web-resource-name>
|
||||
<url-pattern>/unsecured/*</url-pattern>
|
||||
</web-resource-collection>
|
||||
</security-constraint>
|
||||
|
||||
<login-config>
|
||||
<auth-method>KEYCLOAK</auth-method>
|
||||
|
|
Loading…
Reference in a new issue