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