keycloak-uncached
Changes
broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java 14(+1 -13)
Details
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
index 152823f..47847dc 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProvider.java
@@ -4,17 +4,20 @@ import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.PemUtils;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import java.io.IOException;
+import java.security.PublicKey;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@@ -40,10 +43,12 @@ public class KeycloakOIDCIdentityProvider extends OIDCIdentityProvider {
@Path(AdapterConstants.K_LOGOUT)
public Response backchannelLogout(String input) {
JWSInput token = new JWSInput(input);
- String signingCert = getConfig().getSigningCertificate();
- if (signingCert != null && !signingCert.trim().equals("")) {
- if (!token.verify(getConfig().getSigningCertificate())) {
- return Response.status(400).build(); }
+ PublicKey key = getExternalIdpKey();
+ if (key != null) {
+ if (!verify(token, key)) {
+ logger.warn("Failed to verify logout request");
+ return Response.status(400).build();
+ }
}
LogoutAction action = null;
try {
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
index 9c46cd8..6a47135 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/KeycloakOIDCIdentityProviderFactory.java
@@ -50,19 +50,7 @@ public class KeycloakOIDCIdentityProviderFactory extends AbstractIdentityProvide
@Override
public Map<String, String> parseConfig(InputStream inputStream) {
- OIDCConfigurationRepresentation rep = null;
- try {
- rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
- } catch (IOException e) {
- throw new RuntimeException("failed to load openid connect metadata", e);
- }
- OIDCIdentityProviderConfig config = new OIDCIdentityProviderConfig(new IdentityProviderModel());
- config.setIssuer(rep.getIssuer());
- config.setLogoutUrl(rep.getLogoutEndpoint());
- config.setAuthorizationUrl(rep.getAuthorizationEndpoint());
- config.setTokenUrl(rep.getTokenEndpoint());
- config.setUserInfoUrl(rep.getUserinfoEndpoint());
- return config.getConfig();
+ return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream);
}
}
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
index b4058b1..a1ea53b 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java
@@ -19,6 +19,7 @@ package org.keycloak.broker.oidc;
import org.codehaus.jackson.JsonNode;
import org.jboss.logging.Logger;
+import org.keycloak.RSATokenVerifier;
import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.FederatedIdentity;
@@ -28,6 +29,7 @@ import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventGroup;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse;
@@ -38,6 +40,7 @@ import org.keycloak.services.resources.IdentityBrokerService;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.flows.Flows;
import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.PemUtils;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
@@ -47,6 +50,7 @@ import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
+import java.security.PublicKey;
import java.util.Map;
/**
@@ -74,6 +78,28 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
return new OIDCEndpoint(callback, realm, event);
}
+ protected PublicKey getExternalIdpKey() {
+ String signingCert = getConfig().getCertificateSignatureVerifier();
+ try {
+ if (signingCert != null && !signingCert.trim().equals("")) {
+ return PemUtils.decodeCertificate(signingCert).getPublicKey();
+ } else if (getConfig().getPublicKeySignatureVerifier() != null && !getConfig().getPublicKeySignatureVerifier().trim().equals("")) {
+ return PemUtils.decodePublicKey(getConfig().getPublicKeySignatureVerifier());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ return null;
+
+ }
+
+ protected boolean verify(JWSInput jws, PublicKey key) {
+ if (key == null) return true;
+ if (!getConfig().isValidateSignature()) return true;
+ return RSAProvider.verify(jws, key);
+
+ }
+
protected class OIDCEndpoint extends Endpoint {
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
super(callback, realm, event);
@@ -140,11 +166,8 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
} catch (IOException e) {
throw new IdentityBrokerException("Could not decode access token response.", e);
}
- String accessToken = tokenResponse.getToken();
-
- if (accessToken == null) {
- throw new IdentityBrokerException("No access_token from server.");
- }
+ PublicKey key = getExternalIdpKey();
+ String accessToken = verifyAccessToken(key, tokenResponse);
String encodedIdToken = tokenResponse.getIdToken();
@@ -154,7 +177,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
notes.put(FEDERATED_TOKEN_EXPIRATION, Long.toString(tokenResponse.getExpiresIn()));
- IDToken idToken = validateIdToken(encodedIdToken);
+ IDToken idToken = validateIdToken(key, encodedIdToken);
try {
String id = idToken.getSubject();
@@ -204,19 +227,32 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
}
- private IDToken validateIdToken(String encodedToken) {
+ private String verifyAccessToken(PublicKey key, AccessTokenResponse tokenResponse) {
+ String accessToken = tokenResponse.getToken();
+
+ if (accessToken == null) {
+ throw new IdentityBrokerException("No access_token from server.");
+ }
+ return accessToken;
+ }
+
+ private IDToken validateIdToken(PublicKey key, String encodedToken) {
if (encodedToken == null) {
throw new IdentityBrokerException("No id_token from server.");
}
try {
- IDToken idToken = new JWSInput(encodedToken).readJsonContent(IDToken.class);
+ JWSInput jws = new JWSInput(encodedToken);
+ if (!verify(jws, key)) {
+ throw new IdentityBrokerException("IDToken signature validation failed");
+ }
+ IDToken idToken = jws.readJsonContent(IDToken.class);
String aud = idToken.getAudience();
String iss = idToken.getIssuer();
if (aud != null && !aud.equals(getConfig().getClientId())) {
- throw new RuntimeException("Wrong audience from id_token..");
+ throw new IdentityBrokerException("Wrong audience from id_token..");
}
String trustedIssuers = getConfig().getIssuer();
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
index 90707a7..ce89bfa 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderConfig.java
@@ -47,13 +47,29 @@ public class OIDCIdentityProviderConfig extends OAuth2IdentityProviderConfig {
public void setLogoutUrl(String url) {
getConfig().put("logoutUrl", url);
}
- public String getSigningCertificate() {
- return getConfig().get("signingCertificate");
+ public String getCertificateSignatureVerifier() {
+ return getConfig().get("certificateSignatureVerifier");
}
- public void setSigningCertificate(String signingCertificate) {
- getConfig().put("signingCertificate", signingCertificate);
+ public void setCertificateSignatureVerifier(String signingCertificate) {
+ getConfig().put("certificateSignatureVerifier", signingCertificate);
}
+ public String getPublicKeySignatureVerifier() {
+ return getConfig().get("publicKeySignatureVerifier");
+ }
+
+ public void setPublicKeySignatureVerifier(String signingCertificate) {
+ getConfig().put("publicKeySignatureVerifier", signingCertificate);
+ }
+
+ public boolean isValidateSignature() {
+ return Boolean.valueOf(getConfig().get("validateSignature"));
+ }
+
+ public void setValidateSignature(boolean validateSignature) {
+ getConfig().put("validateSignature", String.valueOf(validateSignature));
+ }
+
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
index 89ac5d4..4610140 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProviderFactory.java
@@ -17,13 +17,21 @@
*/
package org.keycloak.broker.oidc;
+import org.codehaus.jackson.annotate.JsonProperty;
+import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.jose.jwk.JWK;
+import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.protocol.oidc.representations.JSONWebKeySet;
import org.keycloak.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.util.JsonSerialization;
+import org.keycloak.util.PemUtils;
import java.io.IOException;
import java.io.InputStream;
+import java.security.PublicKey;
import java.util.Map;
/**
@@ -50,6 +58,10 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
@Override
public Map<String, String> parseConfig(InputStream inputStream) {
+ return parseOIDCConfig(inputStream);
+ }
+
+ protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
OIDCConfigurationRepresentation rep = null;
try {
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
@@ -62,7 +74,27 @@ public class OIDCIdentityProviderFactory extends AbstractIdentityProviderFactory
config.setAuthorizationUrl(rep.getAuthorizationEndpoint());
config.setTokenUrl(rep.getTokenEndpoint());
config.setUserInfoUrl(rep.getUserinfoEndpoint());
- return config.getConfig();
+ if (rep.getJwksUri() != null) {
+ String uri = rep.getJwksUri();
+ String keySetString = null;
+ try {
+ keySetString = SimpleHttp.doGet(uri).asString();
+ JSONWebKeySet keySet = JsonSerialization.readValue(keySetString, JSONWebKeySet.class);
+ for (JWK jwk : keySet.getKeys()) {
+ JWKParser parse = JWKParser.create(jwk);
+ if (parse.getJwk().getPublicKeyUse().equals(JWK.SIG_USE)) {
+ PublicKey key = parse.toPublicKey();
+ config.setPublicKeySignatureVerifier(KeycloakModelUtils.getPemFromKey(key));
+ config.setValidateSignature(true);
+ break;
+ }
+ }
+ } catch (IOException e) {
+ throw new RuntimeException("F ailed to query JWKSet from: " + uri, e);
+ }
+
+ }
+ return config.getConfig();
}
}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWK.java b/core/src/main/java/org/keycloak/jose/jwk/JWK.java
old mode 100644
new mode 100755
index d292f41..6b2cd9c
--- a/core/src/main/java/org/keycloak/jose/jwk/JWK.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWK.java
@@ -1,7 +1,12 @@
package org.keycloak.jose.jwk;
+import org.codehaus.jackson.annotate.JsonAnyGetter;
+import org.codehaus.jackson.annotate.JsonAnySetter;
import org.codehaus.jackson.annotate.JsonProperty;
+import java.util.HashMap;
+import java.util.Map;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -15,6 +20,9 @@ public class JWK {
public static final String PUBLIC_KEY_USE = "use";
+ public static final String SIG_USE = "sig";
+ public static final String ENCRYPTION_USE = "enc";
+
@JsonProperty(KEY_ID)
private String keyId;
@@ -27,6 +35,9 @@ public class JWK {
@JsonProperty(PUBLIC_KEY_USE)
private String publicKeyUse;
+ protected Map<String, Object> otherClaims = new HashMap<String, Object>();
+
+
public String getKeyId() {
return keyId;
}
@@ -59,4 +70,15 @@ public class JWK {
this.publicKeyUse = publicKeyUse;
}
+ @JsonAnyGetter
+ public Map<String, Object> getOtherClaims() {
+ return otherClaims;
+ }
+
+ @JsonAnySetter
+ public void setOtherClaims(String name, Object value) {
+ otherClaims.put(name, value);
+ }
+
+
}
diff --git a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
old mode 100644
new mode 100755
index 38f02d8..b498cc1
--- a/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
+++ b/core/src/main/java/org/keycloak/jose/jwk/JWKParser.java
@@ -17,29 +17,41 @@ public class JWKParser {
private static TypeReference<Map<String,String>> typeRef = new TypeReference<Map<String,String>>() {};
- private Map<String, String> values;
+ private JWK jwk;
private JWKParser() {
}
+ public JWKParser(JWK jwk) {
+ this.jwk = jwk;
+ }
+
public static JWKParser create() {
return new JWKParser();
}
+ public static JWKParser create(JWK jwk) {
+ return new JWKParser(jwk);
+ }
+
public JWKParser parse(String jwk) {
try {
- this.values = JsonSerialization.mapper.readValue(jwk, typeRef);
+ this.jwk = JsonSerialization.mapper.readValue(jwk, JWK.class);
return this;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
+ public JWK getJwk() {
+ return jwk;
+ }
+
public PublicKey toPublicKey() {
- String algorithm = values.get(JWK.KEY_TYPE);
+ String algorithm = jwk.getKeyType();
if (RSAPublicJWK.RSA.equals(algorithm)) {
- BigInteger modulus = new BigInteger(1, Base64Url.decode(values.get(RSAPublicJWK.MODULUS)));
- BigInteger publicExponent = new BigInteger(1, Base64Url.decode(values.get(RSAPublicJWK.PUBLIC_EXPONENT)));
+ 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()));
try {
return KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
index b2a9091..6019f2c 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-oidc.html
@@ -132,6 +132,20 @@
</div>
<span tooltip-placement="right" tooltip="Specifies whether the Authorization Server prompts the End-User for reauthentication and consent." class="fa fa-info-circle"></span>
</div>
+ <div class="form-group">
+ <label class="col-sm-2 control-label" for="validateSignature">Validate Signatures</label>
+ <div class="col-sm-4">
+ <input ng-model="identityProvider.config.validateSignature" id="validateSignature" value="'true'" onoffswitchvalue />
+ </div>
+ <span tooltip-placement="right" tooltip="Enable/disable signature validation of external IDP signatures." class="fa fa-info-circle"></span>
+ </div>
+ <div class="form-group clearfix" data-ng-show="identityProvider.config.validateSignature == 'true'">
+ <label class="col-sm-2 control-label" for="publicKeySignatureVerifier">Validating Public Key</label>
+ <div class="col-sm-4">
+ <textarea class="form-control" id="publicKeySignatureVerifier" ng-model="identityProvider.config.publicKeySignatureVerifier"/>
+ </div>
+ <span tooltip-placement="right" tooltip="The public key in PEM format that must be used to verify external IDP signatures." class="fa fa-info-circle"></span>
+ </div>
</fieldset>
<fieldset data-ng-show="newIdentityProvider">
<legend uncollapsed><span class="text">Import External IDP Config</span> <span tooltip-placement="right" tooltip="Allows you to load external IDP metadata from a config file or to download it from a URL." class="fa fa-info-circle"></span></legend>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
index ed67b7c..6e483f2 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-saml.html
@@ -83,12 +83,19 @@
</div>
<span tooltip-placement="right" tooltip="Specifies the URI reference corresponding to a name identifier format. Defaults to urn:oasis:names:tc:SAML:2.0:nameid-format:persistent." class="fa fa-info-circle"></span>
</div>
- <div class="form-group clearfix">
- <label class="col-sm-2 control-label" for="signingCertificate">Validating X509 Certificate</label>
+ <div class="form-group">
+ <label class="col-sm-2 control-label" for="postBindingResponse">HTTP-POST Binding Response</label>
<div class="col-sm-4">
- <textarea class="form-control" id="signingCertificate" ng-model="identityProvider.config.signingCertificate"/>
+ <input ng-model="identityProvider.config.postBindingResponse" id="postBindingResponse" value="'true'" onoffswitchvalue />
</div>
- <span tooltip-placement="right" tooltip="The certificate in PEM format that must be used to check for signatures." class="fa fa-info-circle"></span>
+ <span tooltip-placement="right" tooltip="Indicates whether to respond to requests using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used." class="fa fa-info-circle"></span>
+ </div>
+ <div class="form-group">
+ <label class="col-sm-2 control-label" for="postBindingAuthnRequest">HTTP-POST Binding for AuthnRequest</label>
+ <div class="col-sm-4">
+ <input ng-model="identityProvider.config.postBindingAuthnRequest" id="postBindingAuthnRequest" value="'true'" onoffswitchvalue />
+ </div>
+ <span tooltip-placement="right" tooltip="Indicates whether the AuthnRequest must be sent using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used." class="fa fa-info-circle"></span>
</div>
<div class="form-group">
<label class="col-sm-2 control-label" for="wantAuthnRequestsSigned">Want AuthnRequests Signed</label>
@@ -111,19 +118,12 @@
</div>
<span tooltip-placement="right" tooltip="Enable/disable signature validation of SAML responses." class="fa fa-info-circle"></span>
</div>
- <div class="form-group">
- <label class="col-sm-2 control-label" for="postBindingResponse">HTTP-POST Binding Response</label>
- <div class="col-sm-4">
- <input ng-model="identityProvider.config.postBindingResponse" id="postBindingResponse" value="'true'" onoffswitchvalue />
- </div>
- <span tooltip-placement="right" tooltip="Indicates whether the identity provider must respond to the AuthnRequest using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used." class="fa fa-info-circle"></span>
- </div>
- <div class="form-group">
- <label class="col-sm-2 control-label" for="postBindingAuthnRequest">HTTP-POST Binding for AuthnRequest</label>
+ <div class="form-group clearfix" data-ng-show="identityProvider.config.validateSignature == 'true'">
+ <label class="col-sm-2 control-label" for="signingCertificate">Validating X509 Certificate</label>
<div class="col-sm-4">
- <input ng-model="identityProvider.config.postBindingAuthnRequest" id="postBindingAuthnRequest" value="'true'" onoffswitchvalue />
+ <textarea class="form-control" id="signingCertificate" ng-model="identityProvider.config.signingCertificate"/>
</div>
- <span tooltip-placement="right" tooltip="Indicates whether the AuthnRequest must be sent using HTTP-POST binding. If false, HTTP-REDIRECT binding will be used." class="fa fa-info-circle"></span>
+ <span tooltip-placement="right" tooltip="The certificate in PEM format that must be used to check for signatures." class="fa fa-info-circle"></span>
</div>
</fieldset>
<fieldset data-ng-show="newIdentityProvider">