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}