001/*******************************************************************************
002 * Copyright 2018 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 *******************************************************************************/
018package org.mitre.openid.connect.client;
019
020import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.PRIVATE_KEY;
021import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.SECRET_BASIC;
022import static org.mitre.oauth2.model.ClientDetailsEntity.AuthMethod.SECRET_JWT;
023
024import java.io.IOException;
025import java.math.BigInteger;
026import java.net.URI;
027import java.nio.charset.StandardCharsets;
028import java.security.MessageDigest;
029import java.security.NoSuchAlgorithmException;
030import java.security.SecureRandom;
031import java.text.ParseException;
032import java.util.Date;
033import java.util.Map;
034import java.util.UUID;
035
036import javax.servlet.ServletException;
037import javax.servlet.http.HttpServletRequest;
038import javax.servlet.http.HttpServletResponse;
039import javax.servlet.http.HttpSession;
040
041import org.apache.http.client.HttpClient;
042import org.apache.http.client.config.RequestConfig;
043import org.apache.http.impl.client.HttpClientBuilder;
044import org.mitre.jwt.signer.service.JWTSigningAndValidationService;
045import org.mitre.jwt.signer.service.impl.JWKSetCacheService;
046import org.mitre.jwt.signer.service.impl.SymmetricKeyJWTValidatorCacheService;
047import org.mitre.oauth2.model.PKCEAlgorithm;
048import org.mitre.oauth2.model.RegisteredClient;
049import org.mitre.openid.connect.client.model.IssuerServiceResponse;
050import org.mitre.openid.connect.client.service.AuthRequestOptionsService;
051import org.mitre.openid.connect.client.service.AuthRequestUrlBuilder;
052import org.mitre.openid.connect.client.service.ClientConfigurationService;
053import org.mitre.openid.connect.client.service.IssuerService;
054import org.mitre.openid.connect.client.service.ServerConfigurationService;
055import org.mitre.openid.connect.client.service.impl.StaticAuthRequestOptionsService;
056import org.mitre.openid.connect.config.ServerConfiguration;
057import org.mitre.openid.connect.model.PendingOIDCAuthenticationToken;
058import org.springframework.beans.factory.annotation.Autowired;
059import org.springframework.http.HttpMethod;
060import org.springframework.http.client.ClientHttpRequest;
061import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
062import org.springframework.security.authentication.AuthenticationServiceException;
063import org.springframework.security.core.Authentication;
064import org.springframework.security.core.AuthenticationException;
065import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
066import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
067import org.springframework.util.LinkedMultiValueMap;
068import org.springframework.util.MultiValueMap;
069import org.springframework.web.client.RestClientException;
070import org.springframework.web.client.RestTemplate;
071import org.springframework.web.util.UriUtils;
072
073import com.google.common.base.Strings;
074import com.google.common.collect.Iterables;
075import com.google.common.collect.Lists;
076import com.google.gson.JsonElement;
077import com.google.gson.JsonObject;
078import com.google.gson.JsonParser;
079import com.nimbusds.jose.Algorithm;
080import com.nimbusds.jose.JWSAlgorithm;
081import com.nimbusds.jose.JWSHeader;
082import com.nimbusds.jose.util.Base64;
083import com.nimbusds.jose.util.Base64URL;
084import com.nimbusds.jwt.JWT;
085import com.nimbusds.jwt.JWTClaimsSet;
086import com.nimbusds.jwt.JWTParser;
087import com.nimbusds.jwt.PlainJWT;
088import com.nimbusds.jwt.SignedJWT;
089
090/**
091 * OpenID Connect Authentication Filter class
092 *
093 * @author nemonik, jricher
094 *
095 */
096public class OIDCAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
097
098        protected final static String REDIRECT_URI_SESION_VARIABLE = "redirect_uri";
099        protected final static String CODE_VERIFIER_SESSION_VARIABLE = "code_verifier";
100        protected final static String STATE_SESSION_VARIABLE = "state";
101        protected final static String NONCE_SESSION_VARIABLE = "nonce";
102        protected final static String ISSUER_SESSION_VARIABLE = "issuer";
103        protected final static String TARGET_SESSION_VARIABLE = "target";
104        protected final static int HTTP_SOCKET_TIMEOUT = 30000;
105
106        public final static String FILTER_PROCESSES_URL = "/openid_connect_login";
107
108        // Allow for time sync issues by having a window of X seconds.
109        private int timeSkewAllowance = 300;
110
111        // fetches and caches public keys for servers
112        @Autowired(required=false)
113        private JWKSetCacheService validationServices;
114
115        // creates JWT signer/validators for symmetric keys
116        @Autowired(required=false)
117        private SymmetricKeyJWTValidatorCacheService symmetricCacheService;
118
119        // signer based on keypair for this client (for outgoing auth requests)
120        @Autowired(required=false)
121        private JWTSigningAndValidationService authenticationSignerService;
122
123        @Autowired(required=false)
124        private HttpClient httpClient;
125
126        /*
127         * Modular services to build out client filter.
128         */
129        // looks at the request and determines which issuer to use for lookup on the server
130        private IssuerService issuerService;
131        // holds server information (auth URI, token URI, etc.), indexed by issuer
132        private ServerConfigurationService servers;
133        // holds client information (client ID, redirect URI, etc.), indexed by issuer of the server
134        private ClientConfigurationService clients;
135        // provides extra options to inject into the outbound request
136        private AuthRequestOptionsService authOptions = new StaticAuthRequestOptionsService(); // initialize with an empty set of options
137        // builds the actual request URI based on input from all other services
138        private AuthRequestUrlBuilder authRequestBuilder;
139
140        // private helpers to handle target link URLs
141        private TargetLinkURIAuthenticationSuccessHandler targetSuccessHandler = new TargetLinkURIAuthenticationSuccessHandler();
142        private TargetLinkURIChecker deepLinkFilter;
143
144        protected int httpSocketTimeout = HTTP_SOCKET_TIMEOUT;
145
146        /**
147         * OpenIdConnectAuthenticationFilter constructor
148         */
149        public OIDCAuthenticationFilter() {
150                super(FILTER_PROCESSES_URL);
151                targetSuccessHandler.passthrough = super.getSuccessHandler();
152                super.setAuthenticationSuccessHandler(targetSuccessHandler);
153        }
154
155        @Override
156        public void afterPropertiesSet() {
157                super.afterPropertiesSet();
158
159                // if our JOSE validators don't get wired in, drop defaults into place
160
161                if (validationServices == null) {
162                        validationServices = new JWKSetCacheService();
163                }
164
165                if (symmetricCacheService == null) {
166                        symmetricCacheService = new SymmetricKeyJWTValidatorCacheService();
167                }
168
169        }
170
171        /*
172         * This is the main entry point for the filter.
173         *
174         * (non-Javadoc)
175         *
176         * @see org.springframework.security.web.authentication.
177         * AbstractAuthenticationProcessingFilter
178         * #attemptAuthentication(javax.servlet.http.HttpServletRequest,
179         * javax.servlet.http.HttpServletResponse)
180         */
181        @Override
182        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
183
184                if (!Strings.isNullOrEmpty(request.getParameter("error"))) {
185
186                        // there's an error coming back from the server, need to handle this
187                        handleError(request, response);
188                        return null; // no auth, response is sent to display page or something
189
190                } else if (!Strings.isNullOrEmpty(request.getParameter("code"))) {
191
192                        // we got back the code, need to process this to get our tokens
193                        Authentication auth = handleAuthorizationCodeResponse(request, response);
194                        return auth;
195
196                } else {
197
198                        // not an error, not a code, must be an initial login of some type
199                        handleAuthorizationRequest(request, response);
200
201                        return null; // no auth, response redirected to the server's Auth Endpoint (or possibly to the account chooser)
202                }
203
204        }
205
206        /**
207         * Initiate an Authorization request
208         *
209         * @param request
210         *            The request from which to extract parameters and perform the
211         *            authentication
212         * @param response
213         * @throws IOException
214         *             If an input or output exception occurs
215         */
216        protected void handleAuthorizationRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
217
218                HttpSession session = request.getSession();
219
220                IssuerServiceResponse issResp = issuerService.getIssuer(request);
221
222                if (issResp == null) {
223                        logger.error("Null issuer response returned from service.");
224                        throw new AuthenticationServiceException("No issuer found.");
225                }
226
227                if (issResp.shouldRedirect()) {
228                        response.sendRedirect(issResp.getRedirectUrl());
229                } else {
230                        String issuer = issResp.getIssuer();
231
232                        if (!Strings.isNullOrEmpty(issResp.getTargetLinkUri())) {
233                                // there's a target URL in the response, we should save this so we can forward to it later
234                                session.setAttribute(TARGET_SESSION_VARIABLE, issResp.getTargetLinkUri());
235                        }
236
237                        if (Strings.isNullOrEmpty(issuer)) {
238                                logger.error("No issuer found: " + issuer);
239                                throw new AuthenticationServiceException("No issuer found: " + issuer);
240                        }
241
242                        ServerConfiguration serverConfig = servers.getServerConfiguration(issuer);
243                        if (serverConfig == null) {
244                                logger.error("No server configuration found for issuer: " + issuer);
245                                throw new AuthenticationServiceException("No server configuration found for issuer: " + issuer);
246                        }
247
248
249                        session.setAttribute(ISSUER_SESSION_VARIABLE, serverConfig.getIssuer());
250
251                        RegisteredClient clientConfig = clients.getClientConfiguration(serverConfig);
252                        if (clientConfig == null) {
253                                logger.error("No client configuration found for issuer: " + issuer);
254                                throw new AuthenticationServiceException("No client configuration found for issuer: " + issuer);
255                        }
256
257                        String redirectUri = null;
258                        if (clientConfig.getRegisteredRedirectUri() != null && clientConfig.getRegisteredRedirectUri().size() == 1) {
259                                // if there's a redirect uri configured (and only one), use that
260                                redirectUri = Iterables.getOnlyElement(clientConfig.getRegisteredRedirectUri());
261                        } else {
262                                // otherwise our redirect URI is this current URL, with no query parameters
263                                redirectUri = request.getRequestURL().toString();
264                        }
265                        session.setAttribute(REDIRECT_URI_SESION_VARIABLE, redirectUri);
266
267                        // this value comes back in the id token and is checked there
268                        String nonce = createNonce(session);
269
270                        // this value comes back in the auth code response
271                        String state = createState(session);
272
273                        Map<String, String> options = authOptions.getOptions(serverConfig, clientConfig, request);
274
275                        // if we're using PKCE, handle the challenge here
276                        if (clientConfig.getCodeChallengeMethod() != null) {
277                                String codeVerifier = createCodeVerifier(session);
278                                options.put("code_challenge_method", clientConfig.getCodeChallengeMethod().getName());
279                                if (clientConfig.getCodeChallengeMethod().equals(PKCEAlgorithm.plain)) {
280                                        options.put("code_challenge", codeVerifier);
281                                } else if (clientConfig.getCodeChallengeMethod().equals(PKCEAlgorithm.S256)) {
282                                        try {
283                                                MessageDigest digest = MessageDigest.getInstance("SHA-256");
284                                                String hash = Base64URL.encode(digest.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII))).toString();
285                                                options.put("code_challenge", hash);
286                                        } catch (NoSuchAlgorithmException e) {
287                                                // TODO Auto-generated catch block
288                                                e.printStackTrace();
289                                        }
290
291
292                                }
293                        }
294
295                        String authRequest = authRequestBuilder.buildAuthRequestUrl(serverConfig, clientConfig, redirectUri, nonce, state, options, issResp.getLoginHint());
296
297                        logger.debug("Auth Request:  " + authRequest);
298
299                        response.sendRedirect(authRequest);
300                }
301        }
302
303        /**
304         * @param request
305         *            The request from which to extract parameters and perform the
306         *            authentication
307         * @return The authenticated user token, or null if authentication is
308         *         incomplete.
309         */
310        protected Authentication handleAuthorizationCodeResponse(HttpServletRequest request, HttpServletResponse response) {
311
312                String authorizationCode = request.getParameter("code");
313
314                HttpSession session = request.getSession();
315
316                // check for state, if it doesn't match we bail early
317                String storedState = getStoredState(session);
318                String requestState = request.getParameter("state");
319                if (storedState == null || !storedState.equals(requestState)) {
320                        throw new AuthenticationServiceException("State parameter mismatch on return. Expected " + storedState + " got " + requestState);
321                }
322
323                // look up the issuer that we set out to talk to
324                String issuer = getStoredSessionString(session, ISSUER_SESSION_VARIABLE);
325
326                // pull the configurations based on that issuer
327                ServerConfiguration serverConfig = servers.getServerConfiguration(issuer);
328                final RegisteredClient clientConfig = clients.getClientConfiguration(serverConfig);
329
330                MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
331                form.add("grant_type", "authorization_code");
332                form.add("code", authorizationCode);
333                form.setAll(authOptions.getTokenOptions(serverConfig, clientConfig, request));
334
335                String codeVerifier = getStoredCodeVerifier(session);
336                if (codeVerifier != null) {
337                        form.add("code_verifier", codeVerifier);
338                }
339
340                String redirectUri = getStoredSessionString(session, REDIRECT_URI_SESION_VARIABLE);
341                if (redirectUri != null) {
342                        form.add("redirect_uri", redirectUri);
343                }
344
345                // Handle Token Endpoint interaction
346
347                if(httpClient == null) {
348                        httpClient = HttpClientBuilder.create()
349                                        .useSystemProperties()
350                                        .setDefaultRequestConfig(RequestConfig.custom()
351                                                        .setSocketTimeout(httpSocketTimeout)
352                                                        .build())
353                                        .build();
354                }
355
356                HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
357
358                RestTemplate restTemplate;
359
360                if (SECRET_BASIC.equals(clientConfig.getTokenEndpointAuthMethod())){
361                        // use BASIC auth if configured to do so
362                        restTemplate = new RestTemplate(factory) {
363
364                                @Override
365                                protected ClientHttpRequest createRequest(URI url, HttpMethod method) throws IOException {
366                                        ClientHttpRequest httpRequest = super.createRequest(url, method);
367                                        httpRequest.getHeaders().add("Authorization",
368                                                        String.format("Basic %s", Base64.encode(String.format("%s:%s",
369                                                                        UriUtils.encodePathSegment(clientConfig.getClientId(), "UTF-8"),
370                                                                        UriUtils.encodePathSegment(clientConfig.getClientSecret(), "UTF-8")))));
371
372                                        return httpRequest;
373                                }
374                        };
375                } else {
376                        // we're not doing basic auth, figure out what other flavor we have
377                        restTemplate = new RestTemplate(factory);
378
379                        if (SECRET_JWT.equals(clientConfig.getTokenEndpointAuthMethod()) || PRIVATE_KEY.equals(clientConfig.getTokenEndpointAuthMethod())) {
380                                // do a symmetric secret signed JWT for auth
381
382
383                                JWTSigningAndValidationService signer = null;
384                                JWSAlgorithm alg = clientConfig.getTokenEndpointAuthSigningAlg();
385
386                                if (SECRET_JWT.equals(clientConfig.getTokenEndpointAuthMethod()) &&
387                                                (JWSAlgorithm.HS256.equals(alg)
388                                                                || JWSAlgorithm.HS384.equals(alg)
389                                                                || JWSAlgorithm.HS512.equals(alg))) {
390
391                                        // generate one based on client secret
392                                        signer = symmetricCacheService.getSymmetricValidtor(clientConfig.getClient());
393
394                                } else if (PRIVATE_KEY.equals(clientConfig.getTokenEndpointAuthMethod())) {
395
396                                        // needs to be wired in to the bean
397                                        signer = authenticationSignerService;
398
399                                        if (alg == null) {
400                                                alg = authenticationSignerService.getDefaultSigningAlgorithm();
401                                        }
402                                }
403
404                                if (signer == null) {
405                                        throw new AuthenticationServiceException("Couldn't find required signer service for use with private key auth.");
406                                }
407
408                                JWTClaimsSet.Builder claimsSet = new JWTClaimsSet.Builder();
409
410                                claimsSet.issuer(clientConfig.getClientId());
411                                claimsSet.subject(clientConfig.getClientId());
412                                claimsSet.audience(Lists.newArrayList(serverConfig.getTokenEndpointUri()));
413                                claimsSet.jwtID(UUID.randomUUID().toString());
414
415                                // TODO: make this configurable
416                                Date exp = new Date(System.currentTimeMillis() + (60 * 1000)); // auth good for 60 seconds
417                                claimsSet.expirationTime(exp);
418
419                                Date now = new Date(System.currentTimeMillis());
420                                claimsSet.issueTime(now);
421                                claimsSet.notBeforeTime(now);
422
423                                JWSHeader header = new JWSHeader(alg, null, null, null, null, null, null, null, null, null,
424                                                signer.getDefaultSignerKeyId(),
425                                                null, null);
426                                SignedJWT jwt = new SignedJWT(header, claimsSet.build());
427
428                                signer.signJwt(jwt, alg);
429
430                                form.add("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer");
431                                form.add("client_assertion", jwt.serialize());
432                        } else {
433                                //Alternatively use form based auth
434                                form.add("client_id", clientConfig.getClientId());
435                                form.add("client_secret", clientConfig.getClientSecret());
436                        }
437
438                }
439
440                logger.debug("tokenEndpointURI = " + serverConfig.getTokenEndpointUri());
441                logger.debug("form = " + form);
442
443                String jsonString = null;
444
445                try {
446                        jsonString = restTemplate.postForObject(serverConfig.getTokenEndpointUri(), form, String.class);
447                } catch (RestClientException e) {
448
449                        // Handle error
450
451                        logger.error("Token Endpoint error response:  " + e.getMessage());
452
453                        throw new AuthenticationServiceException("Unable to obtain Access Token: " + e.getMessage());
454                }
455
456                logger.debug("from TokenEndpoint jsonString = " + jsonString);
457
458                JsonElement jsonRoot = new JsonParser().parse(jsonString);
459                if (!jsonRoot.isJsonObject()) {
460                        throw new AuthenticationServiceException("Token Endpoint did not return a JSON object: " + jsonRoot);
461                }
462
463                JsonObject tokenResponse = jsonRoot.getAsJsonObject();
464
465                if (tokenResponse.get("error") != null) {
466
467                        // Handle error
468
469                        String error = tokenResponse.get("error").getAsString();
470
471                        logger.error("Token Endpoint returned: " + error);
472
473                        throw new AuthenticationServiceException("Unable to obtain Access Token.  Token Endpoint returned: " + error);
474
475                } else {
476
477                        // Extract the id_token to insert into the
478                        // OIDCAuthenticationToken
479
480                        // get out all the token strings
481                        String accessTokenValue = null;
482                        String idTokenValue = null;
483                        String refreshTokenValue = null;
484
485                        if (tokenResponse.has("access_token")) {
486                                accessTokenValue = tokenResponse.get("access_token").getAsString();
487                        } else {
488                                throw new AuthenticationServiceException("Token Endpoint did not return an access_token: " + jsonString);
489                        }
490
491                        if (tokenResponse.has("id_token")) {
492                                idTokenValue = tokenResponse.get("id_token").getAsString();
493                        } else {
494                                logger.error("Token Endpoint did not return an id_token");
495                                throw new AuthenticationServiceException("Token Endpoint did not return an id_token");
496                        }
497
498                        if (tokenResponse.has("refresh_token")) {
499                                refreshTokenValue = tokenResponse.get("refresh_token").getAsString();
500                        }
501
502                        try {
503                                JWT idToken = JWTParser.parse(idTokenValue);
504
505                                // validate our ID Token over a number of tests
506                                JWTClaimsSet idClaims = idToken.getJWTClaimsSet();
507
508                                // check the signature
509                                JWTSigningAndValidationService jwtValidator = null;
510
511                                Algorithm tokenAlg = idToken.getHeader().getAlgorithm();
512
513                                Algorithm clientAlg = clientConfig.getIdTokenSignedResponseAlg();
514
515                                if (clientAlg != null) {
516                                        if (!clientAlg.equals(tokenAlg)) {
517                                                throw new AuthenticationServiceException("Token algorithm " + tokenAlg + " does not match expected algorithm " + clientAlg);
518                                        }
519                                }
520
521                                if (idToken instanceof PlainJWT) {
522
523                                        if (clientAlg == null) {
524                                                throw new AuthenticationServiceException("Unsigned ID tokens can only be used if explicitly configured in client.");
525                                        }
526
527                                        if (tokenAlg != null && !tokenAlg.equals(Algorithm.NONE)) {
528                                                throw new AuthenticationServiceException("Unsigned token received, expected signature with " + tokenAlg);
529                                        }
530                                } else if (idToken instanceof SignedJWT) {
531
532                                        SignedJWT signedIdToken = (SignedJWT)idToken;
533
534                                        if (tokenAlg.equals(JWSAlgorithm.HS256)
535                                                        || tokenAlg.equals(JWSAlgorithm.HS384)
536                                                        || tokenAlg.equals(JWSAlgorithm.HS512)) {
537
538                                                // generate one based on client secret
539                                                jwtValidator = symmetricCacheService.getSymmetricValidtor(clientConfig.getClient());
540                                        } else {
541                                                // otherwise load from the server's public key
542                                                jwtValidator = validationServices.getValidator(serverConfig.getJwksUri());
543                                        }
544
545                                        if (jwtValidator != null) {
546                                                if(!jwtValidator.validateSignature(signedIdToken)) {
547                                                        throw new AuthenticationServiceException("Signature validation failed");
548                                                }
549                                        } else {
550                                                logger.error("No validation service found. Skipping signature validation");
551                                                throw new AuthenticationServiceException("Unable to find an appropriate signature validator for ID Token.");
552                                        }
553                                } // TODO: encrypted id tokens
554
555                                // check the issuer
556                                if (idClaims.getIssuer() == null) {
557                                        throw new AuthenticationServiceException("Id Token Issuer is null");
558                                } else if (!idClaims.getIssuer().equals(serverConfig.getIssuer())){
559                                        throw new AuthenticationServiceException("Issuers do not match, expected " + serverConfig.getIssuer() + " got " + idClaims.getIssuer());
560                                }
561
562                                // check expiration
563                                if (idClaims.getExpirationTime() == null) {
564                                        throw new AuthenticationServiceException("Id Token does not have required expiration claim");
565                                } else {
566                                        // it's not null, see if it's expired
567                                        Date now = new Date(System.currentTimeMillis() - (timeSkewAllowance * 1000));
568                                        if (now.after(idClaims.getExpirationTime())) {
569                                                throw new AuthenticationServiceException("Id Token is expired: " + idClaims.getExpirationTime());
570                                        }
571                                }
572
573                                // check not before
574                                if (idClaims.getNotBeforeTime() != null) {
575                                        Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
576                                        if (now.before(idClaims.getNotBeforeTime())){
577                                                throw new AuthenticationServiceException("Id Token not valid untill: " + idClaims.getNotBeforeTime());
578                                        }
579                                }
580
581                                // check issued at
582                                if (idClaims.getIssueTime() == null) {
583                                        throw new AuthenticationServiceException("Id Token does not have required issued-at claim");
584                                } else {
585                                        // since it's not null, see if it was issued in the future
586                                        Date now = new Date(System.currentTimeMillis() + (timeSkewAllowance * 1000));
587                                        if (now.before(idClaims.getIssueTime())) {
588                                                throw new AuthenticationServiceException("Id Token was issued in the future: " + idClaims.getIssueTime());
589                                        }
590                                }
591
592                                // check audience
593                                if (idClaims.getAudience() == null) {
594                                        throw new AuthenticationServiceException("Id token audience is null");
595                                } else if (!idClaims.getAudience().contains(clientConfig.getClientId())) {
596                                        throw new AuthenticationServiceException("Audience does not match, expected " + clientConfig.getClientId() + " got " + idClaims.getAudience());
597                                }
598
599                                // compare the nonce to our stored claim
600                                String nonce = idClaims.getStringClaim("nonce");
601                                if (Strings.isNullOrEmpty(nonce)) {
602
603                                        logger.error("ID token did not contain a nonce claim.");
604
605                                        throw new AuthenticationServiceException("ID token did not contain a nonce claim.");
606                                }
607
608                                String storedNonce = getStoredNonce(session);
609                                if (!nonce.equals(storedNonce)) {
610                                        logger.error("Possible replay attack detected! The comparison of the nonce in the returned "
611                                                        + "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
612
613                                        throw new AuthenticationServiceException(
614                                                        "Possible replay attack detected! The comparison of the nonce in the returned "
615                                                                        + "ID Token to the session " + NONCE_SESSION_VARIABLE + " failed. Expected " + storedNonce + " got " + nonce + ".");
616                                }
617
618                                // construct an PendingOIDCAuthenticationToken and return a Authentication object w/the userId and the idToken
619
620                                PendingOIDCAuthenticationToken token = new PendingOIDCAuthenticationToken(idClaims.getSubject(), idClaims.getIssuer(),
621                                                serverConfig,
622                                                idToken, accessTokenValue, refreshTokenValue);
623
624                                Authentication authentication = this.getAuthenticationManager().authenticate(token);
625
626                                return authentication;
627                        } catch (ParseException e) {
628                                throw new AuthenticationServiceException("Couldn't parse idToken: ", e);
629                        }
630
631
632
633                }
634        }
635
636        /**
637         * Handle Authorization Endpoint error
638         *
639         * @param request
640         *            The request from which to extract parameters and handle the
641         *            error
642         * @param response
643         *            The response, needed to do a redirect to display the error
644         * @throws IOException
645         *             If an input or output exception occurs
646         */
647        protected void handleError(HttpServletRequest request, HttpServletResponse response) throws IOException {
648
649                String error = request.getParameter("error");
650                String errorDescription = request.getParameter("error_description");
651                String errorURI = request.getParameter("error_uri");
652
653                throw new AuthorizationEndpointException(error, errorDescription, errorURI);
654        }
655
656        /**
657         * Get the named stored session variable as a string. Return null if not found or not a string.
658         * @param session
659         * @param key
660         * @return
661         */
662        private static String getStoredSessionString(HttpSession session, String key) {
663                Object o = session.getAttribute(key);
664                if (o != null && o instanceof String) {
665                        return o.toString();
666                } else {
667                        return null;
668                }
669        }
670
671        /**
672         * Create a cryptographically random nonce and store it in the session
673         * @param session
674         * @return
675         */
676        protected static String createNonce(HttpSession session) {
677                String nonce = new BigInteger(50, new SecureRandom()).toString(16);
678                session.setAttribute(NONCE_SESSION_VARIABLE, nonce);
679
680                return nonce;
681        }
682
683        /**
684         * Get the nonce we stored in the session
685         * @param session
686         * @return
687         */
688        protected static String getStoredNonce(HttpSession session) {
689                return getStoredSessionString(session, NONCE_SESSION_VARIABLE);
690        }
691
692        /**
693         * Create a cryptographically random state and store it in the session
694         * @param session
695         * @return
696         */
697        protected static String createState(HttpSession session) {
698                String state = new BigInteger(50, new SecureRandom()).toString(16);
699                session.setAttribute(STATE_SESSION_VARIABLE, state);
700
701                return state;
702        }
703
704        /**
705         * Get the state we stored in the session
706         * @param session
707         * @return
708         */
709        protected static String getStoredState(HttpSession session) {
710                return getStoredSessionString(session, STATE_SESSION_VARIABLE);
711        }
712
713        /**
714         * Create a random code challenge and store it in the session
715         * @param session
716         * @return
717         */
718        protected static String createCodeVerifier(HttpSession session) {
719                String challenge = new BigInteger(50, new SecureRandom()).toString(16);
720                session.setAttribute(CODE_VERIFIER_SESSION_VARIABLE, challenge);
721                return challenge;
722        }
723
724        /**
725         * Retrieve the stored challenge from our session
726         * @param session
727         * @return
728         */
729        protected static String getStoredCodeVerifier(HttpSession session) {
730                return getStoredSessionString(session, CODE_VERIFIER_SESSION_VARIABLE);
731        }
732
733
734        @Override
735        public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
736                targetSuccessHandler.passthrough = successHandler;
737                super.setAuthenticationSuccessHandler(targetSuccessHandler);
738        }
739
740
741
742
743        /**
744         * Handle a successful authentication event. If the issuer service sets
745         * a target URL, we'll go to that. Otherwise we'll let the superclass handle
746         * it for us with the configured behavior.
747         */
748        protected class TargetLinkURIAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
749
750                private AuthenticationSuccessHandler passthrough;
751
752                @Override
753                public void onAuthenticationSuccess(HttpServletRequest request,
754                                HttpServletResponse response, Authentication authentication)
755                                                throws IOException, ServletException {
756
757                        HttpSession session = request.getSession();
758
759                        // check to see if we've got a target
760                        String target = getStoredSessionString(session, TARGET_SESSION_VARIABLE);
761
762                        if (!Strings.isNullOrEmpty(target)) {
763                                session.removeAttribute(TARGET_SESSION_VARIABLE);
764
765                                if (deepLinkFilter != null) {
766                                        target = deepLinkFilter.filter(target);
767                                }
768
769                                response.sendRedirect(target);
770                        } else {
771                                // if the target was blank, use the default behavior here
772                                passthrough.onAuthenticationSuccess(request, response, authentication);
773                        }
774
775                }
776
777        }
778
779
780        //
781        // Getters and setters for configuration variables
782        //
783
784
785        public int getTimeSkewAllowance() {
786                return timeSkewAllowance;
787        }
788
789        public void setTimeSkewAllowance(int timeSkewAllowance) {
790                this.timeSkewAllowance = timeSkewAllowance;
791        }
792
793        /**
794         * @return the validationServices
795         */
796        public JWKSetCacheService getValidationServices() {
797                return validationServices;
798        }
799
800        /**
801         * @param validationServices the validationServices to set
802         */
803        public void setValidationServices(JWKSetCacheService validationServices) {
804                this.validationServices = validationServices;
805        }
806
807        /**
808         * @return the servers
809         */
810        public ServerConfigurationService getServerConfigurationService() {
811                return servers;
812        }
813
814        /**
815         * @param servers the servers to set
816         */
817        public void setServerConfigurationService(ServerConfigurationService servers) {
818                this.servers = servers;
819        }
820
821        /**
822         * @return the clients
823         */
824        public ClientConfigurationService getClientConfigurationService() {
825                return clients;
826        }
827
828        /**
829         * @param clients the clients to set
830         */
831        public void setClientConfigurationService(ClientConfigurationService clients) {
832                this.clients = clients;
833        }
834
835        /**
836         * @return the issuerService
837         */
838        public IssuerService getIssuerService() {
839                return issuerService;
840        }
841
842        /**
843         * @param issuerService the issuerService to set
844         */
845        public void setIssuerService(IssuerService issuerService) {
846                this.issuerService = issuerService;
847        }
848
849        /**
850         * @return the authRequestBuilder
851         */
852        public AuthRequestUrlBuilder getAuthRequestUrlBuilder() {
853                return authRequestBuilder;
854        }
855
856        /**
857         * @param authRequestBuilder the authRequestBuilder to set
858         */
859        public void setAuthRequestUrlBuilder(AuthRequestUrlBuilder authRequestBuilder) {
860                this.authRequestBuilder = authRequestBuilder;
861        }
862
863        /**
864         * @return the authOptions
865         */
866        public AuthRequestOptionsService getAuthRequestOptionsService() {
867                return authOptions;
868        }
869
870        /**
871         * @param authOptions the authOptions to set
872         */
873        public void setAuthRequestOptionsService(AuthRequestOptionsService authOptions) {
874                this.authOptions = authOptions;
875        }
876
877        public SymmetricKeyJWTValidatorCacheService getSymmetricCacheService() {
878                return symmetricCacheService;
879        }
880
881        public void setSymmetricCacheService(SymmetricKeyJWTValidatorCacheService symmetricCacheService) {
882                this.symmetricCacheService = symmetricCacheService;
883        }
884
885        public TargetLinkURIAuthenticationSuccessHandler getTargetLinkURIAuthenticationSuccessHandler() {
886                return targetSuccessHandler;
887        }
888
889        public void setTargetLinkURIAuthenticationSuccessHandler(
890                        TargetLinkURIAuthenticationSuccessHandler targetSuccessHandler) {
891                this.targetSuccessHandler = targetSuccessHandler;
892        }
893
894        public TargetLinkURIChecker targetLinkURIChecker() {
895                return deepLinkFilter;
896        }
897
898        public void setTargetLinkURIChecker(TargetLinkURIChecker deepLinkFilter) {
899                this.deepLinkFilter = deepLinkFilter;
900        }
901
902}