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