Skip to content

Commit

Permalink
jwks parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
patriot1burke committed Mar 31, 2015
1 parent 4a650e5 commit 2d7e861
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 51 deletions.
Expand Up @@ -4,17 +4,20 @@
import org.keycloak.constants.AdapterConstants; import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.adapters.action.AdminAction; import org.keycloak.representations.adapters.action.AdminAction;
import org.keycloak.representations.adapters.action.LogoutAction; import org.keycloak.representations.adapters.action.LogoutAction;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.PemUtils;


import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path; import javax.ws.rs.Path;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.io.IOException; import java.io.IOException;
import java.security.PublicKey;


/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
Expand All @@ -40,10 +43,12 @@ public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, Event
@Path(AdapterConstants.K_LOGOUT) @Path(AdapterConstants.K_LOGOUT)
public Response backchannelLogout(String input) { public Response backchannelLogout(String input) {
JWSInput token = new JWSInput(input); JWSInput token = new JWSInput(input);
String signingCert = getConfig().getSigningCertificate(); PublicKey key = getExternalIdpKey();
if (signingCert != null && !signingCert.trim().equals("")) { if (key != null) {
if (!token.verify(getConfig().getSigningCertificate())) { if (!verify(token, key)) {
return Response.status(400).build(); } logger.warn("Failed to verify logout request");
return Response.status(400).build();
}
} }
LogoutAction action = null; LogoutAction action = null;
try { try {
Expand Down
Expand Up @@ -50,19 +50,7 @@ public String getId() {


@Override @Override
public Map<String, String> parseConfig(InputStream inputStream) { public Map<String, String> parseConfig(InputStream inputStream) {
OIDCConfigurationRepresentation rep = null; return OIDCIdentityProviderFactory.parseOIDCConfig(inputStream);
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();


} }
} }
Expand Up @@ -19,6 +19,7 @@


import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonNode;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.RSATokenVerifier;
import org.keycloak.broker.oidc.util.SimpleHttp; import org.keycloak.broker.oidc.util.SimpleHttp;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.FederatedIdentity; import org.keycloak.broker.provider.FederatedIdentity;
Expand All @@ -28,6 +29,7 @@
import org.keycloak.events.EventGroup; import org.keycloak.events.EventGroup;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSInput; import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.AccessTokenResponse;
Expand All @@ -38,6 +40,7 @@
import org.keycloak.services.resources.RealmsResource; import org.keycloak.services.resources.RealmsResource;
import org.keycloak.services.resources.flows.Flows; import org.keycloak.services.resources.flows.Flows;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.PemUtils;


import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.Path; import javax.ws.rs.Path;
Expand All @@ -47,6 +50,7 @@
import javax.ws.rs.core.UriBuilder; import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo; import javax.ws.rs.core.UriInfo;
import java.io.IOException; import java.io.IOException;
import java.security.PublicKey;
import java.util.Map; import java.util.Map;


/** /**
Expand Down Expand Up @@ -74,6 +78,28 @@ public Object callback(RealmModel realm, AuthenticationCallback callback, EventB
return new OIDCEndpoint(callback, realm, event); 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 { protected class OIDCEndpoint extends Endpoint {
public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) { public OIDCEndpoint(AuthenticationCallback callback, RealmModel realm, EventBuilder event) {
super(callback, realm, event); super(callback, realm, event);
Expand Down Expand Up @@ -140,11 +166,8 @@ protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, Stri
} catch (IOException e) { } catch (IOException e) {
throw new IdentityBrokerException("Could not decode access token response.", e); throw new IdentityBrokerException("Could not decode access token response.", e);
} }
String accessToken = tokenResponse.getToken(); PublicKey key = getExternalIdpKey();

String accessToken = verifyAccessToken(key, tokenResponse);
if (accessToken == null) {
throw new IdentityBrokerException("No access_token from server.");
}


String encodedIdToken = tokenResponse.getIdToken(); String encodedIdToken = tokenResponse.getIdToken();


Expand All @@ -154,7 +177,7 @@ protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, Stri
notes.put(FEDERATED_TOKEN_EXPIRATION, Long.toString(tokenResponse.getExpiresIn())); notes.put(FEDERATED_TOKEN_EXPIRATION, Long.toString(tokenResponse.getExpiresIn()));




IDToken idToken = validateIdToken(encodedIdToken); IDToken idToken = validateIdToken(key, encodedIdToken);


try { try {
String id = idToken.getSubject(); String id = idToken.getSubject();
Expand Down Expand Up @@ -204,19 +227,32 @@ protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, Stri
} }
} }


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) { if (encodedToken == null) {
throw new IdentityBrokerException("No id_token from server."); throw new IdentityBrokerException("No id_token from server.");
} }


try { 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 aud = idToken.getAudience();
String iss = idToken.getIssuer(); String iss = idToken.getIssuer();


if (aud != null && !aud.equals(getConfig().getClientId())) { 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(); String trustedIssuers = getConfig().getIssuer();
Expand Down
Expand Up @@ -47,13 +47,29 @@ public String getLogoutUrl() {
public void setLogoutUrl(String url) { public void setLogoutUrl(String url) {
getConfig().put("logoutUrl", url); getConfig().put("logoutUrl", url);
} }
public String getSigningCertificate() { public String getCertificateSignatureVerifier() {
return getConfig().get("signingCertificate"); return getConfig().get("certificateSignatureVerifier");
} }


public void setSigningCertificate(String signingCertificate) { public void setCertificateSignatureVerifier(String signingCertificate) {
getConfig().put("signingCertificate", 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));
}







Expand Down
Expand Up @@ -17,13 +17,21 @@
*/ */
package org.keycloak.broker.oidc; 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.broker.provider.AbstractIdentityProviderFactory;
import org.keycloak.jose.jwk.JWK;
import org.keycloak.jose.jwk.JWKParser;
import org.keycloak.models.IdentityProviderModel; 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.protocol.oidc.representations.OIDCConfigurationRepresentation;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.util.PemUtils;


import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.security.PublicKey;
import java.util.Map; import java.util.Map;


/** /**
Expand All @@ -50,6 +58,10 @@ public String getId() {


@Override @Override
public Map<String, String> parseConfig(InputStream inputStream) { public Map<String, String> parseConfig(InputStream inputStream) {
return parseOIDCConfig(inputStream);
}

protected static Map<String, String> parseOIDCConfig(InputStream inputStream) {
OIDCConfigurationRepresentation rep = null; OIDCConfigurationRepresentation rep = null;
try { try {
rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class); rep = JsonSerialization.readValue(inputStream, OIDCConfigurationRepresentation.class);
Expand All @@ -62,7 +74,27 @@ public Map<String, String> parseConfig(InputStream inputStream) {
config.setAuthorizationUrl(rep.getAuthorizationEndpoint()); config.setAuthorizationUrl(rep.getAuthorizationEndpoint());
config.setTokenUrl(rep.getTokenEndpoint()); config.setTokenUrl(rep.getTokenEndpoint());
config.setUserInfoUrl(rep.getUserinfoEndpoint()); 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();
} }
} }
22 changes: 22 additions & 0 deletions core/src/main/java/org/keycloak/jose/jwk/JWK.java 100644 → 100755
@@ -1,7 +1,12 @@
package org.keycloak.jose.jwk; package org.keycloak.jose.jwk;


import org.codehaus.jackson.annotate.JsonAnyGetter;
import org.codehaus.jackson.annotate.JsonAnySetter;
import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonProperty;


import java.util.HashMap;
import java.util.Map;

/** /**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a> * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/ */
Expand All @@ -15,6 +20,9 @@ public class JWK {


public static final String PUBLIC_KEY_USE = "use"; 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) @JsonProperty(KEY_ID)
private String keyId; private String keyId;


Expand All @@ -27,6 +35,9 @@ public class JWK {
@JsonProperty(PUBLIC_KEY_USE) @JsonProperty(PUBLIC_KEY_USE)
private String publicKeyUse; private String publicKeyUse;


protected Map<String, Object> otherClaims = new HashMap<String, Object>();


public String getKeyId() { public String getKeyId() {
return keyId; return keyId;
} }
Expand Down Expand Up @@ -59,4 +70,15 @@ public void setPublicKeyUse(String publicKeyUse) {
this.publicKeyUse = publicKeyUse; this.publicKeyUse = publicKeyUse;
} }


@JsonAnyGetter
public Map<String, Object> getOtherClaims() {
return otherClaims;
}

@JsonAnySetter
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}


} }
22 changes: 17 additions & 5 deletions core/src/main/java/org/keycloak/jose/jwk/JWKParser.java 100644 → 100755
Expand Up @@ -17,29 +17,41 @@ public class JWKParser {


private static TypeReference<Map<String,String>> typeRef = new TypeReference<Map<String,String>>() {}; private static TypeReference<Map<String,String>> typeRef = new TypeReference<Map<String,String>>() {};


private Map<String, String> values; private JWK jwk;


private JWKParser() { private JWKParser() {
} }


public JWKParser(JWK jwk) {
this.jwk = jwk;
}

public static JWKParser create() { public static JWKParser create() {
return new JWKParser(); return new JWKParser();
} }


public static JWKParser create(JWK jwk) {
return new JWKParser(jwk);
}

public JWKParser parse(String jwk) { public JWKParser parse(String jwk) {
try { try {
this.values = JsonSerialization.mapper.readValue(jwk, typeRef); this.jwk = JsonSerialization.mapper.readValue(jwk, JWK.class);
return this; return this;
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }


public JWK getJwk() {
return jwk;
}

public PublicKey toPublicKey() { public PublicKey toPublicKey() {
String algorithm = values.get(JWK.KEY_TYPE); String algorithm = jwk.getKeyType();
if (RSAPublicJWK.RSA.equals(algorithm)) { if (RSAPublicJWK.RSA.equals(algorithm)) {
BigInteger modulus = new BigInteger(1, Base64Url.decode(values.get(RSAPublicJWK.MODULUS))); BigInteger modulus = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.MODULUS).toString()));
BigInteger publicExponent = new BigInteger(1, Base64Url.decode(values.get(RSAPublicJWK.PUBLIC_EXPONENT))); BigInteger publicExponent = new BigInteger(1, Base64Url.decode(jwk.getOtherClaims().get(RSAPublicJWK.PUBLIC_EXPONENT).toString()));


try { try {
return KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); return KeyFactory.getInstance("RSA").generatePublic(new RSAPublicKeySpec(modulus, publicExponent));
Expand Down
Expand Up @@ -132,6 +132,20 @@ <h2 class="pull-left">{{identityProvider.alias}} Provider Settings</h2>
</div> </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> <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>
<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>
<fieldset data-ng-show="newIdentityProvider"> <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> <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>
Expand Down

0 comments on commit 2d7e861

Please sign in to comment.