diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/AbstractOIDCAuthenticationFilter.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/AbstractOIDCAuthenticationFilter.java index b026e39c3..3f638a6d2 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/AbstractOIDCAuthenticationFilter.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/AbstractOIDCAuthenticationFilter.java @@ -21,9 +21,9 @@ import java.math.BigInteger; import java.security.PublicKey; import java.security.SecureRandom; import java.security.interfaces.RSAPublicKey; +import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Date; import java.util.Enumeration; import java.util.HashMap; 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.impl.client.DefaultHttpClient; 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.impl.DefaultJwtSigningAndValidationService; import org.mitre.key.fetch.KeyFetcher; import org.mitre.openid.connect.config.OIDCServerConfiguration; -import org.mitre.openid.connect.model.IdToken; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.security.authentication.AuthenticationServiceException; 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.RestTemplate; +import com.google.common.collect.ImmutableMap; import com.google.gson.JsonElement; import com.google.gson.JsonObject; 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 @@ -339,95 +339,105 @@ public class AbstractOIDCAuthenticationFilter extends 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 - JwtClaims idClaims = idToken.getClaims(); - - // check the signature - JwtSigningAndValidationService jwtValidator = getValidatorForServer(serverConfig); - if (jwtValidator != null) { - if(!jwtValidator.validateSignature(idToken.toString())) { - throw new AuthenticationServiceException("Signature validation failed"); - } - } else { - logger.info("No validation service found. Skipping signature validation"); - } - - // check the issuer - if (idClaims.getIssuer() == null) { - throw new AuthenticationServiceException("Id Token Issuer is null"); - } else if (!idClaims.getIssuer().equals(serverConfig.getIssuer())){ - throw new AuthenticationServiceException("Issuers do not match, expected " + serverConfig.getIssuer() + " got " + idClaims.getIssuer()); - } - - // check expiration - if (idClaims.getExpiration() == null) { - throw new AuthenticationServiceException("Id Token does not have required expiration claim"); - } else { - // it's not null, see if it's expired - Date now = new Date(System.currentTimeMillis() - (timeSkewAllowance * 1000)); - if (now.after(idClaims.getExpiration())) { - throw new AuthenticationServiceException("Id Token is expired: " + idClaims.getExpiration()); - } - } - - // check not before - 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())) { - throw new AuthenticationServiceException("Audience does not match, expected " + serverConfig.getClientId() + " got " + idClaims.getAudience()); - } - - // check issued at - if (idClaims.getIssuedAt() == null) { - throw new AuthenticationServiceException("Id Token does not have required issued-at claim"); - } else { - // 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()); - } - } + // validate our ID Token over a number of tests + ReadOnlyJWTClaimsSet idClaims = idToken.getJWTClaimsSet(); + + // check the signature + JwtSigningAndValidationService jwtValidator = getValidatorForServer(serverConfig); + if (jwtValidator != null) { + if(!jwtValidator.validateSignature(idToken)) { + throw new AuthenticationServiceException("Signature validation failed"); + } + } else { + logger.info("No validation service found. Skipping signature validation"); + } + + // check the issuer + if (idClaims.getIssuerClaim() == null) { + throw new AuthenticationServiceException("Id Token Issuer is null"); + } else if (!idClaims.getIssuerClaim().equals(serverConfig.getIssuer())){ + throw new AuthenticationServiceException("Issuers do not match, expected " + serverConfig.getIssuer() + " got " + idClaims.getIssuerClaim()); + } + + // check expiration + // FIXME: Nimbus Date Fields + /* + if (idClaims.getExpirationTimeClaim() == 0) { + throw new AuthenticationServiceException("Id Token does not have required expiration claim"); + } else { + // it's not null, see if it's expired + 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 + // FIXME: Nimbus Date Fields + 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 issued at + if (idClaims.getIssuedAt() == null) { + throw new AuthenticationServiceException("Id Token does not have required issued-at claim"); + } else { + // 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 - 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."); + } - 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); - 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 + "."); + throw new AuthenticationServiceException( + "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 + "."); + } - throw new AuthenticationServiceException( - "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 + "."); - } + // pull the subject (user id) out as a claim on the id_token + + 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 - - 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); - Authentication authentication = this.getAuthenticationManager().authenticate(token); - - return authentication; + return authentication; + } catch (ParseException e) { + throw new AuthenticationServiceException("Couldn't parse idToken: ", e); + } @@ -618,27 +628,17 @@ public class AbstractOIDCAuthenticationFilter extends } if (signingKey != null) { - Map signers = new HashMap(); - - 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); - signers.put(serverConfig.getIssuer() + JwsAlgorithm.RS384.getJwaName(), signer384); - signers.put(serverConfig.getIssuer() + JwsAlgorithm.RS512.getJwaName(), signer512); - } - - JwtSigningAndValidationService signingAndValidationService = new DefaultJwtSigningAndValidationService(signers); + // TODO: this assumes RSA + JWSVerifier verifier = new RSASSAVerifier((RSAPublicKey) signingKey); - validationServices.put(serverConfig, signingAndValidationService); + Map verifiers = ImmutableMap.of(serverConfig.getIssuer(), verifier); - return signingAndValidationService; + JwtSigningAndValidationService service = new DefaultJwtSigningAndValidationService(); + + 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 diff --git a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCSignedRequestFilter.java b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCSignedRequestFilter.java index ce5a52cd3..1074c2add 100644 --- a/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCSignedRequestFilter.java +++ b/openid-connect-client/src/main/java/org/mitre/openid/connect/client/OIDCSignedRequestFilter.java @@ -2,6 +2,7 @@ package org.mitre.openid.connect.client; import java.io.IOException; import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; import java.util.HashMap; import java.util.Map; import java.util.UUID; @@ -12,9 +13,6 @@ import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; 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.openid.connect.config.OIDCServerConfiguration; import org.mitre.openid.connect.view.JwkKeyListView; @@ -30,6 +28,9 @@ import org.springframework.util.Assert; import org.springframework.web.servlet.ModelAndView; 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 { @@ -108,7 +109,7 @@ public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter im public void handleAuthorizationRequest(HttpServletRequest request, HttpServletResponse response, OIDCServerConfiguration serverConfiguration) throws IOException { - Jwt jwt = createAndSignRequestJwt(request, response, serverConfiguration); + SignedJWT jwt = createAndSignRequestJwt(request, response, serverConfiguration); Map urlVariables = new HashMap(); @@ -121,31 +122,33 @@ public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter im 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(); - Jwt jwt = new Jwt(); - JwtClaims claims = jwt.getClaims(); + + JWTClaimsSet claims = new JWTClaimsSet(); //set parameters to JwtHeader // header.setAlgorithm(JwsAlgorithm.getByName(SIGNING_ALGORITHM).toString()); //set parameters to JwtClaims - claims.setClaim("response_type", "code"); - claims.setClaim("client_id", serverConfiguration.getClientId()); - claims.setClaim("scope", scope); + claims.setCustomClaim("response_type", "code"); + claims.setCustomClaim("client_id", serverConfiguration.getClientId()); + claims.setCustomClaim("scope", scope); // build our redirect URI String redirectUri = buildRedirectURI(request, null); - claims.setClaim("redirect_uri", redirectUri); + claims.setCustomClaim("redirect_uri", redirectUri); session.setAttribute(REDIRECT_URI_SESION_VARIABLE, redirectUri); //create random nonce and state, save them to the session String nonce = createNonce(session); - claims.setClaim("nonce", nonce); + claims.setCustomClaim("nonce", nonce); String state = createState(session); - claims.setClaim("state", state); + claims.setCustomClaim("state", state); + + SignedJWT jwt = new SignedJWT(new JWSHeader(serverConfiguration.getSigningAlgorithm()), claims); try { signingAndValidationService.signJwt(jwt); @@ -285,12 +288,12 @@ public class OIDCSignedRequestFilter extends AbstractOIDCAuthenticationFilter im */ public ModelAndView publishClientJwk() { - // map from key id to signer - Map signers = signingAndValidationService.getAllSigners(); + // map from key id to key + Map keys = signingAndValidationService.getAllPublicKeys(); // 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 */ public ModelAndView publishClientx509() { - // map from key id to signer - Map signers = signingAndValidationService.getAllSigners(); - + // map from key id to key + Map keys = signingAndValidationService.getAllPublicKeys(); + // 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); } /** diff --git a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJwtSigningAndValidationService.java b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJwtSigningAndValidationService.java index 4d894556c..328a0bd6b 100644 --- a/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJwtSigningAndValidationService.java +++ b/openid-connect-common/src/main/java/org/mitre/jwt/signer/service/impl/DefaultJwtSigningAndValidationService.java @@ -53,6 +53,11 @@ public class DefaultJwtSigningAndValidationService implements JwtSigningAndValid } + public DefaultJwtSigningAndValidationService(Map signers, Map verifiers) { + this.signers = signers; + this.verifiers = verifiers; + } + public DefaultJwtSigningAndValidationService(Map builders) { for (Entry e : builders.entrySet()) { diff --git a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/OIDCServerConfiguration.java b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/OIDCServerConfiguration.java index 316e92eb9..c1199eced 100644 --- a/openid-connect-common/src/main/java/org/mitre/openid/connect/config/OIDCServerConfiguration.java +++ b/openid-connect-common/src/main/java/org/mitre/openid/connect/config/OIDCServerConfiguration.java @@ -15,6 +15,10 @@ ******************************************************************************/ package org.mitre.openid.connect.config; +import javax.persistence.Basic; + +import com.nimbusds.jose.JWSAlgorithm; + /** * @author nemonik @@ -41,6 +45,8 @@ public class OIDCServerConfiguration { private String jwkSigningUrl; private String userInfoUrl; + + private JWSAlgorithm signingAlgorithm; public String getAuthorizationEndpointUrl() { return authorizationEndpointUrl; @@ -137,4 +143,44 @@ public class OIDCServerConfiguration { + 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; + } + } + } \ No newline at end of file