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.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>
Expand All @@ -40,10 +43,12 @@ public KeycloakEndpoint(AuthenticationCallback callback, RealmModel realm, Event
@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 {
Expand Down
Expand Up @@ -50,19 +50,7 @@ public String getId() {

@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);

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

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;
Expand All @@ -28,6 +29,7 @@
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;
Expand All @@ -38,6 +40,7 @@
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;
Expand All @@ -47,6 +50,7 @@
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.io.IOException;
import java.security.PublicKey;
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);
}

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);
Expand Down Expand Up @@ -140,11 +166,8 @@ protected FederatedIdentity getFederatedIdentity(Map<String, String> notes, Stri
} 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();

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


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

try {
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) {
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();
Expand Down
Expand Up @@ -47,13 +47,29 @@ public String getLogoutUrl() {
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));
}




Expand Down
Expand Up @@ -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;

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

@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);
Expand All @@ -62,7 +74,27 @@ public Map<String, String> parseConfig(InputStream inputStream) {
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();
}
}
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;

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>
*/
Expand All @@ -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;

Expand All @@ -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;
}
Expand Down Expand Up @@ -59,4 +70,15 @@ public void setPublicKeyUse(String 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 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));
Expand Down
Expand Up @@ -132,6 +132,20 @@ <h2 class="pull-left">{{identityProvider.alias}} Provider Settings</h2>
</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>
Expand Down

0 comments on commit 2d7e861

Please sign in to comment.