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, section 2. 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 uri The URI of the end-session endpoint. 122 * May be {@code null} if the 123 * {@link #toHTTPRequest} method will not 124 * 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 uri, 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(uri); 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 uri The URI of the end-session endpoint. 164 * May be {@code null} if the 165 * {@link #toHTTPRequest} method will not 166 * 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 uri, 176 final JWT idTokenHint, 177 final URI postLogoutRedirectURI, 178 final State state) { 179 this(uri, 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 uri The URI of the end-session endpoint. May be 188 * {@code null} if the {@link #toHTTPRequest} method 189 * will not be used. 190 * @param idTokenHint The ID token hint (recommended), {@code null} if 191 * not specified. 192 */ 193 public LogoutRequest(final URI uri, 194 final JWT idTokenHint) { 195 this(uri, idTokenHint, null, null); 196 } 197 198 199 /** 200 * Creates a new OpenID Connect logout request without a post-logout 201 * redirection. 202 * 203 * @param uri The URI of the end-session endpoint. May be {@code null} 204 * if the {@link #toHTTPRequest} method will not be used. 205 */ 206 public LogoutRequest(final URI uri) { 207 this(uri, null, null, null); 208 } 209 210 211 /** 212 * Returns the ID token hint. Corresponds to the optional 213 * {@code id_token_hint} parameter. 214 * 215 * @return The ID token hint, {@code null} if not specified. 216 */ 217 public JWT getIDTokenHint() { 218 return idTokenHint; 219 } 220 221 222 /** 223 * Returns the logout hint. Corresponds to the optional 224 * {@code logout_hint} parameter. 225 * 226 * @return The logout hint, {@code null} if not specified. 227 */ 228 public String getLogoutHint() { 229 return logoutHint; 230 } 231 232 233 /** 234 * Returns the client ID. Corresponds to the optional {@code client_id} 235 * parameter. 236 * 237 * @return The client ID, {@code null} if not specified. 238 */ 239 public ClientID getClientID() { 240 return clientID; 241 } 242 243 244 /** 245 * Return the post-logout redirection URI. 246 * 247 * @return The post-logout redirection URI, {@code null} if not 248 * specified. 249 */ 250 public URI getPostLogoutRedirectionURI() { 251 return postLogoutRedirectURI; 252 } 253 254 255 /** 256 * Returns the state parameter for a post-logout redirection URI. 257 * Corresponds to the optional {@code state} parameter. 258 * 259 * @return The state parameter, {@code null} if not specified. 260 */ 261 public State getState() { 262 return state; 263 } 264 265 266 /** 267 * Returns the end-user's preferred languages and scripts for the user 268 * interface, ordered by preference. Corresponds to the optional 269 * {@code ui_locales} parameter. 270 * 271 * @return The preferred UI locales, {@code null} if not specified. 272 */ 273 public List<LangTag> getUILocales() { 274 return uiLocales; 275 } 276 277 278 /** 279 * Returns the parameters for this logout request. 280 * 281 * <p>Example parameters: 282 * 283 * <pre> 284 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 285 * post_logout_redirect_uri = https://client.example.com/post-logout 286 * state = af0ifjsldkj 287 * </pre> 288 * 289 * @return The parameters. 290 */ 291 public Map<String,List<String>> toParameters() { 292 293 Map <String,List<String>> params = new LinkedHashMap<>(); 294 295 if (getIDTokenHint() != null) { 296 try { 297 params.put("id_token_hint", Collections.singletonList(getIDTokenHint().serialize())); 298 } catch (IllegalStateException e) { 299 throw new SerializeException("Couldn't serialize ID token: " + e.getMessage(), e); 300 } 301 } 302 303 if (getLogoutHint() != null) { 304 params.put("logout_hint", Collections.singletonList(getLogoutHint())); 305 } 306 307 if (getClientID() != null) { 308 params.put("client_id", Collections.singletonList(getClientID().getValue())); 309 } 310 311 if (getPostLogoutRedirectionURI() != null) { 312 params.put("post_logout_redirect_uri", Collections.singletonList(getPostLogoutRedirectionURI().toString())); 313 } 314 315 if (getState() != null) { 316 params.put("state", Collections.singletonList(getState().getValue())); 317 } 318 319 if (getUILocales() != null) { 320 params.put("ui_locales", Collections.singletonList(LangTagUtils.concat(getUILocales()))); 321 } 322 323 return params; 324 } 325 326 327 /** 328 * Returns the URI query string for this logout request. 329 * 330 * <p>Note that the '?' character preceding the query string in a URI 331 * is not included in the returned string. 332 * 333 * <p>Example URI query string: 334 * 335 * <pre> 336 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 337 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 338 * &state=af0ifjsldkj 339 * </pre> 340 * 341 * @return The URI query string. 342 */ 343 public String toQueryString() { 344 return URLUtils.serializeParameters(toParameters()); 345 } 346 347 348 /** 349 * Returns the complete URI representation for this logout request, 350 * consisting of the {@link #getEndpointURI end-session endpoint URI} 351 * with the {@link #toQueryString query string} appended. 352 * 353 * <p>Example URI: 354 * 355 * <pre> 356 * https://server.example.com/logout? 357 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 358 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 359 * &state=af0ifjsldkj 360 * </pre> 361 * 362 * @return The URI representation. 363 */ 364 public URI toURI() { 365 366 if (getEndpointURI() == null) 367 throw new SerializeException("The end-session endpoint URI is not specified"); 368 369 final Map<String, List<String>> mergedQueryParams = new HashMap<>(URLUtils.parseParameters(getEndpointURI().getQuery())); 370 mergedQueryParams.putAll(toParameters()); 371 String query = URLUtils.serializeParameters(mergedQueryParams); 372 if (StringUtils.isNotBlank(query)) { 373 query = '?' + query; 374 } 375 try { 376 return new URI(URIUtils.getBaseURI(getEndpointURI()) + query); 377 } catch (URISyntaxException e) { 378 throw new SerializeException(e.getMessage(), e); 379 } 380 } 381 382 383 @Override 384 public HTTPRequest toHTTPRequest() { 385 386 if (getEndpointURI() == null) 387 throw new SerializeException("The endpoint URI is not specified"); 388 389 Map<String, List<String>> mergedQueryParams = new LinkedHashMap<>(URLUtils.parseParameters(getEndpointURI().getQuery())); 390 mergedQueryParams.putAll(toParameters()); 391 392 URL baseURL; 393 try { 394 baseURL = URLUtils.getBaseURL(getEndpointURI().toURL()); 395 } catch (MalformedURLException e) { 396 throw new SerializeException(e.getMessage(), e); 397 } 398 399 HTTPRequest httpRequest; 400 httpRequest = new HTTPRequest(HTTPRequest.Method.POST, baseURL); 401 httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED); 402 httpRequest.setBody(URLUtils.serializeParameters(mergedQueryParams)); 403 return httpRequest; 404 } 405 406 407 /** 408 * Parses a logout request from the specified parameters. 409 * 410 * <p>Example parameters: 411 * 412 * <pre> 413 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 414 * post_logout_redirect_uri = https://client.example.com/post-logout 415 * state = af0ifjsldkj 416 * </pre> 417 * 418 * @param params The parameters, empty map if none. Must not be 419 * {@code null}. 420 * 421 * @return The logout request. 422 * 423 * @throws ParseException If the parameters couldn't be parsed to a 424 * logout request. 425 */ 426 public static LogoutRequest parse(final Map<String,List<String>> params) 427 throws ParseException { 428 429 return parse(null, params); 430 } 431 432 433 /** 434 * Parses a logout request from the specified URI and query parameters. 435 * 436 * <p>Example parameters: 437 * 438 * <pre> 439 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 440 * post_logout_redirect_uri = https://client.example.com/post-logout 441 * state = af0ifjsldkj 442 * </pre> 443 * 444 * @param uri The URI of the end-session endpoint. May be 445 * {@code null} if the {@link #toHTTPRequest()} method 446 * will not be used. 447 * @param params The parameters, empty map if none. Must not be 448 * {@code null}. 449 * 450 * @return The logout request. 451 * 452 * @throws ParseException If the parameters couldn't be parsed to a 453 * logout request. 454 */ 455 public static LogoutRequest parse(final URI uri, final Map<String,List<String>> params) 456 throws ParseException { 457 458 String v = MultivaluedMapUtils.getFirstValue(params, "id_token_hint"); 459 460 JWT idTokenHint = null; 461 462 if (StringUtils.isNotBlank(v)) { 463 464 try { 465 idTokenHint = JWTParser.parse(v); 466 } catch (java.text.ParseException e) { 467 throw new ParseException("Invalid id_token_hint: " + e.getMessage(), e); 468 } 469 } 470 471 String logoutHint = MultivaluedMapUtils.getFirstValue(params, "logout_hint"); 472 473 ClientID clientID = null; 474 475 v = MultivaluedMapUtils.getFirstValue(params, "client_id"); 476 477 if (StringUtils.isNotBlank(v)) { 478 clientID = new ClientID(v); 479 } 480 481 v = MultivaluedMapUtils.getFirstValue(params, "post_logout_redirect_uri"); 482 483 URI postLogoutRedirectURI = null; 484 485 if (StringUtils.isNotBlank(v)) { 486 try { 487 postLogoutRedirectURI = new URI(v); 488 } catch (URISyntaxException e) { 489 throw new ParseException("Invalid post_logout_redirect_uri parameter: " + e.getMessage(), e); 490 } 491 } 492 493 State state = null; 494 495 v = MultivaluedMapUtils.getFirstValue(params, "state"); 496 497 if (postLogoutRedirectURI != null && StringUtils.isNotBlank(v)) { 498 state = new State(v); 499 } 500 501 List<LangTag> uiLocales; 502 try { 503 uiLocales = LangTagUtils.parseLangTagList(MultivaluedMapUtils.getFirstValue(params, "ui_locales")); 504 } catch (LangTagException e) { 505 throw new ParseException("Invalid ui_locales parameter: " + e.getMessage(), e); 506 } 507 508 return new LogoutRequest(uri, idTokenHint, logoutHint, clientID, postLogoutRedirectURI, state, uiLocales); 509 } 510 511 512 /** 513 * Parses a logout request from the specified URI query string. 514 * 515 * <p>Example URI query string: 516 * 517 * <pre> 518 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 519 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 520 * &state=af0ifjsldkj 521 * </pre> 522 * 523 * @param query The URI query string, {@code null} if none. 524 * 525 * @return The logout request. 526 * 527 * @throws ParseException If the query string couldn't be parsed to a 528 * logout request. 529 */ 530 public static LogoutRequest parse(final String query) 531 throws ParseException { 532 533 return parse(null, URLUtils.parseParameters(query)); 534 } 535 536 537 /** 538 * Parses a logout request from the specified URI query string. 539 * 540 * <p>Example URI query string: 541 * 542 * <pre> 543 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 544 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 545 * &state=af0ifjsldkj 546 * </pre> 547 * 548 * @param uri The URI of the end-session endpoint. May be 549 * {@code null} if the {@link #toHTTPRequest()} method 550 * will not be used. 551 * @param query The URI query string, {@code null} if none. 552 * 553 * @return The logout request. 554 * 555 * @throws ParseException If the query string couldn't be parsed to a 556 * logout request. 557 */ 558 public static LogoutRequest parse(final URI uri, final String query) 559 throws ParseException { 560 561 return parse(uri, URLUtils.parseParameters(query)); 562 } 563 564 565 /** 566 * Parses a logout request from the specified URI. 567 * 568 * <p>Example URI: 569 * 570 * <pre> 571 * https://server.example.com/logout? 572 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 573 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 574 * &state=af0ifjsldkj 575 * </pre> 576 * 577 * @param uri The URI. Must not be {@code null}. 578 * 579 * @return The logout request. 580 * 581 * @throws ParseException If the URI couldn't be parsed to a logout 582 * request. 583 */ 584 public static LogoutRequest parse(final URI uri) 585 throws ParseException { 586 587 return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getRawQuery())); 588 } 589 590 591 /** 592 * Parses a logout request from the specified HTTP GET or POST request. 593 * 594 * <p>Example HTTP POST request: 595 * 596 * <pre> 597 * POST /op/logout HTTP/1.1 598 * Host: server.example.com 599 * Content-Type: application/x-www-form-urlencoded 600 * 601 * id_token_hint=eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 602 * &post_logout_redirect_uri=https%3A%2F%2Fclient.example.org%2Fpost-logout 603 * &state=af0ifjsldkj 604 * </pre> 605 * 606 * @param httpRequest The HTTP request. Must not be {@code null}. 607 * 608 * @return The logout request. 609 * 610 * @throws ParseException If the HTTP request couldn't be parsed to a 611 * logout request. 612 */ 613 public static LogoutRequest parse(final HTTPRequest httpRequest) 614 throws ParseException { 615 616 if (HTTPRequest.Method.POST.equals(httpRequest.getMethod())) { 617 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 618 return LogoutRequest.parse(httpRequest.getURI(), httpRequest.getBodyAsFormParameters()); 619 } 620 621 if (HTTPRequest.Method.GET.equals(httpRequest.getMethod())) { 622 return LogoutRequest.parse(httpRequest.getURI()); 623 } 624 625 throw new ParseException("The HTTP request method must be POST or GET"); 626 } 627}