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