001/* 002 * oauth2-oidc-sdk 003 * 004 * Copyright 2012-2016, Connect2id Ltd and contributors. 005 * 006 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use 007 * this file except in compliance with the License. You may obtain a copy of the 008 * 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 distributed 013 * under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 014 * CONDITIONS OF ANY KIND, either express or implied. See the License for the 015 * specific language governing permissions and limitations under the License. 016 */ 017 018package com.nimbusds.oauth2.sdk; 019 020 021import java.net.MalformedURLException; 022import java.net.URI; 023import java.net.URISyntaxException; 024import java.util.List; 025import java.util.Map; 026 027import com.nimbusds.common.contenttype.ContentType; 028import com.nimbusds.jwt.JWT; 029import com.nimbusds.jwt.JWTClaimsSet; 030import com.nimbusds.oauth2.sdk.http.HTTPRequest; 031import com.nimbusds.oauth2.sdk.http.HTTPResponse; 032import com.nimbusds.oauth2.sdk.id.Issuer; 033import com.nimbusds.oauth2.sdk.id.State; 034import com.nimbusds.oauth2.sdk.jarm.JARMUtils; 035import com.nimbusds.oauth2.sdk.jarm.JARMValidator; 036import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 037import com.nimbusds.oauth2.sdk.util.StringUtils; 038import com.nimbusds.oauth2.sdk.util.URIUtils; 039import com.nimbusds.oauth2.sdk.util.URLUtils; 040 041 042/** 043 * The base abstract class for authorisation success and error responses. 044 * 045 * <p>Related specifications: 046 * 047 * <ul> 048 * <li>OAuth 2.0 (RFC 6749), section 3.1. 049 * <li>OAuth 2.0 Multiple Response Type Encoding Practices 1.0. 050 * <li>OAuth 2.0 Form Post Response Mode 1.0. 051 * <li>Financial-grade API: JWT Secured Authorization Response Mode for 052 * OAuth 2.0 (JARM). 053 * <li>OAuth 2.0 Authorization Server Issuer Identifier in Authorization 054 * Response (draft-meyerzuselhausen-oauth-iss-auth-resp-01). 055 * </ul> 056 */ 057public abstract class AuthorizationResponse implements Response { 058 059 060 /** 061 * The base redirection URI. 062 */ 063 private final URI redirectURI; 064 065 066 /** 067 * The optional state parameter to be echoed back to the client. 068 */ 069 private final State state; 070 071 072 /** 073 * Optional issuer. 074 */ 075 private final Issuer issuer; 076 077 078 /** 079 * For a JWT-secured response. 080 */ 081 private final JWT jwtResponse; 082 083 084 /** 085 * The optional explicit response mode. 086 */ 087 private final ResponseMode rm; 088 089 090 /** 091 * Creates a new authorisation response. 092 * 093 * @param redirectURI The base redirection URI. Must not be 094 * {@code null}. 095 * @param state The state, {@code null} if not requested. 096 * @param issuer The issuer, {@code null} if not specified. 097 * @param rm The response mode, {@code null} if not specified. 098 */ 099 protected AuthorizationResponse(final URI redirectURI, 100 final State state, 101 final Issuer issuer, 102 final ResponseMode rm) { 103 104 if (redirectURI == null) { 105 throw new IllegalArgumentException("The redirection URI must not be null"); 106 } 107 108 this.redirectURI = redirectURI; 109 110 jwtResponse = null; 111 112 this.state = state; 113 114 this.issuer = issuer; 115 116 this.rm = rm; 117 } 118 119 120 /** 121 * Creates a new JSON Web Token (JWT) secured authorisation response. 122 * 123 * @param redirectURI The base redirection URI. Must not be 124 * {@code null}. 125 * @param jwtResponse The JWT response. Must not be {@code null}. 126 * @param rm The response mode, {@code null} if not specified. 127 */ 128 protected AuthorizationResponse(final URI redirectURI, final JWT jwtResponse, final ResponseMode rm) { 129 130 if (redirectURI == null) { 131 throw new IllegalArgumentException("The redirection URI must not be null"); 132 } 133 134 this.redirectURI = redirectURI; 135 136 if (jwtResponse == null) { 137 throw new IllegalArgumentException("The JWT response must not be null"); 138 } 139 140 this.jwtResponse = jwtResponse; 141 142 this.state = null; 143 144 this.issuer = null; 145 146 this.rm = rm; 147 } 148 149 150 /** 151 * Returns the base redirection URI. 152 * 153 * @return The base redirection URI (without the appended error 154 * response parameters). 155 */ 156 public URI getRedirectionURI() { 157 158 return redirectURI; 159 } 160 161 162 /** 163 * Returns the optional state. 164 * 165 * @return The state, {@code null} if not requested or if the response 166 * is JWT-secured in which case the state parameter may be 167 * included as a JWT claim. 168 */ 169 public State getState() { 170 171 return state; 172 } 173 174 175 /** 176 * Returns the optional issuer. 177 * 178 * @return The issuer, {@code null} if not specified. 179 */ 180 public Issuer getIssuer() { 181 182 return issuer; 183 } 184 185 186 /** 187 * Returns the JSON Web Token (JWT) secured response. 188 * 189 * @return The JWT-secured response, {@code null} for a regular 190 * authorisation response. 191 */ 192 public JWT getJWTResponse() { 193 194 return jwtResponse; 195 } 196 197 198 /** 199 * Returns the optional explicit response mode. 200 * 201 * @return The response mode, {@code null} if not specified. 202 */ 203 public ResponseMode getResponseMode() { 204 205 return rm; 206 } 207 208 209 /** 210 * Determines the implied response mode. 211 * 212 * @return The implied response mode. 213 */ 214 public abstract ResponseMode impliedResponseMode(); 215 216 217 /** 218 * Returns the parameters of this authorisation response. 219 * 220 * <p>Example parameters (authorisation success): 221 * 222 * <pre> 223 * access_token = 2YotnFZFEjr1zCsicMWpAA 224 * state = xyz 225 * token_type = example 226 * expires_in = 3600 227 * </pre> 228 * 229 * @return The parameters as a map. 230 */ 231 public abstract Map<String,List<String>> toParameters(); 232 233 234 /** 235 * Returns a URI representation (redirection URI + fragment / query 236 * string) of this authorisation response. 237 * 238 * <p>Example URI: 239 * 240 * <pre> 241 * http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA 242 * &state=xyz 243 * &token_type=example 244 * &expires_in=3600 245 * </pre> 246 * 247 * @return A URI representation of this authorisation response. 248 */ 249 public URI toURI() { 250 251 final ResponseMode rm = impliedResponseMode(); 252 253 StringBuilder sb = new StringBuilder(getRedirectionURI().toString()); 254 255 String serializedParameters = URLUtils.serializeParameters(toParameters()); 256 257 if (StringUtils.isNotBlank(serializedParameters)) { 258 259 if (ResponseMode.QUERY.equals(rm) || ResponseMode.QUERY_JWT.equals(rm)) { 260 if (getRedirectionURI().toString().endsWith("?")) { 261 // '?' present 262 } else if (StringUtils.isBlank(getRedirectionURI().getRawQuery())) { 263 sb.append('?'); 264 } else { 265 // The original redirect_uri may contain query params, 266 // see http://tools.ietf.org/html/rfc6749#section-3.1.2 267 sb.append('&'); 268 } 269 } else if (ResponseMode.FRAGMENT.equals(rm) || ResponseMode.FRAGMENT_JWT.equals(rm)) { 270 sb.append('#'); 271 } else { 272 throw new SerializeException("The (implied) response mode must be query or fragment"); 273 } 274 275 sb.append(serializedParameters); 276 } 277 278 try { 279 return new URI(sb.toString()); 280 } catch (URISyntaxException e) { 281 throw new SerializeException("Couldn't serialize response: " + e.getMessage(), e); 282 } 283 } 284 285 286 /** 287 * Returns an HTTP response for this authorisation response. Applies to 288 * the {@code query} or {@code fragment} response mode using HTTP 302 289 * redirection. 290 * 291 * <p>Example HTTP response (authorisation success): 292 * 293 * <pre> 294 * HTTP/1.1 302 Found 295 * Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA 296 * &state=xyz 297 * &token_type=example 298 * &expires_in=3600 299 * </pre> 300 * 301 * @see #toHTTPRequest() 302 * 303 * @return An HTTP response for this authorisation response. 304 */ 305 @Override 306 public HTTPResponse toHTTPResponse() { 307 308 if (ResponseMode.FORM_POST.equals(rm)) { 309 throw new SerializeException("The response mode must not be form_post"); 310 } 311 312 HTTPResponse response= new HTTPResponse(HTTPResponse.SC_FOUND); 313 response.setLocation(toURI()); 314 return response; 315 } 316 317 318 /** 319 * Returns an HTTP request for this authorisation response. Applies to 320 * the {@code form_post} response mode. 321 * 322 * <p>Example HTTP request (authorisation success): 323 * 324 * <pre> 325 * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz HTTP/1.1 326 * Host: client.example.com 327 * </pre> 328 * 329 * @see #toHTTPResponse() 330 * 331 * @return An HTTP request for this authorisation response. 332 */ 333 public HTTPRequest toHTTPRequest() { 334 335 if (! ResponseMode.FORM_POST.equals(rm)) { 336 throw new SerializeException("The response mode must be form_post"); 337 } 338 339 // Use HTTP POST 340 HTTPRequest request; 341 342 try { 343 request = new HTTPRequest(HTTPRequest.Method.POST, redirectURI.toURL()); 344 345 } catch (MalformedURLException e) { 346 throw new SerializeException(e.getMessage(), e); 347 } 348 349 request.setEntityContentType(ContentType.APPLICATION_URLENCODED); 350 request.setQuery(URLUtils.serializeParameters(toParameters())); 351 return request; 352 } 353 354 355 /** 356 * Casts this response to an authorisation success response. 357 * 358 * @return The authorisation success response. 359 */ 360 public AuthorizationSuccessResponse toSuccessResponse() { 361 362 return (AuthorizationSuccessResponse) this; 363 } 364 365 366 /** 367 * Casts this response to an authorisation error response. 368 * 369 * @return The authorisation error response. 370 */ 371 public AuthorizationErrorResponse toErrorResponse() { 372 373 return (AuthorizationErrorResponse) this; 374 } 375 376 377 /** 378 * Parses an authorisation response. 379 * 380 * @param redirectURI The base redirection URI. Must not be 381 * {@code null}. 382 * @param params The response parameters to parse. Must not be 383 * {@code null}. 384 * 385 * @return The authorisation success or error response. 386 * 387 * @throws ParseException If the parameters couldn't be parsed to an 388 * authorisation success or error response. 389 */ 390 public static AuthorizationResponse parse(final URI redirectURI, final Map<String,List<String>> params) 391 throws ParseException { 392 393 return parse(redirectURI, params, null); 394 } 395 396 397 /** 398 * Parses an authorisation response which may be JSON Web Token (JWT) 399 * secured. 400 * 401 * @param redirectURI The base redirection URI. Must not be 402 * {@code null}. 403 * @param params The response parameters to parse. Must not be 404 * {@code null}. 405 * @param jarmValidator The validator of JSON Web Token (JWT) secured 406 * authorisation responses (JARM), {@code null} if 407 * a plain response is expected. 408 * 409 * @return The authorisation success or error response. 410 * 411 * @throws ParseException If the parameters couldn't be parsed to an 412 * authorisation success or error response, or 413 * if validation of the JWT secured response 414 * failed. 415 */ 416 public static AuthorizationResponse parse(final URI redirectURI, 417 final Map<String,List<String>> params, 418 final JARMValidator jarmValidator) 419 throws ParseException { 420 421 Map<String,List<String>> workParams = params; 422 423 String jwtResponseString = MultivaluedMapUtils.getFirstValue(params, "response"); 424 425 if (jarmValidator != null) { 426 if (StringUtils.isBlank(jwtResponseString)) { 427 throw new ParseException("Missing JWT-secured (JARM) authorization response parameter"); 428 } 429 try { 430 JWTClaimsSet jwtClaimsSet = jarmValidator.validate(jwtResponseString); 431 workParams = JARMUtils.toMultiValuedStringParameters(jwtClaimsSet); 432 } catch (Exception e) { 433 throw new ParseException("Invalid JWT-secured (JARM) authorization response: " + e.getMessage()); 434 } 435 } 436 437 if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(workParams, "error"))) { 438 return AuthorizationErrorResponse.parse(redirectURI, workParams); 439 } else if (StringUtils.isNotBlank(jwtResponseString)) { 440 // JARM that wasn't validated, peek into JWT if signed only 441 boolean likelyError = JARMUtils.impliesAuthorizationErrorResponse(jwtResponseString); 442 if (likelyError) { 443 return AuthorizationErrorResponse.parse(redirectURI, workParams); 444 } else { 445 return AuthorizationSuccessResponse.parse(redirectURI, workParams); 446 } 447 448 } else { 449 return AuthorizationSuccessResponse.parse(redirectURI, workParams); 450 } 451 } 452 453 454 /** 455 * Parses an authorisation response. 456 * 457 * <p>Use a relative URI if the host, port and path details are not 458 * known: 459 * 460 * <pre> 461 * URI relUrl = new URI("https:///?code=Qcb0Orv1...&state=af0ifjsldkj"); 462 * </pre> 463 * 464 * @param uri The URI to parse. Can be absolute or relative, with a 465 * fragment or query string containing the authorisation 466 * response parameters. Must not be {@code null}. 467 * 468 * @return The authorisation success or error response. 469 * 470 * @throws ParseException If no authorisation response parameters were 471 * found in the URL. 472 */ 473 public static AuthorizationResponse parse(final URI uri) 474 throws ParseException { 475 476 return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri)); 477 } 478 479 480 /** 481 * Parses and validates a JSON Web Token (JWT) secured authorisation 482 * response. 483 * 484 * <p>Use a relative URI if the host, port and path details are not 485 * known: 486 * 487 * <pre> 488 * URI relUrl = new URI("https:///?response=eyJhbGciOiJSUzI1NiIsI..."); 489 * </pre> 490 * 491 * @param uri The URI to parse. Can be absolute or relative, 492 * with a fragment or query string containing the 493 * authorisation response parameters. Must not be 494 * {@code null}. 495 * @param jarmValidator The validator of JSON Web Token (JWT) secured 496 * authorisation responses (JARM). Must not be 497 * {@code null}. 498 * 499 * @return The authorisation success or error response. 500 * 501 * @throws ParseException If no authorisation response parameters were 502 * found in the URL of if validation of the JWT 503 * response failed. 504 */ 505 public static AuthorizationResponse parse(final URI uri, final JARMValidator jarmValidator) 506 throws ParseException { 507 508 if (jarmValidator == null) { 509 throw new IllegalArgumentException("The JARM validator must not be null"); 510 } 511 512 return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri), jarmValidator); 513 } 514 515 516 /** 517 * Parses an authorisation response from the specified initial HTTP 302 518 * redirect response output at the authorisation endpoint. 519 * 520 * <p>Example HTTP response (authorisation success): 521 * 522 * <pre> 523 * HTTP/1.1 302 Found 524 * Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz 525 * </pre> 526 * 527 * @see #parse(HTTPRequest) 528 * 529 * @param httpResponse The HTTP response to parse. Must not be 530 * {@code null}. 531 * 532 * @return The authorisation response. 533 * 534 * @throws ParseException If the HTTP response couldn't be parsed to an 535 * authorisation response. 536 */ 537 public static AuthorizationResponse parse(final HTTPResponse httpResponse) 538 throws ParseException { 539 540 URI location = httpResponse.getLocation(); 541 542 if (location == null) { 543 throw new ParseException("Missing redirection URI / HTTP Location header"); 544 } 545 546 return parse(location); 547 } 548 549 550 /** 551 * Parses and validates a JSON Web Token (JWT) secured authorisation 552 * response from the specified initial HTTP 302 redirect response 553 * output at the authorisation endpoint. 554 * 555 * <p>Example HTTP response (authorisation success): 556 * 557 * <pre> 558 * HTTP/1.1 302 Found 559 * Location: https://client.example.com/cb?response=eyJhbGciOiJSUzI1... 560 * </pre> 561 * 562 * @see #parse(HTTPRequest) 563 * 564 * @param httpResponse The HTTP response to parse. Must not be 565 * {@code null}. 566 * @param jarmValidator The validator of JSON Web Token (JWT) secured 567 * authorisation responses (JARM). Must not be 568 * {@code null}. 569 * 570 * @return The authorisation response. 571 * 572 * @throws ParseException If the HTTP response couldn't be parsed to an 573 * authorisation response or if validation of 574 * the JWT response failed. 575 */ 576 public static AuthorizationResponse parse(final HTTPResponse httpResponse, 577 final JARMValidator jarmValidator) 578 throws ParseException { 579 580 URI location = httpResponse.getLocation(); 581 582 if (location == null) { 583 throw new ParseException("Missing redirection URI / HTTP Location header"); 584 } 585 586 return parse(location, jarmValidator); 587 } 588 589 590 /** 591 * Parses an authorisation response from the specified HTTP request at 592 * the client redirection (callback) URI. Applies to the {@code query}, 593 * {@code fragment} and {@code form_post} response modes. 594 * 595 * <p>Example HTTP request (authorisation success): 596 * 597 * <pre> 598 * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz HTTP/1.1 599 * Host: client.example.com 600 * </pre> 601 * 602 * @see #parse(HTTPResponse) 603 * 604 * @param httpRequest The HTTP request to parse. Must not be 605 * {@code null}. 606 * 607 * @return The authorisation response. 608 * 609 * @throws ParseException If the HTTP request couldn't be parsed to an 610 * authorisation response. 611 */ 612 public static AuthorizationResponse parse(final HTTPRequest httpRequest) 613 throws ParseException { 614 615 return parse(httpRequest.getURI(), parseResponseParameters(httpRequest)); 616 } 617 618 619 /** 620 * Parses and validates a JSON Web Token (JWT) secured authorisation 621 * response from the specified HTTP request at the client redirection 622 * (callback) URI. Applies to the {@code query.jwt}, 623 * {@code fragment.jwt} and {@code form_post.jwt} response modes. 624 * 625 * <p>Example HTTP request (authorisation success): 626 * 627 * <pre> 628 * GET /cb?response=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... HTTP/1.1 629 * Host: client.example.com 630 * </pre> 631 * 632 * @see #parse(HTTPResponse) 633 * 634 * @param httpRequest The HTTP request to parse. Must not be 635 * {@code null}. 636 * @param jarmValidator The validator of JSON Web Token (JWT) secured 637 * authorisation responses (JARM). Must not be 638 * {@code null}. 639 * 640 * @return The authorisation response. 641 * 642 * @throws ParseException If the HTTP request couldn't be parsed to an 643 * authorisation response or if validation of 644 * the JWT response failed. 645 */ 646 public static AuthorizationResponse parse(final HTTPRequest httpRequest, 647 final JARMValidator jarmValidator) 648 throws ParseException { 649 650 if (jarmValidator == null) { 651 throw new IllegalArgumentException("The JARM validator must not be null"); 652 } 653 654 return parse(httpRequest.getURI(), parseResponseParameters(httpRequest), jarmValidator); 655 } 656 657 658 /** 659 * Parses the relevant authorisation response parameters. This method 660 * is intended for internal SDK usage only. 661 * 662 * @param uri The URI to parse its query or fragment parameters. Must 663 * not be {@code null}. 664 * 665 * @return The authorisation response parameters. 666 * 667 * @throws ParseException If parsing failed. 668 */ 669 public static Map<String,List<String>> parseResponseParameters(final URI uri) 670 throws ParseException { 671 672 if (uri.getRawFragment() != null) { 673 return URLUtils.parseParameters(uri.getRawFragment()); 674 } else if (uri.getRawQuery() != null) { 675 return URLUtils.parseParameters(uri.getRawQuery()); 676 } else { 677 throw new ParseException("Missing URI fragment or query string"); 678 } 679 } 680 681 682 /** 683 * Parses the relevant authorisation response parameters. This method 684 * is intended for internal SDK usage only. 685 * 686 * @param httpRequest The HTTP request. Must not be {@code null}. 687 * 688 * @return The authorisation response parameters. 689 * 690 * @throws ParseException If parsing failed. 691 */ 692 public static Map<String,List<String>> parseResponseParameters(final HTTPRequest httpRequest) 693 throws ParseException { 694 695 if (httpRequest.getQuery() != null) { 696 // For query string and form_post response mode 697 return URLUtils.parseParameters(httpRequest.getQuery()); 698 } else if (httpRequest.getFragment() != null) { 699 // For fragment response mode (never available in actual HTTP request from browser) 700 return URLUtils.parseParameters(httpRequest.getFragment()); 701 } else { 702 throw new ParseException("Missing URI fragment, query string or post body"); 703 } 704 } 705}