001/******************************************************************************* 002 * Copyright 2017 The MIT Internet Trust Consortium 003 * 004 * Portions copyright 2011-2013 The MITRE Corporation 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); 007 * you may not use this file except in compliance with the License. 008 * You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 *******************************************************************************/ 018/** 019 * 020 */ 021package org.mitre.openid.connect.assertion; 022 023import java.text.ParseException; 024import java.util.Date; 025import java.util.HashSet; 026import java.util.Set; 027 028import org.mitre.jwt.signer.service.JWTSigningAndValidationService; 029import org.mitre.jwt.signer.service.impl.ClientKeyCacheService; 030import org.mitre.oauth2.model.ClientDetailsEntity; 031import org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod; 032import org.mitre.oauth2.service.ClientDetailsEntityService; 033import org.mitre.openid.connect.config.ConfigurationPropertiesBean; 034import org.slf4j.Logger; 035import org.slf4j.LoggerFactory; 036import org.springframework.beans.factory.annotation.Autowired; 037import org.springframework.security.authentication.AuthenticationProvider; 038import org.springframework.security.authentication.AuthenticationServiceException; 039import org.springframework.security.core.Authentication; 040import org.springframework.security.core.AuthenticationException; 041import org.springframework.security.core.GrantedAuthority; 042import org.springframework.security.core.authority.SimpleGrantedAuthority; 043import org.springframework.security.core.userdetails.UsernameNotFoundException; 044import org.springframework.security.oauth2.common.exceptions.InvalidClientException; 045 046import com.nimbusds.jose.JWSAlgorithm; 047import com.nimbusds.jwt.JWT; 048import com.nimbusds.jwt.JWTClaimsSet; 049import com.nimbusds.jwt.SignedJWT; 050 051/** 052 * @author jricher 053 * 054 */ 055public class JWTBearerAuthenticationProvider implements AuthenticationProvider { 056 057 /** 058 * Logger for this class 059 */ 060 private static final Logger logger = LoggerFactory.getLogger(JWTBearerAuthenticationProvider.class); 061 062 private static final GrantedAuthority ROLE_CLIENT = new SimpleGrantedAuthority("ROLE_CLIENT"); 063 064 // map of verifiers, load keys for clients 065 @Autowired 066 private ClientKeyCacheService validators; 067 068 // Allow for time sync issues by having a window of X seconds. 069 private int timeSkewAllowance = 300; 070 071 // to load clients 072 @Autowired 073 private ClientDetailsEntityService clientService; 074 075 // to get our server's issuer url 076 @Autowired 077 private ConfigurationPropertiesBean config; 078 079 /** 080 * Try to validate the client credentials by parsing and validating the JWT. 081 */ 082 @Override 083 public Authentication authenticate(Authentication authentication) throws AuthenticationException { 084 085 JWTBearerAssertionAuthenticationToken jwtAuth = (JWTBearerAssertionAuthenticationToken)authentication; 086 087 088 try { 089 ClientDetailsEntity client = clientService.loadClientByClientId(jwtAuth.getName()); 090 091 JWT jwt = jwtAuth.getJwt(); 092 JWTClaimsSet jwtClaims = jwt.getJWTClaimsSet(); 093 094 if (!(jwt instanceof SignedJWT)) { 095 throw new AuthenticationServiceException("Unsupported JWT type: " + jwt.getClass().getName()); 096 } 097 098 // check the signature with nimbus 099 SignedJWT jws = (SignedJWT) jwt; 100 101 JWSAlgorithm alg = jws.getHeader().getAlgorithm(); 102 103 if (client.getTokenEndpointAuthSigningAlg() != null && 104 !client.getTokenEndpointAuthSigningAlg().equals(alg)) { 105 throw new AuthenticationServiceException("Client's registered token endpoint signing algorithm (" + client.getTokenEndpointAuthSigningAlg() 106 + ") does not match token's actual algorithm (" + alg.getName() + ")"); 107 } 108 109 if (client.getTokenEndpointAuthMethod() == null || 110 client.getTokenEndpointAuthMethod().equals(AuthMethod.NONE) || 111 client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_BASIC) || 112 client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_POST)) { 113 114 // this client doesn't support this type of authentication 115 throw new AuthenticationServiceException("Client does not support this authentication method."); 116 117 } else if ((client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY) && 118 (alg.equals(JWSAlgorithm.RS256) 119 || alg.equals(JWSAlgorithm.RS384) 120 || alg.equals(JWSAlgorithm.RS512) 121 || alg.equals(JWSAlgorithm.ES256) 122 || alg.equals(JWSAlgorithm.ES384) 123 || alg.equals(JWSAlgorithm.ES512) 124 || alg.equals(JWSAlgorithm.PS256) 125 || alg.equals(JWSAlgorithm.PS384) 126 || alg.equals(JWSAlgorithm.PS512))) 127 || (client.getTokenEndpointAuthMethod().equals(AuthMethod.SECRET_JWT) && 128 (alg.equals(JWSAlgorithm.HS256) 129 || alg.equals(JWSAlgorithm.HS384) 130 || alg.equals(JWSAlgorithm.HS512)))) { 131 132 // double-check the method is asymmetrical if we're in HEART mode 133 if (config.isHeartMode() && !client.getTokenEndpointAuthMethod().equals(AuthMethod.PRIVATE_KEY)) { 134 throw new AuthenticationServiceException("[HEART mode] Invalid authentication method"); 135 } 136 137 JWTSigningAndValidationService validator = validators.getValidator(client, alg); 138 139 if (validator == null) { 140 throw new AuthenticationServiceException("Unable to create signature validator for client " + client + " and algorithm " + alg); 141 } 142 143 if (!validator.validateSignature(jws)) { 144 throw new AuthenticationServiceException("Signature did not validate for presented JWT authentication."); 145 } 146 } else { 147 throw new AuthenticationServiceException("Unable to create signature validator for method " + client.getTokenEndpointAuthMethod() + " and algorithm " + alg); 148 } 149 150 // check the issuer 151 if (jwtClaims.getIssuer() == null) { 152 throw new AuthenticationServiceException("Assertion Token Issuer is null"); 153 } else if (!jwtClaims.getIssuer().equals(client.getClientId())){ 154 throw new AuthenticationServiceException("Issuers do not match, expected " + client.getClientId() + " got " + jwtClaims.getIssuer()); 155 } 156 157 // check expiration 158 if (jwtClaims.getExpirationTime() == null) { 159 throw new AuthenticationServiceException("Assertion Token does not have required expiration claim"); 160 } else { 161 // it's not null, see if it's expired 162 Date now = new Date(System.currentTimeMillis() - (timeSkewAllowance * 1000)); 163 if (now.after(jwtClaims.getExpirationTime())) { 164 throw new AuthenticationServiceException("Assertion Token is expired: " + jwtClaims.getExpirationTime()); 165 } 166 } 167 168 // check not before 169 if (jwtClaims.getNotBeforeTime() != null) { 170 Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000)); 171 if (now.before(jwtClaims.getNotBeforeTime())){ 172 throw new AuthenticationServiceException("Assertion Token not valid untill: " + jwtClaims.getNotBeforeTime()); 173 } 174 } 175 176 // check issued at 177 if (jwtClaims.getIssueTime() != null) { 178 // since it's not null, see if it was issued in the future 179 Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000)); 180 if (now.before(jwtClaims.getIssueTime())) { 181 throw new AuthenticationServiceException("Assertion Token was issued in the future: " + jwtClaims.getIssueTime()); 182 } 183 } 184 185 // check audience 186 if (jwtClaims.getAudience() == null) { 187 throw new AuthenticationServiceException("Assertion token audience is null"); 188 } else if (!(jwtClaims.getAudience().contains(config.getIssuer()) || jwtClaims.getAudience().contains(config.getIssuer() + "token"))) { 189 throw new AuthenticationServiceException("Audience does not match, expected " + config.getIssuer() + " or " + (config.getIssuer() + "token") + " got " + jwtClaims.getAudience()); 190 } 191 192 // IFF we managed to get all the way down here, the token is valid 193 194 // add in the ROLE_CLIENT authority 195 Set<GrantedAuthority> authorities = new HashSet<>(client.getAuthorities()); 196 authorities.add(ROLE_CLIENT); 197 198 return new JWTBearerAssertionAuthenticationToken(jwt, authorities); 199 200 } catch (InvalidClientException e) { 201 throw new UsernameNotFoundException("Could not find client: " + jwtAuth.getName()); 202 } catch (ParseException e) { 203 204 logger.error("Failure during authentication, error was: ", e); 205 206 throw new AuthenticationServiceException("Invalid JWT format"); 207 } 208 } 209 210 /** 211 * We support {@link JWTBearerAssertionAuthenticationToken}s only. 212 */ 213 @Override 214 public boolean supports(Class<?> authentication) { 215 return (JWTBearerAssertionAuthenticationToken.class.isAssignableFrom(authentication)); 216 } 217 218}