keycloak-uncached

jwks parsing

3/31/2015 3:25:26 PM

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">