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