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.openid.connect.sdk; 019 020 021import com.nimbusds.common.contenttype.ContentType; 022import com.nimbusds.jwt.JWT; 023import com.nimbusds.jwt.JWTParser; 024import com.nimbusds.langtag.LangTag; 025import com.nimbusds.langtag.LangTagException; 026import com.nimbusds.langtag.LangTagUtils; 027import com.nimbusds.oauth2.sdk.AbstractRequest; 028import com.nimbusds.oauth2.sdk.ParseException; 029import com.nimbusds.oauth2.sdk.SerializeException; 030import com.nimbusds.oauth2.sdk.http.HTTPRequest; 031import com.nimbusds.oauth2.sdk.id.ClientID; 032import com.nimbusds.oauth2.sdk.id.State; 033import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 034import com.nimbusds.oauth2.sdk.util.StringUtils; 035import com.nimbusds.oauth2.sdk.util.URIUtils; 036import com.nimbusds.oauth2.sdk.util.URLUtils; 037import net.jcip.annotations.Immutable; 038 039import java.net.MalformedURLException; 040import java.net.URI; 041import java.net.URISyntaxException; 042import java.net.URL; 043import java.util.*; 044 045 046/** 047 * Logout request initiated by an OpenID relying party (RP). Supports HTTP GET 048 * and POST. HTTP POST is the recommended method to protect the optional ID 049 * token hint parameter from potentially getting recorded in access logs. 050 * 051 * <p>Example HTTP POST request: 052 * 053 * <pre> 054 * POST /op/logout HTTP/1.1 055 * Host: server.example.com 056 * Content-Type: application/x-www-form-urlencoded 057 * 058 * id_token_hint=eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 059 * &post_logout_redirect_uri=https%3A%2F%2Fclient.example.org%2Fpost-logout 060 * &state=af0ifjsldkj 061 * </pre> 062 * 063 * <p>Example URL for an HTTP GET request: 064 * 065 * <pre> 066 * https://server.example.com/op/logout? 067 * id_token_hint=eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 068 * &post_logout_redirect_uri=https%3A%2F%2Fclient.example.org%2Fpost-logout 069 * &state=af0ifjsldkj 070 * </pre> 071 * 072 * <p>Related specifications: 073 * 074 * <ul> 075 * <li>OpenID Connect RP-Initiated Logout 1.0 076 * </ul> 077 */ 078@Immutable 079public class LogoutRequest extends AbstractRequest { 080 081 082 /** 083 * The ID token hint (recommended). 084 */ 085 private final JWT idTokenHint; 086 087 088 /** 089 * The logout hint (optional). 090 */ 091 private final String logoutHint; 092 093 094 /** 095 * The client ID (optional). 096 */ 097 private final ClientID clientID; 098 099 100 /** 101 * The post-logout redirection URI (optional). 102 */ 103 private final URI postLogoutRedirectURI; 104 105 106 /** 107 * The state parameter (optional). 108 */ 109 private final State state; 110 111 112 /** 113 * The UI locales (optional). 114 */ 115 private final List<LangTag> uiLocales; 116 117 118 /** 119 * Creates a new OpenID Connect logout request. 120 * 121 * @param endpoint The URI of the end-session endpoint. 122 * May be {@code null} if the 123 * {@link #toHTTPRequest} method is not 124 * going to be used. 125 * @param idTokenHint The ID token hint (recommended), 126 * {@code null} if not specified. 127 * @param logoutHint The optional logout hint, {@code null} 128 * if not specified. 129 * @param clientID The optional client ID, {@code null} if 130 * not specified. 131 * @param postLogoutRedirectURI The optional post-logout redirection 132 * URI, {@code null} if not specified. 133 * @param state The optional state parameter for the 134 * post-logout redirection URI, 135 * {@code null} if not specified. 136 * @param uiLocales The optional end-user's preferred 137 * languages and scripts for the user 138 * interface, ordered by preference. 139 */ 140 public LogoutRequest(final URI endpoint, 141 final JWT idTokenHint, 142 final String logoutHint, 143 final ClientID clientID, 144 final URI postLogoutRedirectURI, 145 final State state, 146 final List<LangTag> uiLocales) { 147 super(endpoint); 148 this.idTokenHint = idTokenHint; 149 this.logoutHint = logoutHint; 150 this.clientID = clientID; 151 this.postLogoutRedirectURI = postLogoutRedirectURI; 152 if (postLogoutRedirectURI == null && state != null) { 153 throw new IllegalArgumentException("The state parameter requires a post-logout redirection URI"); 154 } 155 this.state = state; 156 this.uiLocales = uiLocales; 157 } 158 159 160 /** 161 * Creates a new OpenID Connect logout request. 162 * 163 * @param endpoint The URI of the end-session endpoint. 164 * May be {@code null} if the 165 * {@link #toHTTPRequest} method is not 166 * going to be used. 167 * @param idTokenHint The ID token hint (recommended), 168 * {@code null} if not specified. 169 * @param postLogoutRedirectURI The optional post-logout redirection 170 * URI, {@code null} if not specified. 171 * @param state The optional state parameter for the 172 * post-logout redirection URI, 173 * {@code null} if not specified. 174 */ 175 public LogoutRequest(final URI endpoint, 176 final JWT idTokenHint, 177 final URI postLogoutRedirectURI, 178 final State state) { 179 this(endpoint, idTokenHint, null, null, postLogoutRedirectURI, state, null); 180 } 181 182 183 /** 184 * Creates a new OpenID Connect logout request without a post-logout 185 * redirection. 186 * 187 * @param endpoint The URI of the end-session endpoint. May be 188 * {@code null} if the {@link #toHTTPRequest} method 189 * is not going to be used. 190 * @param idTokenHint The ID token hint (recommended), {@code null} if 191 * not specified. 192 */ 193 public LogoutRequest(final URI endpoint, 194 final JWT idTokenHint) { 195 this(endpoint, idTokenHint, null, null); 196 } 197 198 199 /** 200 * Creates a new OpenID Connect logout request without a post-logout 201 * redirection. 202 * 203 * @param endpoint The URI of the end-session endpoint. May be 204 * {@code null} if the {@link #toHTTPRequest} method is 205 * not going to be used. 206 */ 207 public LogoutRequest(final URI endpoint) { 208 this(endpoint, null, null, null); 209 } 210 211 212 /** 213 * Returns the ID token hint. Corresponds to the optional 214 * {@code id_token_hint} parameter. 215 * 216 * @return The ID token hint, {@code null} if not specified. 217 */ 218 public JWT getIDTokenHint() { 219 return idTokenHint; 220 } 221 222 223 /** 224 * Returns the logout hint. Corresponds to the optional 225 * {@code logout_hint} parameter. 226 * 227 * @return The logout hint, {@code null} if not specified. 228 */ 229 public String getLogoutHint() { 230 return logoutHint; 231 } 232 233 234 /** 235 * Returns the client ID. Corresponds to the optional {@code client_id} 236 * parameter. 237 * 238 * @return The client ID, {@code null} if not specified. 239 */ 240 public ClientID getClientID() { 241 return clientID; 242 } 243 244 245 /** 246 * Return the post-logout redirection URI. 247 * 248 * @return The post-logout redirection URI, {@code null} if not 249 * specified. 250 */ 251 public URI getPostLogoutRedirectionURI() { 252 return postLogoutRedirectURI; 253 } 254 255 256 /** 257 * Returns the state parameter for a post-logout redirection URI. 258 * Corresponds to the optional {@code state} parameter. 259 * 260 * @return The state parameter, {@code null} if not specified. 261 */ 262 public State getState() { 263 return state; 264 } 265 266 267 /** 268 * Returns the end-user's preferred languages and scripts for the user 269 * interface, ordered by preference. Corresponds to the optional 270 * {@code ui_locales} parameter. 271 * 272 * @return The preferred UI locales, {@code null} if not specified. 273 */ 274 public List<LangTag> getUILocales() { 275 return uiLocales; 276 } 277 278 279 /** 280 * Returns the parameters for this logout request. 281 * 282 * <p>Example parameters: 283 * 284 * <pre> 285 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 286 * post_logout_redirect_uri = https://client.example.com/post-logout 287 * state = af0ifjsldkj 288 * </pre> 289 * 290 * @return The parameters. 291 */ 292 public Map<String,List<String>> toParameters() { 293 294 Map <String,List<String>> params = new LinkedHashMap<>(); 295 296 if (getIDTokenHint() != null) { 297 try { 298 params.put("id_token_hint", Collections.singletonList(getIDTokenHint().serialize())); 299 } catch (IllegalStateException e) { 300 throw new SerializeException("Couldn't serialize ID token: " + e.getMessage(), e); 301 } 302 } 303 304 if (getLogoutHint() != null) { 305 params.put("logout_hint", Collections.singletonList(getLogoutHint())); 306 } 307 308 if (getClientID() != null) { 309 params.put("client_id", Collections.singletonList(getClientID().getValue())); 310 } 311 312 if (getPostLogoutRedirectionURI() != null) { 313 params.put("post_logout_redirect_uri", Collections.singletonList(getPostLogoutRedirectionURI().toString())); 314 } 315 316 if (getState() != null) { 317 params.put("state", Collections.singletonList(getState().getValue())); 318 } 319 320 if (getUILocales() != null) { 321 params.put("ui_locales", Collections.singletonList(LangTagUtils.concat(getUILocales()))); 322 } 323 324 return params; 325 } 326 327 328 /** 329 * Returns the URI query string for this logout request. 330 * 331 * <p>Note that the '?' character preceding the query string in a URI 332 * is not included in the returned string. 333 * 334 * <p>Example URI query string: 335 * 336 * <pre> 337 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 338 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 339 * &state=af0ifjsldkj 340 * </pre> 341 * 342 * @return The URI query string. 343 */ 344 public String toQueryString() { 345 return URLUtils.serializeParameters(toParameters()); 346 } 347 348 349 /** 350 * Returns the complete URI representation for this logout request, 351 * consisting of the {@link #getEndpointURI end-session endpoint URI} 352 * with the {@link #toQueryString query string} appended. 353 * 354 * <p>Example URI: 355 * 356 * <pre> 357 * https://server.example.com/logout? 358 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 359 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 360 * &state=af0ifjsldkj 361 * </pre> 362 * 363 * @return The URI representation. 364 */ 365 public URI toURI() { 366 367 if (getEndpointURI() == null) 368 throw new SerializeException("The end-session endpoint URI is not specified"); 369 370 final Map<String, List<String>> mergedQueryParams = new HashMap<>(URLUtils.parseParameters(getEndpointURI().getQuery())); 371 mergedQueryParams.putAll(toParameters()); 372 String query = URLUtils.serializeParameters(mergedQueryParams); 373 if (StringUtils.isNotBlank(query)) { 374 query = '?' + query; 375 } 376 try { 377 return new URI(URIUtils.getBaseURI(getEndpointURI()) + query); 378 } catch (URISyntaxException e) { 379 throw new SerializeException(e.getMessage(), e); 380 } 381 } 382 383 384 @Override 385 public HTTPRequest toHTTPRequest() { 386 387 if (getEndpointURI() == null) 388 throw new SerializeException("The endpoint URI is not specified"); 389 390 Map<String, List<String>> mergedQueryParams = new LinkedHashMap<>(URLUtils.parseParameters(getEndpointURI().getQuery())); 391 mergedQueryParams.putAll(toParameters()); 392 393 URL baseURL; 394 try { 395 baseURL = URLUtils.getBaseURL(getEndpointURI().toURL()); 396 } catch (MalformedURLException e) { 397 throw new SerializeException(e.getMessage(), e); 398 } 399 400 HTTPRequest httpRequest; 401 httpRequest = new HTTPRequest(HTTPRequest.Method.POST, baseURL); 402 httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED); 403 httpRequest.setBody(URLUtils.serializeParameters(mergedQueryParams)); 404 return httpRequest; 405 } 406 407 408 /** 409 * Parses a logout request from the specified parameters. 410 * 411 * <p>Example parameters: 412 * 413 * <pre> 414 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 415 * post_logout_redirect_uri = https://client.example.com/post-logout 416 * state = af0ifjsldkj 417 * </pre> 418 * 419 * @param params The parameters, empty map if none. Must not be 420 * {@code null}. 421 * 422 * @return The logout request. 423 * 424 * @throws ParseException If the parameters couldn't be parsed to a 425 * logout request. 426 */ 427 public static LogoutRequest parse(final Map<String,List<String>> params) 428 throws ParseException { 429 430 return parse(null, params); 431 } 432 433 434 /** 435 * Parses a logout request from the specified URI and query parameters. 436 * 437 * <p>Example parameters: 438 * 439 * <pre> 440 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 441 * post_logout_redirect_uri = https://client.example.com/post-logout 442 * state = af0ifjsldkj 443 * </pre> 444 * 445 * @param uri The URI of the end-session endpoint. May be 446 * {@code null} if the {@link #toHTTPRequest()} method 447 * will not be used. 448 * @param params The parameters, empty map if none. Must not be 449 * {@code null}. 450 * 451 * @return The logout request. 452 * 453 * @throws ParseException If the parameters couldn't be parsed to a 454 * logout request. 455 */ 456 public static LogoutRequest parse(final URI uri, final Map<String,List<String>> params) 457 throws ParseException { 458 459 String v = MultivaluedMapUtils.getFirstValue(params, "id_token_hint"); 460 461 JWT idTokenHint = null; 462 463 if (StringUtils.isNotBlank(v)) { 464 465 try { 466 idTokenHint = JWTParser.parse(v); 467 } catch (java.text.ParseException e) { 468 throw new ParseException("Invalid id_token_hint: " + e.getMessage(), e); 469 } 470 } 471 472 String logoutHint = MultivaluedMapUtils.getFirstValue(params, "logout_hint"); 473 474 ClientID clientID = null; 475 476 v = MultivaluedMapUtils.getFirstValue(params, "client_id"); 477 478 if (StringUtils.isNotBlank(v)) { 479 clientID = new ClientID(v); 480 } 481 482 v = MultivaluedMapUtils.getFirstValue(params, "post_logout_redirect_uri"); 483 484 URI postLogoutRedirectURI = null; 485 486 if (StringUtils.isNotBlank(v)) { 487 try { 488 postLogoutRedirectURI = new URI(v); 489 } catch (URISyntaxException e) { 490 throw new ParseException("Invalid post_logout_redirect_uri parameter: " + e.getMessage(), e); 491 } 492 } 493 494 State state = null; 495 496 v = MultivaluedMapUtils.getFirstValue(params, "state"); 497 498 if (postLogoutRedirectURI != null && StringUtils.isNotBlank(v)) { 499 state = new State(v); 500 } 501 502 List<LangTag> uiLocales; 503 try { 504 uiLocales = LangTagUtils.parseLangTagList(MultivaluedMapUtils.getFirstValue(params, "ui_locales")); 505 } catch (LangTagException e) { 506 throw new ParseException("Invalid ui_locales parameter: " + e.getMessage(), e); 507 } 508 509 return new LogoutRequest(uri, idTokenHint, logoutHint, clientID, postLogoutRedirectURI, state, uiLocales); 510 } 511 512 513 /** 514 * Parses a logout request from the specified URI query string. 515 * 516 * <p>Example URI query string: 517 * 518 * <pre> 519 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 520 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 521 * &state=af0ifjsldkj 522 * </pre> 523 * 524 * @param query The URI query string, {@code null} if none. 525 * 526 * @return The logout request. 527 * 528 * @throws ParseException If the query string couldn't be parsed to a 529 * logout request. 530 */ 531 public static LogoutRequest parse(final String query) 532 throws ParseException { 533 534 return parse(null, URLUtils.parseParameters(query)); 535 } 536 537 538 /** 539 * Parses a logout request from the specified URI query string. 540 * 541 * <p>Example URI query string: 542 * 543 * <pre> 544 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 545 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 546 * &state=af0ifjsldkj 547 * </pre> 548 * 549 * @param uri The URI of the end-session endpoint. May be 550 * {@code null} if the {@link #toHTTPRequest()} method 551 * will not be used. 552 * @param query The URI query string, {@code null} if none. 553 * 554 * @return The logout request. 555 * 556 * @throws ParseException If the query string couldn't be parsed to a 557 * logout request. 558 */ 559 public static LogoutRequest parse(final URI uri, final String query) 560 throws ParseException { 561 562 return parse(uri, URLUtils.parseParameters(query)); 563 } 564 565 566 /** 567 * Parses a logout request from the specified URI. 568 * 569 * <p>Example URI: 570 * 571 * <pre> 572 * https://server.example.com/logout? 573 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 574 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 575 * &state=af0ifjsldkj 576 * </pre> 577 * 578 * @param uri The URI. Must not be {@code null}. 579 * 580 * @return The logout request. 581 * 582 * @throws ParseException If the URI couldn't be parsed to a logout 583 * request. 584 */ 585 public static LogoutRequest parse(final URI uri) 586 throws ParseException { 587 588 return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getRawQuery())); 589 } 590 591 592 /** 593 * Parses a logout request from the specified HTTP GET or POST request. 594 * 595 * <p>Example HTTP POST request: 596 * 597 * <pre> 598 * POST /op/logout HTTP/1.1 599 * Host: server.example.com 600 * Content-Type: application/x-www-form-urlencoded 601 * 602 * id_token_hint=eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 603 * &post_logout_redirect_uri=https%3A%2F%2Fclient.example.org%2Fpost-logout 604 * &state=af0ifjsldkj 605 * </pre> 606 * 607 * @param httpRequest The HTTP request. Must not be {@code null}. 608 * 609 * @return The logout request. 610 * 611 * @throws ParseException If the HTTP request couldn't be parsed to a 612 * logout request. 613 */ 614 public static LogoutRequest parse(final HTTPRequest httpRequest) 615 throws ParseException { 616 617 if (HTTPRequest.Method.POST.equals(httpRequest.getMethod())) { 618 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 619 return LogoutRequest.parse(httpRequest.getURI(), httpRequest.getBodyAsFormParameters()); 620 } 621 622 if (HTTPRequest.Method.GET.equals(httpRequest.getMethod())) { 623 return LogoutRequest.parse(httpRequest.getURI()); 624 } 625 626 throw new ParseException("The HTTP request method must be POST or GET"); 627 } 628}