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