converted client filters to nimbus-jose

pull/306/merge
Justin Richer 2013-02-19 17:33:22 -05:00
parent 8d8010d90f
commit 3d9ec51eb3
4 changed files with 180 additions and 126 deletions

View File

@ -21,9 +21,9 @@ import java.math.BigInteger;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey; import java.security.interfaces.RSAPublicKey;
import java.text.ParseException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration; import java.util.Enumeration;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -40,15 +40,10 @@ import org.apache.http.client.HttpClient;
import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.client.utils.URLEncodedUtils;
import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair; import org.apache.http.message.BasicNameValuePair;
import org.mitre.jwt.model.JwtClaims;
import org.mitre.jwt.signer.JwsAlgorithm;
import org.mitre.jwt.signer.JwtSigner;
import org.mitre.jwt.signer.impl.RsaSigner;
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.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.mitre.openid.connect.model.IdToken;
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;
@ -60,9 +55,14 @@ import org.springframework.util.MultiValueMap;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import com.google.gson.JsonParser; import com.google.gson.JsonParser;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.crypto.RSASSAVerifier;
import com.nimbusds.jwt.ReadOnlyJWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
/** /**
* Abstract OpenID Connect Authentication Filter class * Abstract OpenID Connect Authentication Filter class
@ -339,95 +339,105 @@ public class AbstractOIDCAuthenticationFilter extends
refreshTokenValue = tokenResponse.get("refresh_token").getAsString(); refreshTokenValue = tokenResponse.get("refresh_token").getAsString();
} }
IdToken idToken = IdToken.parse(idTokenValue); // TODO: catch parsing errors? try {
SignedJWT idToken = SignedJWT.parse(idTokenValue);
// validate our ID Token over a number of tests // validate our ID Token over a number of tests
JwtClaims idClaims = idToken.getClaims(); ReadOnlyJWTClaimsSet idClaims = idToken.getJWTClaimsSet();
// check the signature // check the signature
JwtSigningAndValidationService jwtValidator = getValidatorForServer(serverConfig); JwtSigningAndValidationService jwtValidator = getValidatorForServer(serverConfig);
if (jwtValidator != null) { if (jwtValidator != null) {
if(!jwtValidator.validateSignature(idToken.toString())) { if(!jwtValidator.validateSignature(idToken)) {
throw new AuthenticationServiceException("Signature validation failed"); throw new AuthenticationServiceException("Signature validation failed");
} }
} else { } else {
logger.info("No validation service found. Skipping signature validation"); logger.info("No validation service found. Skipping signature validation");
} }
// check the issuer // check the issuer
if (idClaims.getIssuer() == null) { if (idClaims.getIssuerClaim() == null) {
throw new AuthenticationServiceException("Id Token Issuer is null"); throw new AuthenticationServiceException("Id Token Issuer is null");
} else if (!idClaims.getIssuer().equals(serverConfig.getIssuer())){ } else if (!idClaims.getIssuerClaim().equals(serverConfig.getIssuer())){
throw new AuthenticationServiceException("Issuers do not match, expected " + serverConfig.getIssuer() + " got " + idClaims.getIssuer()); throw new AuthenticationServiceException("Issuers do not match, expected " + serverConfig.getIssuer() + " got " + idClaims.getIssuerClaim());
} }
// check expiration // check expiration
if (idClaims.getExpiration() == null) { // FIXME: Nimbus Date Fields
throw new AuthenticationServiceException("Id Token does not have required expiration claim"); /*
} else { if (idClaims.getExpirationTimeClaim() == 0) {
// it's not null, see if it's expired throw new AuthenticationServiceException("Id Token does not have required expiration claim");
Date now = new Date(System.currentTimeMillis() - (timeSkewAllowance * 1000)); } else {
if (now.after(idClaims.getExpiration())) { // it's not null, see if it's expired
throw new AuthenticationServiceException("Id Token is expired: " + idClaims.getExpiration()); Date now = new Date(System.currentTimeMillis() - (timeSkewAllowance * 1000));
} if (now.after(new Date(idClaims.getExpirationTimeClaim()))) {
} throw new AuthenticationServiceException("Id Token is expired: " + idClaims.getExpirationTimeClaim());
}
// check not before }
if (idClaims.getNotBefore() != null) {
Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000)); // check not before
if (now.before(idClaims.getNotBefore())){ // FIXME: Nimbus Date Fields
throw new AuthenticationServiceException("Id Token not valid untill: " + idClaims.getNotBefore()); if (idClaims.getNotBefore() != null) {
} Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
} if (now.before(idClaims.getNotBefore())){
throw new AuthenticationServiceException("Id Token not valid untill: " + idClaims.getNotBefore());
// check audience }
if (idClaims.getAudience() == null) { }
throw new AuthenticationServiceException("Id token audience is null");
} else if (!idClaims.getAudience().equals(serverConfig.getClientId())) { // check issued at
throw new AuthenticationServiceException("Audience does not match, expected " + serverConfig.getClientId() + " got " + idClaims.getAudience()); if (idClaims.getIssuedAt() == null) {
} throw new AuthenticationServiceException("Id Token does not have required issued-at claim");
} else {
// check issued at // since it's not null, see if it was issued in the future
if (idClaims.getIssuedAt() == null) { Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
throw new AuthenticationServiceException("Id Token does not have required issued-at claim"); if (now.before(idClaims.getIssuedAt())) {
} else { throw new AuthenticationServiceException("Id Token was issued in the future: " + idClaims.getIssuedAt());
// since it's not null, see if it was issued in the future }
Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000)); }
if (now.before(idClaims.getIssuedAt())) { */
throw new AuthenticationServiceException("Id Token was issued in the future: " + idClaims.getIssuedAt());
} // check audience
} // FIXME: Nimbus audience collection
if (idClaims.getAudienceClaim() == null) {
throw new AuthenticationServiceException("Id token audience is null");
} else if (!Arrays.asList(idClaims.getAudienceClaim()).contains(serverConfig.getClientId())) {
throw new AuthenticationServiceException("Audience does not match, expected " + serverConfig.getClientId() + " got " + idClaims.getAudienceClaim());
}
// compare the nonce to our stored claim
// FIXME: Nimbus claims as strings?
String nonce = (String) idClaims.getCustomClaim("nonce");
if (StringUtils.isBlank(nonce)) {
logger.error("ID token did not contain a nonce claim.");
// compare the nonce to our stored claim throw new AuthenticationServiceException("ID token did not contain a nonce claim.");
String nonce = idClaims.getNonce(); }
if (StringUtils.isBlank(nonce)) {
logger.error("ID token did not contain a nonce claim.");
throw new AuthenticationServiceException("ID token did not contain a nonce claim."); String storedNonce = getStoredNonce(session);
} if (!nonce.equals(storedNonce)) {
logger.error("Possible replay attack detected! The comparison of the nonce in the returned "
+ "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
String storedNonce = getStoredNonce(session); throw new AuthenticationServiceException(
if (!nonce.equals(storedNonce)) { "Possible replay attack detected! The comparison of the nonce in the returned "
logger.error("Possible replay attack detected! The comparison of the nonce in the returned " + "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
+ "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + "."); }
throw new AuthenticationServiceException( // pull the subject (user id) out as a claim on the id_token
"Possible replay attack detected! The comparison of the nonce in the returned "
+ "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + "."); String userId = idClaims.getSubjectClaim();
}
// construct an OIDCAuthenticationToken and return a Authentication object w/the userId and the idToken
OIDCAuthenticationToken token = new OIDCAuthenticationToken(userId, idClaims.getIssuerClaim(), serverConfig, idTokenValue, accessTokenValue, refreshTokenValue);
// pull the subject (user id) out as a claim on the id_token Authentication authentication = this.getAuthenticationManager().authenticate(token);
String userId = idToken.getClaims().getSubject();
// construct an OIDCAuthenticationToken and return a Authentication object w/the userId and the idToken
OIDCAuthenticationToken token = new OIDCAuthenticationToken(userId, idClaims.getIssuer(), serverConfig, idTokenValue, accessTokenValue, refreshTokenValue);
Authentication authentication = this.getAuthenticationManager().authenticate(token); return authentication;
} catch (ParseException e) {
return authentication; throw new AuthenticationServiceException("Couldn't parse idToken: ", e);
}
@ -618,27 +628,17 @@ public class AbstractOIDCAuthenticationFilter extends
} }
if (signingKey != null) { if (signingKey != null) {
Map<String, JwtSigner> signers = new HashMap<String, JwtSigner>();
if (signingKey instanceof RSAPublicKey) {
RSAPublicKey rsaKey = (RSAPublicKey)signingKey;
// build an RSA signer
RsaSigner signer256 = new RsaSigner(JwsAlgorithm.RS256.getJwaName(), rsaKey, null);
RsaSigner signer384 = new RsaSigner(JwsAlgorithm.RS384.getJwaName(), rsaKey, null);
RsaSigner signer512 = new RsaSigner(JwsAlgorithm.RS512.getJwaName(), rsaKey, null);
signers.put(serverConfig.getIssuer() + JwsAlgorithm.RS256.getJwaName(), signer256); // TODO: this assumes RSA
signers.put(serverConfig.getIssuer() + JwsAlgorithm.RS384.getJwaName(), signer384); JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) signingKey);
signers.put(serverConfig.getIssuer() + JwsAlgorithm.RS512.getJwaName(), signer512);
}
JwtSigningAndValidationService signingAndValidationService = new DefaultJwtSigningAndValidationService(signers);
validationServices.put(serverConfig, signingAndValidationService); Map<String, JWSVerifier> verifiers = ImmutableMap.of(serverConfig.getIssuer(), verifier);
return signingAndValidationService; JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService();
validationServices.put(serverConfig, service);
return service;
} else { } else {
// there were either no keys returned or no URLs configured to fetch them, assume no checking on key signatures // there were either no keys returned or no URLs configured to fetch them, assume no checking on key signatures

View File

@ -2,6 +2,7 @@ package org.mitre.openid.connect.client;
import java.io.IOException; import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
@ -12,9 +13,6 @@ import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.mitre.jwt.model.Jwt;
import org.mitre.jwt.model.JwtClaims;
import org.mitre.jwt.signer.JwtSigner;
import org.mitre.jwt.signer.service.JwtSigningAndValidationService; import org.mitre.jwt.signer.service.JwtSigningAndValidationService;
import org.mitre.openid.connect.config.OIDCServerConfiguration; import org.mitre.openid.connect.config.OIDCServerConfiguration;
import org.mitre.openid.connect.view.JwkKeyListView; import org.mitre.openid.connect.view.JwkKeyListView;
@ -30,6 +28,9 @@ import org.springframework.util.Assert;
import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.ModelAndView;
import com.google.common.base.Strings; import com.google.common.base.Strings;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter implements BeanDefinitionRegistryPostProcessor { public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter implements BeanDefinitionRegistryPostProcessor {
@ -108,7 +109,7 @@ public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter im
public void handleAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, public void handleAuthorizationRequest(HttpServletRequest request, HttpServletResponse response,
OIDCServerConfiguration serverConfiguration) throws IOException { OIDCServerConfiguration serverConfiguration) throws IOException {
Jwt jwt = createAndSignRequestJwt(request, response, serverConfiguration); SignedJWT jwt = createAndSignRequestJwt(request, response, serverConfiguration);
Map<String, String> urlVariables = new HashMap<String, String>(); Map<String, String> urlVariables = new HashMap<String, String>();
@ -121,31 +122,33 @@ public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter im
response.sendRedirect(authRequest); response.sendRedirect(authRequest);
} }
public Jwt createAndSignRequestJwt(HttpServletRequest request, HttpServletResponse response, OIDCServerConfiguration serverConfiguration) { public SignedJWT createAndSignRequestJwt(HttpServletRequest request, HttpServletResponse response, OIDCServerConfiguration serverConfiguration) {
HttpSession session = request.getSession(); HttpSession session = request.getSession();
Jwt jwt = new Jwt();
JwtClaims claims = jwt.getClaims(); JWTClaimsSet claims = new JWTClaimsSet();
//set parameters to JwtHeader //set parameters to JwtHeader
// header.setAlgorithm(JwsAlgorithm.getByName(SIGNING_ALGORITHM).toString()); // header.setAlgorithm(JwsAlgorithm.getByName(SIGNING_ALGORITHM).toString());
//set parameters to JwtClaims //set parameters to JwtClaims
claims.setClaim("response_type", "code"); claims.setCustomClaim("response_type", "code");
claims.setClaim("client_id", serverConfiguration.getClientId()); claims.setCustomClaim("client_id", serverConfiguration.getClientId());
claims.setClaim("scope", scope); claims.setCustomClaim("scope", scope);
// build our redirect URI // build our redirect URI
String redirectUri = buildRedirectURI(request, null); String redirectUri = buildRedirectURI(request, null);
claims.setClaim("redirect_uri", redirectUri); claims.setCustomClaim("redirect_uri", redirectUri);
session.setAttribute(REDIRECT_URI_SESION_VARIABLE, redirectUri); session.setAttribute(REDIRECT_URI_SESION_VARIABLE, redirectUri);
//create random nonce and state, save them to the session //create random nonce and state, save them to the session
String nonce = createNonce(session); String nonce = createNonce(session);
claims.setClaim("nonce", nonce); claims.setCustomClaim("nonce", nonce);
String state = createState(session); String state = createState(session);
claims.setClaim("state", state); claims.setCustomClaim("state", state);
SignedJWT jwt = new SignedJWT(new JWSHeader(serverConfiguration.getSigningAlgorithm()), claims);
try { try {
signingAndValidationService.signJwt(jwt); signingAndValidationService.signJwt(jwt);
@ -285,12 +288,12 @@ public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter im
*/ */
public ModelAndView publishClientJwk() { public ModelAndView publishClientJwk() {
// map from key id to signer // map from key id to key
Map<String, JwtSigner> signers = signingAndValidationService.getAllSigners(); Map<String, PublicKey> keys = signingAndValidationService.getAllPublicKeys();
// TODO: check if keys are empty, return a 404 here or just an empty list? // TODO: check if keys are empty, return a 404 here or just an empty list?
return new ModelAndView(jwkViewName, "signers", signers); return new ModelAndView(jwkViewName, "keys", keys);
} }
/** /**
@ -298,12 +301,12 @@ public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter im
* @return * @return
*/ */
public ModelAndView publishClientx509() { public ModelAndView publishClientx509() {
// map from key id to signer // map from key id to key
Map<String, JwtSigner> signers = signingAndValidationService.getAllSigners(); Map<String, PublicKey> keys = signingAndValidationService.getAllPublicKeys();
// TODO: check if keys are empty, return a 404 here or just an empty list? // TODO: check if keys are empty, return a 404 here or just an empty list?
return new ModelAndView(x509ViewName, "signers", signers); return new ModelAndView(x509ViewName, "keys", keys);
} }
/** /**

View File

@ -53,6 +53,11 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid
} }
public DefaultJwtSigningAndValidationService(Map<String, JWSSigner> signers, Map<String, JWSVerifier> verifiers) {
this.signers = signers;
this.verifiers = verifiers;
}
public DefaultJwtSigningAndValidationService(Map<String, RSASSASignerVerifierBuilder> builders) { public DefaultJwtSigningAndValidationService(Map<String, RSASSASignerVerifierBuilder> builders) {
for (Entry<String, RSASSASignerVerifierBuilder> e : builders.entrySet()) { for (Entry<String, RSASSASignerVerifierBuilder> e : builders.entrySet()) {

View File

@ -15,6 +15,10 @@
******************************************************************************/ ******************************************************************************/
package org.mitre.openid.connect.config; package org.mitre.openid.connect.config;
import javax.persistence.Basic;
import com.nimbusds.jose.JWSAlgorithm;
/** /**
* @author nemonik * @author nemonik
@ -41,6 +45,8 @@ public class OIDCServerConfiguration {
private String jwkSigningUrl; private String jwkSigningUrl;
private String userInfoUrl; private String userInfoUrl;
private JWSAlgorithm signingAlgorithm;
public String getAuthorizationEndpointUrl() { public String getAuthorizationEndpointUrl() {
return authorizationEndpointUrl; return authorizationEndpointUrl;
@ -137,4 +143,44 @@ public class OIDCServerConfiguration {
+ x509SigningUrl + ", jwkEncryptUrl=" + jwkEncryptUrl + ", jwkSigningUrl=" + jwkSigningUrl + ", userInfoUrl=" + userInfoUrl + "]"; + x509SigningUrl + ", jwkEncryptUrl=" + jwkEncryptUrl + ", jwkSigningUrl=" + jwkSigningUrl + ", userInfoUrl=" + userInfoUrl + "]";
} }
/**
* @return the signingAlgorithm
*/
public JWSAlgorithm getSigningAlgorithm() {
return signingAlgorithm;
}
/**
* @param signingAlgorithm the signingAlgorithm to set
*/
public void setSigningAlgorithm(JWSAlgorithm signingAlgorithm) {
this.signingAlgorithm = signingAlgorithm;
}
/**
* Get the name of this algorithm, return null if no algorithm set.
* @return
*/
@Basic
public String getAlgorithmName() {
if (signingAlgorithm != null) {
return signingAlgorithm.getName();
} else {
return null;
}
}
/**
* Set the name of this algorithm.
* Calls JWSAlgorithm.parse()
* @param algorithmName
*/
public void setAlgorithmName(String algorithmName) {
if (algorithmName != null) {
signingAlgorithm = JWSAlgorithm.parse(algorithmName);
} else {
signingAlgorithm = null;
}
}
} }