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}