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