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 String serializedParameters = URLUtils.serializeParameters(toParameters()); 228 229 if (StringUtils.isNotBlank(serializedParameters)) { 230 231 if (ResponseMode.QUERY.equals(rm) || ResponseMode.QUERY_JWT.equals(rm)) { 232 if (getRedirectionURI().toString().endsWith("?")) { 233 // '?' present 234 } else if (StringUtils.isBlank(getRedirectionURI().getRawQuery())) { 235 sb.append('?'); 236 } else { 237 // The original redirect_uri may contain query params, 238 // see http://tools.ietf.org/html/rfc6749#section-3.1.2 239 sb.append('&'); 240 } 241 } else if (ResponseMode.FRAGMENT.equals(rm) || ResponseMode.FRAGMENT_JWT.equals(rm)) { 242 sb.append('#'); 243 } else { 244 throw new SerializeException("The (implied) response mode must be query or fragment"); 245 } 246 247 sb.append(serializedParameters); 248 } 249 250 try { 251 return new URI(sb.toString()); 252 } catch (URISyntaxException e) { 253 throw new SerializeException("Couldn't serialize response: " + e.getMessage(), e); 254 } 255 } 256 257 258 /** 259 * Returns an HTTP response for this authorisation response. Applies to 260 * the {@code query} or {@code fragment} response mode using HTTP 302 261 * redirection. 262 * 263 * <p>Example HTTP response (authorisation success): 264 * 265 * <pre> 266 * HTTP/1.1 302 Found 267 * Location: http://example.com/cb#access_token=2YotnFZFEjr1zCsicMWpAA 268 * &state=xyz 269 * &token_type=example 270 * &expires_in=3600 271 * </pre> 272 * 273 * @see #toHTTPRequest() 274 * 275 * @return An HTTP response for this authorisation response. 276 */ 277 @Override 278 public HTTPResponse toHTTPResponse() { 279 280 if (ResponseMode.FORM_POST.equals(rm)) { 281 throw new SerializeException("The response mode must not be form_post"); 282 } 283 284 HTTPResponse response= new HTTPResponse(HTTPResponse.SC_FOUND); 285 response.setLocation(toURI()); 286 return response; 287 } 288 289 290 /** 291 * Returns an HTTP request for this authorisation response. Applies to 292 * the {@code form_post} response mode. 293 * 294 * <p>Example HTTP request (authorisation success): 295 * 296 * <pre> 297 * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz HTTP/1.1 298 * Host: client.example.com 299 * </pre> 300 * 301 * @see #toHTTPResponse() 302 * 303 * @return An HTTP request for this authorisation response. 304 */ 305 public HTTPRequest toHTTPRequest() { 306 307 if (! ResponseMode.FORM_POST.equals(rm)) { 308 throw new SerializeException("The response mode must be form_post"); 309 } 310 311 // Use HTTP POST 312 HTTPRequest request; 313 314 try { 315 request = new HTTPRequest(HTTPRequest.Method.POST, redirectURI.toURL()); 316 317 } catch (MalformedURLException e) { 318 throw new SerializeException(e.getMessage(), e); 319 } 320 321 request.setEntityContentType(ContentType.APPLICATION_URLENCODED); 322 request.setQuery(URLUtils.serializeParameters(toParameters())); 323 return request; 324 } 325 326 327 /** 328 * Casts this response to an authorisation success response. 329 * 330 * @return The authorisation success response. 331 */ 332 public AuthorizationSuccessResponse toSuccessResponse() { 333 334 return (AuthorizationSuccessResponse) this; 335 } 336 337 338 /** 339 * Casts this response to an authorisation error response. 340 * 341 * @return The authorisation error response. 342 */ 343 public AuthorizationErrorResponse toErrorResponse() { 344 345 return (AuthorizationErrorResponse) this; 346 } 347 348 349 /** 350 * Parses an authorisation response. 351 * 352 * @param redirectURI The base redirection URI. Must not be 353 * {@code null}. 354 * @param params The response parameters to parse. Must not be 355 * {@code null}. 356 * 357 * @return The authorisation success or error response. 358 * 359 * @throws ParseException If the parameters couldn't be parsed to an 360 * authorisation success or error response. 361 */ 362 public static AuthorizationResponse parse(final URI redirectURI, final Map<String,List<String>> params) 363 throws ParseException { 364 365 return parse(redirectURI, params, null); 366 } 367 368 369 /** 370 * Parses an authorisation response which may be JSON Web Token (JWT) 371 * secured. 372 * 373 * @param redirectURI The base redirection URI. Must not be 374 * {@code null}. 375 * @param params The response parameters to parse. Must not be 376 * {@code null}. 377 * @param jarmValidator The validator of JSON Web Token (JWT) secured 378 * authorisation responses (JARM), {@code null} if 379 * a plain response is expected. 380 * 381 * @return The authorisation success or error response. 382 * 383 * @throws ParseException If the parameters couldn't be parsed to an 384 * authorisation success or error response, or 385 * if validation of the JWT secured response 386 * failed. 387 */ 388 public static AuthorizationResponse parse(final URI redirectURI, 389 final Map<String,List<String>> params, 390 final JARMValidator jarmValidator) 391 throws ParseException { 392 393 Map<String,List<String>> workParams = params; 394 395 String jwtResponseString = MultivaluedMapUtils.getFirstValue(params, "response"); 396 397 if (jarmValidator != null) { 398 if (StringUtils.isBlank(jwtResponseString)) { 399 throw new ParseException("Missing JWT-secured (JARM) authorization response parameter"); 400 } 401 try { 402 JWTClaimsSet jwtClaimsSet = jarmValidator.validate(jwtResponseString); 403 workParams = JARMUtils.toMultiValuedStringParameters(jwtClaimsSet); 404 } catch (Exception e) { 405 throw new ParseException("Invalid JWT-secured (JARM) authorization response: " + e.getMessage()); 406 } 407 } 408 409 if (StringUtils.isNotBlank(MultivaluedMapUtils.getFirstValue(workParams, "error"))) { 410 return AuthorizationErrorResponse.parse(redirectURI, workParams); 411 } else if (StringUtils.isNotBlank(jwtResponseString)) { 412 // JARM that wasn't validated, peek into JWT if signed only 413 boolean likelyError = JARMUtils.impliesAuthorizationErrorResponse(jwtResponseString); 414 if (likelyError) { 415 return AuthorizationErrorResponse.parse(redirectURI, workParams); 416 } else { 417 return AuthorizationSuccessResponse.parse(redirectURI, workParams); 418 } 419 420 } else { 421 return AuthorizationSuccessResponse.parse(redirectURI, workParams); 422 } 423 } 424 425 426 /** 427 * Parses an authorisation response. 428 * 429 * <p>Use a relative URI if the host, port and path details are not 430 * known: 431 * 432 * <pre> 433 * URI relUrl = new URI("https:///?code=Qcb0Orv1...&state=af0ifjsldkj"); 434 * </pre> 435 * 436 * @param uri The URI to parse. Can be absolute or relative, with a 437 * fragment or query string containing the authorisation 438 * response parameters. Must not be {@code null}. 439 * 440 * @return The authorisation success or error response. 441 * 442 * @throws ParseException If no authorisation response parameters were 443 * found in the URL. 444 */ 445 public static AuthorizationResponse parse(final URI uri) 446 throws ParseException { 447 448 return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri)); 449 } 450 451 452 /** 453 * Parses and validates a JSON Web Token (JWT) secured authorisation 454 * response. 455 * 456 * <p>Use a relative URI if the host, port and path details are not 457 * known: 458 * 459 * <pre> 460 * URI relUrl = new URI("https:///?response=eyJhbGciOiJSUzI1NiIsI..."); 461 * </pre> 462 * 463 * @param uri The URI to parse. Can be absolute or relative, 464 * with a fragment or query string containing the 465 * authorisation response parameters. Must not be 466 * {@code null}. 467 * @param jarmValidator The validator of JSON Web Token (JWT) secured 468 * authorisation responses (JARM). Must not be 469 * {@code null}. 470 * 471 * @return The authorisation success or error response. 472 * 473 * @throws ParseException If no authorisation response parameters were 474 * found in the URL of if validation of the JWT 475 * response failed. 476 */ 477 public static AuthorizationResponse parse(final URI uri, final JARMValidator jarmValidator) 478 throws ParseException { 479 480 if (jarmValidator == null) { 481 throw new IllegalArgumentException("The JARM validator must not be null"); 482 } 483 484 return parse(URIUtils.getBaseURI(uri), parseResponseParameters(uri), jarmValidator); 485 } 486 487 488 /** 489 * Parses an authorisation response from the specified initial HTTP 302 490 * redirect response output at the authorisation endpoint. 491 * 492 * <p>Example HTTP response (authorisation success): 493 * 494 * <pre> 495 * HTTP/1.1 302 Found 496 * Location: https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz 497 * </pre> 498 * 499 * @see #parse(HTTPRequest) 500 * 501 * @param httpResponse The HTTP response to parse. Must not be 502 * {@code null}. 503 * 504 * @return The authorisation response. 505 * 506 * @throws ParseException If the HTTP response couldn't be parsed to an 507 * authorisation response. 508 */ 509 public static AuthorizationResponse parse(final HTTPResponse httpResponse) 510 throws ParseException { 511 512 URI location = httpResponse.getLocation(); 513 514 if (location == null) { 515 throw new ParseException("Missing redirection URI / HTTP Location header"); 516 } 517 518 return parse(location); 519 } 520 521 522 /** 523 * Parses and validates a JSON Web Token (JWT) secured authorisation 524 * response from the specified initial HTTP 302 redirect response 525 * output at the authorisation endpoint. 526 * 527 * <p>Example HTTP response (authorisation success): 528 * 529 * <pre> 530 * HTTP/1.1 302 Found 531 * Location: https://client.example.com/cb?response=eyJhbGciOiJSUzI1... 532 * </pre> 533 * 534 * @see #parse(HTTPRequest) 535 * 536 * @param httpResponse The HTTP response to parse. Must not be 537 * {@code null}. 538 * @param jarmValidator The validator of JSON Web Token (JWT) secured 539 * authorisation responses (JARM). Must not be 540 * {@code null}. 541 * 542 * @return The authorisation response. 543 * 544 * @throws ParseException If the HTTP response couldn't be parsed to an 545 * authorisation response or if validation of 546 * the JWT response failed. 547 */ 548 public static AuthorizationResponse parse(final HTTPResponse httpResponse, 549 final JARMValidator jarmValidator) 550 throws ParseException { 551 552 URI location = httpResponse.getLocation(); 553 554 if (location == null) { 555 throw new ParseException("Missing redirection URI / HTTP Location header"); 556 } 557 558 return parse(location, jarmValidator); 559 } 560 561 562 /** 563 * Parses an authorisation response from the specified HTTP request at 564 * the client redirection (callback) URI. Applies to the {@code query}, 565 * {@code fragment} and {@code form_post} response modes. 566 * 567 * <p>Example HTTP request (authorisation success): 568 * 569 * <pre> 570 * GET /cb?code=SplxlOBeZQQYbYS6WxSbIA&state=xyz HTTP/1.1 571 * Host: client.example.com 572 * </pre> 573 * 574 * @see #parse(HTTPResponse) 575 * 576 * @param httpRequest The HTTP request to parse. Must not be 577 * {@code null}. 578 * 579 * @return The authorisation response. 580 * 581 * @throws ParseException If the HTTP request couldn't be parsed to an 582 * authorisation response. 583 */ 584 public static AuthorizationResponse parse(final HTTPRequest httpRequest) 585 throws ParseException { 586 587 return parse(httpRequest.getURI(), parseResponseParameters(httpRequest)); 588 } 589 590 591 /** 592 * Parses and validates a JSON Web Token (JWT) secured authorisation 593 * response from the specified HTTP request at the client redirection 594 * (callback) URI. Applies to the {@code query.jwt}, 595 * {@code fragment.jwt} and {@code form_post.jwt} response modes. 596 * 597 * <p>Example HTTP request (authorisation success): 598 * 599 * <pre> 600 * GET /cb?response=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9... HTTP/1.1 601 * Host: client.example.com 602 * </pre> 603 * 604 * @see #parse(HTTPResponse) 605 * 606 * @param httpRequest The HTTP request to parse. Must not be 607 * {@code null}. 608 * @param jarmValidator The validator of JSON Web Token (JWT) secured 609 * authorisation responses (JARM). Must not be 610 * {@code null}. 611 * 612 * @return The authorisation response. 613 * 614 * @throws ParseException If the HTTP request couldn't be parsed to an 615 * authorisation response or if validation of 616 * the JWT response failed. 617 */ 618 public static AuthorizationResponse parse(final HTTPRequest httpRequest, 619 final JARMValidator jarmValidator) 620 throws ParseException { 621 622 if (jarmValidator == null) { 623 throw new IllegalArgumentException("The JARM validator must not be null"); 624 } 625 626 return parse(httpRequest.getURI(), parseResponseParameters(httpRequest), jarmValidator); 627 } 628 629 630 /** 631 * Parses the relevant authorisation response parameters. This method 632 * is intended for internal SDK usage only. 633 * 634 * @param uri The URI to parse its query or fragment parameters. Must 635 * not be {@code null}. 636 * 637 * @return The authorisation response parameters. 638 * 639 * @throws ParseException If parsing failed. 640 */ 641 public static Map<String,List<String>> parseResponseParameters(final URI uri) 642 throws ParseException { 643 644 if (uri.getRawFragment() != null) { 645 return URLUtils.parseParameters(uri.getRawFragment()); 646 } else if (uri.getRawQuery() != null) { 647 return URLUtils.parseParameters(uri.getRawQuery()); 648 } else { 649 throw new ParseException("Missing URI fragment or query string"); 650 } 651 } 652 653 654 /** 655 * Parses the relevant authorisation response parameters. This method 656 * is intended for internal SDK usage only. 657 * 658 * @param httpRequest The HTTP request. Must not be {@code null}. 659 * 660 * @return The authorisation response parameters. 661 * 662 * @throws ParseException If parsing failed. 663 */ 664 public static Map<String,List<String>> parseResponseParameters(final HTTPRequest httpRequest) 665 throws ParseException { 666 667 if (httpRequest.getQuery() != null) { 668 // For query string and form_post response mode 669 return URLUtils.parseParameters(httpRequest.getQuery()); 670 } else if (httpRequest.getFragment() != null) { 671 // For fragment response mode (never available in actual HTTP request from browser) 672 return URLUtils.parseParameters(httpRequest.getFragment()); 673 } else { 674 throw new ParseException("Missing URI fragment, query string or post body"); 675 } 676 } 677}