refactored signing and validation, added jwk-based cache, removed keyfetcher, refactored client side class structure
parent
7e64c4bffc
commit
385853fa1f
|
@ -1,4 +1,4 @@
|
||||||
package org.mitre.oauth2.filter;
|
package org.mitre.oauth2.introspectingfilter;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
|
@ -1,4 +1,4 @@
|
||||||
package org.mitre.oauth2.filter;
|
package org.mitre.oauth2.introspectingfilter;
|
||||||
|
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
|
@ -1,4 +1,4 @@
|
||||||
package org.mitre.oauth2.filter;
|
package org.mitre.oauth2.introspectingfilter;
|
||||||
|
|
||||||
import com.google.gson.JsonElement;
|
import com.google.gson.JsonElement;
|
||||||
import com.google.gson.JsonObject;
|
import com.google.gson.JsonObject;
|
|
@ -43,8 +43,10 @@ import org.apache.http.impl.client.DefaultHttpClient;
|
||||||
import org.apache.http.message.BasicNameValuePair;
|
import org.apache.http.message.BasicNameValuePair;
|
||||||
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||||
import org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService;
|
import org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService;
|
||||||
|
import org.mitre.jwt.signer.service.impl.JWKSetSigningAndValidationServiceCache;
|
||||||
import org.mitre.key.fetch.KeyFetcher;
|
import org.mitre.key.fetch.KeyFetcher;
|
||||||
import org.mitre.openid.connect.config.OIDCServerConfiguration;
|
import org.mitre.openid.connect.config.OIDCServerConfiguration;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
import org.springframework.security.authentication.AuthenticationServiceException;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
|
@ -87,7 +89,8 @@ public class AbstractOIDCAuthenticationFilter extends
|
||||||
// Allow for time sync issues by having a window of X seconds.
|
// Allow for time sync issues by having a window of X seconds.
|
||||||
private int timeSkewAllowance = 300;
|
private int timeSkewAllowance = 300;
|
||||||
|
|
||||||
private Map<OIDCServerConfiguration, JwtSigningAndValidationService> validationServices = new HashMap<OIDCServerConfiguration, JwtSigningAndValidationService>();
|
@Autowired
|
||||||
|
JWKSetSigningAndValidationServiceCache validationServices;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the redirect_uri that will be sent to the Authorization Endpoint.
|
* Builds the redirect_uri that will be sent to the Authorization Endpoint.
|
||||||
|
@ -352,7 +355,7 @@ public class AbstractOIDCAuthenticationFilter extends
|
||||||
ReadOnlyJWTClaimsSet idClaims = idToken.getJWTClaimsSet();
|
ReadOnlyJWTClaimsSet idClaims = idToken.getJWTClaimsSet();
|
||||||
|
|
||||||
// check the signature
|
// check the signature
|
||||||
JwtSigningAndValidationService jwtValidator = getValidatorForServer(serverConfig);
|
JwtSigningAndValidationService jwtValidator = validationServices.get(serverConfig.getJwkSigningUrl());
|
||||||
if (jwtValidator != null) {
|
if (jwtValidator != null) {
|
||||||
if(!jwtValidator.validateSignature(idToken)) {
|
if(!jwtValidator.validateSignature(idToken)) {
|
||||||
throw new AuthenticationServiceException("Signature validation failed");
|
throw new AuthenticationServiceException("Signature validation failed");
|
||||||
|
@ -607,57 +610,6 @@ public class AbstractOIDCAuthenticationFilter extends
|
||||||
return getStoredSessionString(session, REDIRECT_URI_SESION_VARIABLE);
|
return getStoredSessionString(session, REDIRECT_URI_SESION_VARIABLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected JwtSigningAndValidationService getValidatorForServer(OIDCServerConfiguration serverConfig) {
|
|
||||||
|
|
||||||
if(getValidationServices().containsKey(serverConfig)){
|
|
||||||
return validationServices.get(serverConfig);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
KeyFetcher keyFetch = new KeyFetcher();
|
|
||||||
PublicKey signingKey = null;
|
|
||||||
|
|
||||||
if (serverConfig.getJwkSigningUrl() != null) {
|
|
||||||
// prefer the JWK
|
|
||||||
signingKey = keyFetch.retrieveJwkKey(serverConfig.getJwkSigningUrl());
|
|
||||||
} else if (serverConfig.getX509SigningUrl() != null) {
|
|
||||||
// use the x509 only if JWK isn't configured
|
|
||||||
signingKey = keyFetch.retrieveX509Key(serverConfig.getX509SigningUrl());
|
|
||||||
} else {
|
|
||||||
// no keys configured
|
|
||||||
logger.warn("No server key URLs configured for " + serverConfig.getIssuer());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signingKey != null) {
|
|
||||||
|
|
||||||
// TODO: this assumes RSA
|
|
||||||
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) signingKey);
|
|
||||||
|
|
||||||
Map<String, JWSVerifier> verifiers = ImmutableMap.of(serverConfig.getIssuer(), verifier);
|
|
||||||
|
|
||||||
JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService(new HashMap<String, JWSSigner>(), verifiers);
|
|
||||||
|
|
||||||
validationServices.put(serverConfig, service);
|
|
||||||
|
|
||||||
return service;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// there were either no keys returned or no URLs configured to fetch them, assume no checking on key signatures
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public Map<OIDCServerConfiguration, JwtSigningAndValidationService> getValidationServices() {
|
|
||||||
return validationServices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setValidationServices(
|
|
||||||
Map<OIDCServerConfiguration, JwtSigningAndValidationService> validationServices) {
|
|
||||||
this.validationServices = validationServices;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getTimeSkewAllowance() {
|
public int getTimeSkewAllowance() {
|
||||||
return timeSkewAllowance;
|
return timeSkewAllowance;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package org.mitre.openid.connect.client;
|
package org.mitre.openid.connect.client.keypublisher;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package org.mitre.openid.connect.client;
|
package org.mitre.openid.connect.client.keypublisher;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
package org.mitre.openid.connect.client;
|
package org.mitre.openid.connect.client.keypublisher;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
|
@ -19,6 +19,7 @@ import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.nimbusds.jose.JWSAlgorithm;
|
||||||
import com.nimbusds.jwt.SignedJWT;
|
import com.nimbusds.jwt.SignedJWT;
|
||||||
|
|
||||||
public interface JwtSigningAndValidationService {
|
public interface JwtSigningAndValidationService {
|
||||||
|
@ -49,6 +50,12 @@ public interface JwtSigningAndValidationService {
|
||||||
*/
|
*/
|
||||||
public void signJwt(SignedJWT jwt) throws NoSuchAlgorithmException;
|
public void signJwt(SignedJWT jwt) throws NoSuchAlgorithmException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default signing algorithm for use when nothing else has been specified.
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
JWSAlgorithm getDefaultSigningAlgorithm();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign a jwt using the selected algorithm. The algorithm is selected using the String parameter values specified
|
* Sign a jwt using the selected algorithm. The algorithm is selected using the String parameter values specified
|
||||||
* in the JWT spec, section 6. I.E., "HS256" means HMAC with SHA-256 and corresponds to our HmacSigner class.
|
* in the JWT spec, section 6. I.E., "HS256" means HMAC with SHA-256 and corresponds to our HmacSigner class.
|
||||||
|
|
|
@ -28,7 +28,9 @@ import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.nimbusds.jose.JOSEException;
|
import com.nimbusds.jose.JOSEException;
|
||||||
|
import com.nimbusds.jose.JWSAlgorithm;
|
||||||
import com.nimbusds.jose.JWSSigner;
|
import com.nimbusds.jose.JWSSigner;
|
||||||
import com.nimbusds.jose.JWSVerifier;
|
import com.nimbusds.jose.JWSVerifier;
|
||||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||||
|
@ -36,9 +38,6 @@ import com.nimbusds.jwt.SignedJWT;
|
||||||
|
|
||||||
public class DefaultJwtSigningAndValidationService implements JwtSigningAndValidationService, InitializingBean {
|
public class DefaultJwtSigningAndValidationService implements JwtSigningAndValidationService, InitializingBean {
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private ConfigurationPropertiesBean configBean;
|
|
||||||
|
|
||||||
// map of identifier to signer
|
// map of identifier to signer
|
||||||
private Map<String, JWSSigner> signers = new HashMap<String, JWSSigner>();
|
private Map<String, JWSSigner> signers = new HashMap<String, JWSSigner>();
|
||||||
// map of identifier to verifier
|
// map of identifier to verifier
|
||||||
|
@ -46,6 +45,10 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
|
||||||
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(DefaultJwtSigningAndValidationService.class);
|
private static Logger logger = LoggerFactory.getLogger(DefaultJwtSigningAndValidationService.class);
|
||||||
|
|
||||||
|
private String defaultSignerId;
|
||||||
|
|
||||||
|
private JWSAlgorithm defaultAlgorithm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* default constructor
|
* default constructor
|
||||||
*/
|
*/
|
||||||
|
@ -53,17 +56,26 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefaultJwtSigningAndValidationService(Map<String, JWSSigner> signers, Map<String, JWSVerifier> verifiers) {
|
/**
|
||||||
this.signers = signers;
|
* Create a new validation service from the given set of verifiers (no signing)
|
||||||
|
*/
|
||||||
|
public DefaultJwtSigningAndValidationService(Map<String, JWSVerifier> verifiers) {
|
||||||
this.verifiers = verifiers;
|
this.verifiers = verifiers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public DefaultJwtSigningAndValidationService(Map<String, RSASSASignerVerifierBuilder> builders) {
|
/**
|
||||||
|
* Load this signing and validation service from the given builders (which load keys from keystores)
|
||||||
|
* @param builders
|
||||||
|
*/
|
||||||
|
public void setBuilders(Map<String, RSASSASignerVerifierBuilder> builders) {
|
||||||
|
|
||||||
for (Entry<String, RSASSASignerVerifierBuilder> e : builders.entrySet()) {
|
for (Entry<String, RSASSASignerVerifierBuilder> e : builders.entrySet()) {
|
||||||
|
|
||||||
JWSSigner signer = e.getValue().buildSigner();
|
JWSSigner signer = e.getValue().buildSigner();
|
||||||
signers.put(e.getKey(), signer);
|
signers.put(e.getKey(), signer);
|
||||||
|
if (e.getValue().isDefault()) {
|
||||||
|
defaultSignerId = e.getKey();
|
||||||
|
}
|
||||||
|
|
||||||
JWSVerifier verifier = e.getValue().buildVerifier();
|
JWSVerifier verifier = e.getValue().buildVerifier();
|
||||||
verifiers.put(e.getKey(), verifier);
|
verifiers.put(e.getKey(), verifier);
|
||||||
|
@ -72,6 +84,39 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the defaultSignerId
|
||||||
|
*/
|
||||||
|
public String getDefaultSignerId() {
|
||||||
|
return defaultSignerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param defaultSignerId the defaultSignerId to set
|
||||||
|
*/
|
||||||
|
public void setDefaultSignerId(String defaultSignerId) {
|
||||||
|
this.defaultSignerId = defaultSignerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public JWSAlgorithm getDefaultSigningAlgorithm() {
|
||||||
|
return defaultAlgorithm;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultSigningAlgorithmName(String algName) {
|
||||||
|
defaultAlgorithm = JWSAlgorithm.parse(algName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDefaultSigningAlgorithmName() {
|
||||||
|
if (defaultAlgorithm != null) {
|
||||||
|
return defaultAlgorithm.getName();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
/*
|
/*
|
||||||
* (non-Javadoc)
|
* (non-Javadoc)
|
||||||
*
|
*
|
||||||
|
@ -80,40 +125,19 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet(){
|
public void afterPropertiesSet(){
|
||||||
// used for debugging...
|
logger.info("DefaultJwtSigningAndValidationService is ready: " + this.toString());
|
||||||
if (!signers.isEmpty()) {
|
|
||||||
logger.info(this.toString());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("DefaultJwtSigningAndValidationService is open for business");
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the configBean
|
|
||||||
*/
|
|
||||||
public ConfigurationPropertiesBean getConfigBean() {
|
|
||||||
return configBean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param configBean the configBean to set
|
|
||||||
*/
|
|
||||||
public void setConfigBean(ConfigurationPropertiesBean configBean) {
|
|
||||||
this.configBean = configBean;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign a jwt in place using the configured default signer.
|
* Sign a jwt in place using the configured default signer.
|
||||||
* @throws JOSEException
|
|
||||||
* @throws NoSuchAlgorithmException
|
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void signJwt(SignedJWT jwt) {
|
public void signJwt(SignedJWT jwt) {
|
||||||
String signerId = configBean.getDefaultJwtSigner();
|
if (getDefaultSignerId() == null) {
|
||||||
|
throw new IllegalStateException("Tried to call default signing with no default signer ID set");
|
||||||
|
}
|
||||||
|
|
||||||
JWSSigner signer = signers.get(signerId);
|
JWSSigner signer = signers.get(getDefaultSignerId());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jwt.sign(signer);
|
jwt.sign(signer);
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package org.mitre.jwt.signer.service.impl;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.security.KeyFactory;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
import java.security.spec.RSAPublicKeySpec;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.apache.http.client.HttpClient;
|
||||||
|
import org.apache.http.impl.client.DefaultHttpClient;
|
||||||
|
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||||
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import com.google.common.cache.Cache;
|
||||||
|
import com.google.common.cache.CacheBuilder;
|
||||||
|
import com.google.common.cache.CacheLoader;
|
||||||
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
import com.nimbusds.jose.JWK;
|
||||||
|
import com.nimbusds.jose.JWKSet;
|
||||||
|
import com.nimbusds.jose.JWSSigner;
|
||||||
|
import com.nimbusds.jose.JWSVerifier;
|
||||||
|
import com.nimbusds.jose.KeyType;
|
||||||
|
import com.nimbusds.jose.RSAKey;
|
||||||
|
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Creates a
|
||||||
|
*
|
||||||
|
* @author jricher
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class JWKSetSigningAndValidationServiceCache {
|
||||||
|
|
||||||
|
private Cache<String, JwtSigningAndValidationService> cache;
|
||||||
|
|
||||||
|
public JWKSetSigningAndValidationServiceCache() {
|
||||||
|
this.cache = CacheBuilder.newBuilder()
|
||||||
|
.maximumSize(100)
|
||||||
|
.build(new JWKSetFetcher());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param key
|
||||||
|
* @return
|
||||||
|
* @throws ExecutionException
|
||||||
|
* @see com.google.common.cache.Cache#get(java.lang.Object)
|
||||||
|
*/
|
||||||
|
public JwtSigningAndValidationService get(String key) {
|
||||||
|
try {
|
||||||
|
return cache.get(key);
|
||||||
|
} catch (ExecutionException e) {
|
||||||
|
// TODO Auto-generated catch block
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author jricher
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private class JWKSetFetcher extends CacheLoader<String, JwtSigningAndValidationService> {
|
||||||
|
private HttpClient httpClient = new DefaultHttpClient();
|
||||||
|
private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
||||||
|
private RestTemplate restTemplate = new RestTemplate(httpFactory);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the JWK Set and build the appropriate signing service.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public JwtSigningAndValidationService load(String key) throws Exception {
|
||||||
|
|
||||||
|
String jsonString = restTemplate.getForObject(key, String.class);
|
||||||
|
JWKSet jwkSet = JWKSet.parse(jsonString);
|
||||||
|
|
||||||
|
Map<String, JWSVerifier> verifiers = new HashMap<String, JWSVerifier>();
|
||||||
|
|
||||||
|
for (JWK jwk : jwkSet.getKeys()) {
|
||||||
|
|
||||||
|
if (jwk.getKeyType().equals(KeyType.RSA)) {
|
||||||
|
// we can handle RSA
|
||||||
|
RSAKey rsa = (RSAKey)jwk;
|
||||||
|
|
||||||
|
byte[] modulusByte = rsa.getModulus().decode();
|
||||||
|
BigInteger modulus = new BigInteger(1, modulusByte);
|
||||||
|
byte[] exponentByte = rsa.getExponent().decode();
|
||||||
|
BigInteger exponent = new BigInteger(1, exponentByte);
|
||||||
|
|
||||||
|
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
|
||||||
|
KeyFactory factory = KeyFactory.getInstance("RSA");
|
||||||
|
|
||||||
|
RSAPublicKey pub = (RSAPublicKey) factory.generatePublic(spec);
|
||||||
|
|
||||||
|
|
||||||
|
JWSVerifier verifier = new RSASSAVerifier(pub);
|
||||||
|
|
||||||
|
verifiers.put(jwk.getKeyID(), verifier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService(verifiers);
|
||||||
|
|
||||||
|
return service;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -28,6 +28,8 @@ public class RSASSASignerVerifierBuilder {
|
||||||
private String alias;
|
private String alias;
|
||||||
private String password;
|
private String password;
|
||||||
private KeyStore keystore;
|
private KeyStore keystore;
|
||||||
|
private boolean defaultSigner = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the alias
|
* @return the alias
|
||||||
*/
|
*/
|
||||||
|
@ -65,6 +67,18 @@ public class RSASSASignerVerifierBuilder {
|
||||||
this.keystore = keystore;
|
this.keystore = keystore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the defaultSigner
|
||||||
|
*/
|
||||||
|
public boolean isDefault() {
|
||||||
|
return defaultSigner;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param defaultSigner the defaultSigner to set
|
||||||
|
*/
|
||||||
|
public void setDefault(boolean defaultSigner) {
|
||||||
|
this.defaultSigner = defaultSigner;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Build the signer as configured from the given keystore, null if it can't be built for some reason
|
* Build the signer as configured from the given keystore, null if it can't be built for some reason
|
||||||
* @return
|
* @return
|
||||||
|
|
|
@ -1,109 +0,0 @@
|
||||||
package org.mitre.key.fetch;
|
|
||||||
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
import java.security.KeyFactory;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.CertificateFactory;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.interfaces.RSAPublicKey;
|
|
||||||
import java.security.spec.InvalidKeySpecException;
|
|
||||||
import java.security.spec.RSAPublicKeySpec;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.binary.Base64;
|
|
||||||
import org.apache.http.client.HttpClient;
|
|
||||||
import org.apache.http.impl.client.DefaultHttpClient;
|
|
||||||
import org.mitre.openid.connect.config.OIDCServerConfiguration;
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
|
||||||
import org.springframework.security.authentication.AuthenticationServiceException;
|
|
||||||
import org.springframework.web.client.HttpClientErrorException;
|
|
||||||
import org.springframework.web.client.RestTemplate;
|
|
||||||
|
|
||||||
import com.google.gson.JsonArray;
|
|
||||||
import com.google.gson.JsonObject;
|
|
||||||
import com.google.gson.JsonParser;
|
|
||||||
|
|
||||||
public class KeyFetcher {
|
|
||||||
|
|
||||||
private HttpClient httpClient = new DefaultHttpClient();
|
|
||||||
private HttpComponentsClientHttpRequestFactory httpFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
|
|
||||||
private RestTemplate restTemplate = new RestTemplate(httpFactory);
|
|
||||||
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(KeyFetcher.class);
|
|
||||||
|
|
||||||
public JsonArray retrieveJwk(OIDCServerConfiguration serverConfig){
|
|
||||||
|
|
||||||
String jsonString = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
jsonString = restTemplate.getForObject(
|
|
||||||
serverConfig.getTokenEndpointUrl(), String.class);
|
|
||||||
} catch (HttpClientErrorException httpClientErrorException) {
|
|
||||||
|
|
||||||
throw new AuthenticationServiceException(
|
|
||||||
"Unable to obtain Access Token.");
|
|
||||||
}
|
|
||||||
|
|
||||||
JsonObject json = (JsonObject) new JsonParser().parse(jsonString);
|
|
||||||
JsonArray getArray = json.getAsJsonArray("jwk");
|
|
||||||
|
|
||||||
return getArray;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey retrieveX509Key(String x509url) {
|
|
||||||
|
|
||||||
|
|
||||||
PublicKey key = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
InputStream x509Stream = restTemplate.getForObject(x509url, InputStream.class);
|
|
||||||
CertificateFactory factory = CertificateFactory.getInstance("X.509");
|
|
||||||
X509Certificate cert = (X509Certificate) factory.generateCertificate(x509Stream);
|
|
||||||
key = cert.getPublicKey();
|
|
||||||
} catch (HttpClientErrorException e) {
|
|
||||||
logger.error("HttpClientErrorException in KeyFetcher.java: ", e);
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
logger.error("CertificateException in KeyFetcher.java: ", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PublicKey retrieveJwkKey(String jwkUrl) {
|
|
||||||
RSAPublicKey pub = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
String jwkString = restTemplate.getForObject(jwkUrl, String.class);
|
|
||||||
JsonObject json = (JsonObject) new JsonParser().parse(jwkString);
|
|
||||||
JsonArray getArray = json.getAsJsonArray("keys");
|
|
||||||
for(int i = 0; i < getArray.size(); i++) {
|
|
||||||
JsonObject object = getArray.get(i).getAsJsonObject();
|
|
||||||
String algorithm = object.get("alg").getAsString();
|
|
||||||
|
|
||||||
if(algorithm.equals("RSA")){
|
|
||||||
byte[] modulusByte = Base64.decodeBase64(object.get("mod").getAsString());
|
|
||||||
BigInteger modulus = new BigInteger(1, modulusByte);
|
|
||||||
byte[] exponentByte = Base64.decodeBase64(object.get("exp").getAsString());
|
|
||||||
BigInteger exponent = new BigInteger(1, exponentByte);
|
|
||||||
|
|
||||||
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
|
|
||||||
KeyFactory factory = KeyFactory.getInstance("RSA");
|
|
||||||
pub = (RSAPublicKey) factory.generatePublic(spec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (HttpClientErrorException e) {
|
|
||||||
logger.error("HttpClientErrorException in KeyFetcher.java: ", e);
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
logger.error("NoSuchAlgorithmException in KeyFetcher.java: ", e);
|
|
||||||
} catch (InvalidKeySpecException e) {
|
|
||||||
logger.error("InvalidKeySpecException in KeyFetcher.java: ", e);
|
|
||||||
}
|
|
||||||
return pub;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@
|
||||||
******************************************************************************/
|
******************************************************************************/
|
||||||
package org.mitre.openid.connect.config;
|
package org.mitre.openid.connect.config;
|
||||||
|
|
||||||
import com.nimbusds.jose.JWSAlgorithm;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,10 +29,6 @@ public class ConfigurationPropertiesBean {
|
||||||
|
|
||||||
private String issuer;
|
private String issuer;
|
||||||
|
|
||||||
private String defaultJwtSigner;
|
|
||||||
|
|
||||||
private JWSAlgorithm defaultAlgorithm;
|
|
||||||
|
|
||||||
private String adminConsoleTopbarTitle;
|
private String adminConsoleTopbarTitle;
|
||||||
|
|
||||||
private String logoImageUrl;
|
private String logoImageUrl;
|
||||||
|
@ -46,17 +41,6 @@ public class ConfigurationPropertiesBean {
|
||||||
public ConfigurationPropertiesBean() {
|
public ConfigurationPropertiesBean() {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return the defaultJwtSigner
|
|
||||||
*/
|
|
||||||
public String getDefaultJwtSigner() {
|
|
||||||
return defaultJwtSigner;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultJwtSigner(String signer) {
|
|
||||||
defaultJwtSigner = signer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the baseUrl
|
* @return the baseUrl
|
||||||
*/
|
*/
|
||||||
|
@ -71,25 +55,6 @@ public class ConfigurationPropertiesBean {
|
||||||
issuer = iss;
|
issuer = iss;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public JWSAlgorithm getDefaultSigningAlgorithm() {
|
|
||||||
return defaultAlgorithm;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultSigningAlgorithmName(String algName) {
|
|
||||||
defaultAlgorithm = JWSAlgorithm.parse(algName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDefaultSigningAlgorithmName() {
|
|
||||||
if (defaultAlgorithm != null) {
|
|
||||||
return defaultAlgorithm.getName();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAdminConsoleTopbarTitle() {
|
public String getAdminConsoleTopbarTitle() {
|
||||||
return adminConsoleTopbarTitle;
|
return adminConsoleTopbarTitle;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,11 @@
|
||||||
*/
|
*/
|
||||||
package org.mitre.openid.connect.assertion;
|
package org.mitre.openid.connect.assertion;
|
||||||
|
|
||||||
import java.security.PublicKey;
|
|
||||||
import java.security.interfaces.RSAPublicKey;
|
|
||||||
import java.text.ParseException;
|
import java.text.ParseException;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import org.mitre.key.fetch.KeyFetcher;
|
import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
|
||||||
|
import org.mitre.jwt.signer.service.impl.JWKSetSigningAndValidationServiceCache;
|
||||||
import org.mitre.oauth2.exception.ClientNotFoundException;
|
import org.mitre.oauth2.exception.ClientNotFoundException;
|
||||||
import org.mitre.oauth2.model.ClientDetailsEntity;
|
import org.mitre.oauth2.model.ClientDetailsEntity;
|
||||||
import org.mitre.oauth2.service.ClientDetailsEntityService;
|
import org.mitre.oauth2.service.ClientDetailsEntityService;
|
||||||
|
@ -24,12 +21,9 @@ import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
import org.springframework.security.core.AuthenticationException;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
|
||||||
import com.nimbusds.jose.JOSEException;
|
|
||||||
import com.nimbusds.jose.JWSObject;
|
|
||||||
import com.nimbusds.jose.JWSVerifier;
|
|
||||||
import com.nimbusds.jose.crypto.RSASSAVerifier;
|
|
||||||
import com.nimbusds.jwt.JWT;
|
import com.nimbusds.jwt.JWT;
|
||||||
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
|
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
|
||||||
|
import com.nimbusds.jwt.SignedJWT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author jricher
|
* @author jricher
|
||||||
|
@ -40,7 +34,8 @@ public class JwtBearerAuthenticationProvider implements AuthenticationProvider {
|
||||||
private static final Logger logger = LoggerFactory.getLogger(JwtBearerAuthenticationProvider.class);
|
private static final Logger logger = LoggerFactory.getLogger(JwtBearerAuthenticationProvider.class);
|
||||||
|
|
||||||
// map of verifiers, load keys for clients
|
// map of verifiers, load keys for clients
|
||||||
private Map<ClientDetailsEntity, JWSVerifier> verifiers = new HashMap<ClientDetailsEntity, JWSVerifier>();
|
@Autowired
|
||||||
|
private JWKSetSigningAndValidationServiceCache validators;
|
||||||
|
|
||||||
// Allow for time sync issues by having a window of X seconds.
|
// Allow for time sync issues by having a window of X seconds.
|
||||||
private int timeSkewAllowance = 300;
|
private int timeSkewAllowance = 300;
|
||||||
|
@ -69,11 +64,13 @@ public class JwtBearerAuthenticationProvider implements AuthenticationProvider {
|
||||||
ReadOnlyJWTClaimsSet jwtClaims = jwt.getJWTClaimsSet();
|
ReadOnlyJWTClaimsSet jwtClaims = jwt.getJWTClaimsSet();
|
||||||
|
|
||||||
// check the signature with nimbus
|
// check the signature with nimbus
|
||||||
JWSVerifier verifier = getVerifierForClient(client);
|
if (jwt instanceof SignedJWT) {
|
||||||
JWSObject jws = JWSObject.parse(jwtAuth.getJwt().toString());
|
SignedJWT jws = (SignedJWT)jwt;
|
||||||
if (verifier == null && !jws.verify(verifier)) {
|
JwtSigningAndValidationService validator = validators.get(client.getJwkUrl());
|
||||||
|
if (validator == null || !validator.validateSignature(jws)) {
|
||||||
throw new AuthenticationServiceException("Invalid signature");
|
throw new AuthenticationServiceException("Invalid signature");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// check the issuer
|
// check the issuer
|
||||||
if (jwtClaims.getIssuer() == null) {
|
if (jwtClaims.getIssuer() == null) {
|
||||||
|
@ -125,12 +122,7 @@ public class JwtBearerAuthenticationProvider implements AuthenticationProvider {
|
||||||
} catch (ParseException e) {
|
} catch (ParseException e) {
|
||||||
// TODO Auto-generated catch block
|
// TODO Auto-generated catch block
|
||||||
throw new AuthenticationServiceException("Invalid JWT format");
|
throw new AuthenticationServiceException("Invalid JWT format");
|
||||||
} catch (JOSEException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
throw new AuthenticationServiceException("JOSE Error");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -141,39 +133,5 @@ public class JwtBearerAuthenticationProvider implements AuthenticationProvider {
|
||||||
return (JwtBearerAssertionAuthenticationToken.class.isAssignableFrom(authentication));
|
return (JwtBearerAssertionAuthenticationToken.class.isAssignableFrom(authentication));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected JWSVerifier getVerifierForClient(ClientDetailsEntity client) {
|
|
||||||
|
|
||||||
if(verifiers.containsKey(client)){
|
|
||||||
return verifiers.get(client);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
KeyFetcher keyFetch = new KeyFetcher();
|
|
||||||
PublicKey signingKey = null;
|
|
||||||
|
|
||||||
if (client.getJwkUrl() != null) {
|
|
||||||
// prefer the JWK
|
|
||||||
signingKey = keyFetch.retrieveJwkKey(client.getJwkUrl());
|
|
||||||
} else if (client.getX509Url() != null) {
|
|
||||||
// use the x509 only if JWK isn't configured
|
|
||||||
signingKey = keyFetch.retrieveX509Key(client.getX509Url());
|
|
||||||
} else {
|
|
||||||
// no keys configured
|
|
||||||
logger.warn("No server key URLs configured for " + client.getClientId());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (signingKey != null) {
|
|
||||||
|
|
||||||
// TODO: this assumes RSA
|
|
||||||
JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) signingKey);
|
|
||||||
|
|
||||||
verifiers.put(client, verifier);
|
|
||||||
|
|
||||||
return verifier;
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// there were either no keys returned or no URLs configured to fetch them, assume no checking on key signatures
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ public class ConnectTokenEnhancer implements TokenEnhancer {
|
||||||
OAuth2AccessTokenEntity token = (OAuth2AccessTokenEntity) accessToken;
|
OAuth2AccessTokenEntity token = (OAuth2AccessTokenEntity) accessToken;
|
||||||
|
|
||||||
String clientId = authentication.getAuthorizationRequest().getClientId();
|
String clientId = authentication.getAuthorizationRequest().getClientId();
|
||||||
|
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
|
||||||
|
|
||||||
JWTClaimsSet claims = new JWTClaimsSet();
|
JWTClaimsSet claims = new JWTClaimsSet();
|
||||||
|
|
||||||
|
@ -73,7 +74,9 @@ public class ConnectTokenEnhancer implements TokenEnhancer {
|
||||||
|
|
||||||
claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it
|
claims.setJWTID(UUID.randomUUID().toString()); // set a random NONCE in the middle of it
|
||||||
|
|
||||||
SignedJWT signed = new SignedJWT(new JWSHeader(configBean.getDefaultSigningAlgorithm()), claims);
|
// TODO: use client's default signing algorithm
|
||||||
|
|
||||||
|
SignedJWT signed = new SignedJWT(new JWSHeader(jwtService.getDefaultSigningAlgorithm()), claims);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
jwtService.signJwt(signed);
|
jwtService.signJwt(signed);
|
||||||
|
@ -106,8 +109,6 @@ public class ConnectTokenEnhancer implements TokenEnhancer {
|
||||||
|
|
||||||
idClaims.setIssueTime(new Date());
|
idClaims.setIssueTime(new Date());
|
||||||
|
|
||||||
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
|
|
||||||
|
|
||||||
if (client.getIdTokenValiditySeconds() != null) {
|
if (client.getIdTokenValiditySeconds() != null) {
|
||||||
Date expiration = new Date(System.currentTimeMillis() + (client.getIdTokenValiditySeconds() * 1000L));
|
Date expiration = new Date(System.currentTimeMillis() + (client.getIdTokenValiditySeconds() * 1000L));
|
||||||
idClaims.setExpirationTime(expiration);
|
idClaims.setExpirationTime(expiration);
|
||||||
|
@ -124,7 +125,7 @@ public class ConnectTokenEnhancer implements TokenEnhancer {
|
||||||
idClaims.setCustomClaim("nonce", nonce);
|
idClaims.setCustomClaim("nonce", nonce);
|
||||||
}
|
}
|
||||||
|
|
||||||
SignedJWT idToken = new SignedJWT(new JWSHeader(configBean.getDefaultSigningAlgorithm()), idClaims);
|
SignedJWT idToken = new SignedJWT(new JWSHeader(jwtService.getDefaultSigningAlgorithm()), idClaims);
|
||||||
|
|
||||||
//TODO: check for client's preferred signer alg and use that
|
//TODO: check for client's preferred signer alg and use that
|
||||||
|
|
||||||
|
|
|
@ -10,17 +10,19 @@
|
||||||
|
|
||||||
<bean id="defaultsignerService"
|
<bean id="defaultsignerService"
|
||||||
class="org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService">
|
class="org.mitre.jwt.signer.service.impl.DefaultJwtSigningAndValidationService">
|
||||||
<constructor-arg name="builders">
|
<property name="builders">
|
||||||
<map>
|
<map>
|
||||||
<entry key="rsa1">
|
<entry key="rsa1">
|
||||||
<bean id="rsaSignerBuilder" class="org.mitre.jwt.signer.service.impl.RSASSASignerVerifierBuilder">
|
<bean id="rsaSignerBuilder" class="org.mitre.jwt.signer.service.impl.RSASSASignerVerifierBuilder">
|
||||||
<property name="keystore" ref="defaultKeystore" />
|
<property name="keystore" ref="defaultKeystore" />
|
||||||
<property name="alias" value="rsa" />
|
<property name="alias" value="rsa" />
|
||||||
<property name="password" value="changeit" />
|
<property name="password" value="changeit" />
|
||||||
|
<property name="default" value="true" />
|
||||||
</bean>
|
</bean>
|
||||||
</entry>
|
</entry>
|
||||||
</map>
|
</map>
|
||||||
</constructor-arg>
|
</property>
|
||||||
|
<property name="defaultSigningAlgorithmName" value="RS256" />
|
||||||
</bean>
|
</bean>
|
||||||
|
|
||||||
</beans>
|
</beans>
|
||||||
|
|
|
@ -14,8 +14,6 @@
|
||||||
|
|
||||||
<bean id="configBean" class="org.mitre.openid.connect.config.ConfigurationPropertiesBean">
|
<bean id="configBean" class="org.mitre.openid.connect.config.ConfigurationPropertiesBean">
|
||||||
<property name="issuer" value="http://localhost/" />
|
<property name="issuer" value="http://localhost/" />
|
||||||
<property name="defaultJwtSigner" value="rsa1"/>
|
|
||||||
<property name="defaultSigningAlgorithmName" value="RS256" />
|
|
||||||
<property name="adminConsoleCopyrightFooter" value="The MTIRE Corporation" />
|
<property name="adminConsoleCopyrightFooter" value="The MTIRE Corporation" />
|
||||||
<property name="logoImageUrl" value="https://id.mitre.org/connect/resources/images/openid_connect_small.png" />
|
<property name="logoImageUrl" value="https://id.mitre.org/connect/resources/images/openid_connect_small.png" />
|
||||||
<property name="adminConsoleTopbarTitle" value="MITREid Connect" />
|
<property name="adminConsoleTopbarTitle" value="MITREid Connect" />
|
||||||
|
|
Loading…
Reference in New Issue