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