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 try { 286 baseURL = URLUtils.getBaseURL(getEndpointURI().toURL()); 287 } catch (MalformedURLException e) { 288 throw new SerializeException(e.getMessage(), e); 289 } 290 291 httpRequest = new HTTPRequest(HTTPRequest.Method.GET, baseURL); 292 httpRequest.setQuery(URLUtils.serializeParameters(mergedQueryParams)); 293 return httpRequest; 294 } 295 296 297 /** 298 * Parses a logout request from the specified URI query parameters. 299 * 300 * <p>Example parameters: 301 * 302 * <pre> 303 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 304 * post_logout_redirect_uri = https://client.example.com/post-logout 305 * state = af0ifjsldkj 306 * </pre> 307 * 308 * @param params The parameters, empty map if none. Must not be 309 * {@code null}. 310 * 311 * @return The logout request. 312 * 313 * @throws ParseException If the parameters couldn't be parsed to a 314 * logout request. 315 */ 316 public static LogoutRequest parse(final Map<String,List<String>> params) 317 throws ParseException { 318 319 return parse(null, params); 320 } 321 322 323 /** 324 * Parses a logout request from the specified URI and query parameters. 325 * 326 * <p>Example parameters: 327 * 328 * <pre> 329 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 330 * post_logout_redirect_uri = https://client.example.com/post-logout 331 * state = af0ifjsldkj 332 * </pre> 333 * 334 * @param uri The URI of the end-session endpoint. May be 335 * {@code null} if the {@link #toHTTPRequest()} method 336 * will not be used. 337 * @param params The parameters, empty map if none. Must not be 338 * {@code null}. 339 * 340 * @return The logout request. 341 * 342 * @throws ParseException If the parameters couldn't be parsed to a 343 * logout request. 344 */ 345 public static LogoutRequest parse(final URI uri, final Map<String,List<String>> params) 346 throws ParseException { 347 348 String v = MultivaluedMapUtils.getFirstValue(params, "id_token_hint"); 349 350 JWT idTokenHint = null; 351 352 if (StringUtils.isNotBlank(v)) { 353 354 try { 355 idTokenHint = JWTParser.parse(v); 356 } catch (java.text.ParseException e) { 357 throw new ParseException("Invalid ID token hint: " + e.getMessage(), e); 358 } 359 } 360 361 v = MultivaluedMapUtils.getFirstValue(params, "post_logout_redirect_uri"); 362 363 URI postLogoutRedirectURI = null; 364 365 if (StringUtils.isNotBlank(v)) { 366 367 try { 368 postLogoutRedirectURI = new URI(v); 369 } catch (URISyntaxException e) { 370 throw new ParseException("Invalid \"post_logout_redirect_uri\" parameter: " + e.getMessage(), e); 371 } 372 } 373 374 State state = null; 375 376 v = MultivaluedMapUtils.getFirstValue(params, "state"); 377 378 if (postLogoutRedirectURI != null && StringUtils.isNotBlank(v)) { 379 state = new State(v); 380 } 381 382 return new LogoutRequest(uri, idTokenHint, postLogoutRedirectURI, state); 383 } 384 385 386 /** 387 * Parses a logout request from the specified URI query string. 388 * 389 * <p>Example URI query string: 390 * 391 * <pre> 392 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 393 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 394 * &state=af0ifjsldkj 395 * </pre> 396 * 397 * @param query The URI query string, {@code null} if none. 398 * 399 * @return The logout request. 400 * 401 * @throws ParseException If the query string couldn't be parsed to a 402 * logout request. 403 */ 404 public static LogoutRequest parse(final String query) 405 throws ParseException { 406 407 return parse(null, URLUtils.parseParameters(query)); 408 } 409 410 411 /** 412 * Parses a logout request from the specified URI query string. 413 * 414 * <p>Example URI query string: 415 * 416 * <pre> 417 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 418 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 419 * &state=af0ifjsldkj 420 * </pre> 421 * 422 * @param uri The URI of the end-session endpoint. May be 423 * {@code null} if the {@link #toHTTPRequest()} method 424 * will not be used. 425 * @param query The URI query string, {@code null} if none. 426 * 427 * @return The logout request. 428 * 429 * @throws ParseException If the query string couldn't be parsed to a 430 * logout request. 431 */ 432 public static LogoutRequest parse(final URI uri, final String query) 433 throws ParseException { 434 435 return parse(uri, URLUtils.parseParameters(query)); 436 } 437 438 439 /** 440 * Parses a logout request from the specified URI. 441 * 442 * <p>Example URI: 443 * 444 * <pre> 445 * https://server.example.com/logout? 446 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 447 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 448 * &state=af0ifjsldkj 449 * </pre> 450 * 451 * @param uri The URI. Must not be {@code null}. 452 * 453 * @return The logout request. 454 * 455 * @throws ParseException If the URI couldn't be parsed to a logout 456 * request. 457 */ 458 public static LogoutRequest parse(final URI uri) 459 throws ParseException { 460 461 return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getRawQuery())); 462 } 463 464 465 /** 466 * Parses a logout request from the specified HTTP request. 467 * 468 * <p>Example HTTP request (GET): 469 * 470 * <pre> 471 * https://server.example.com/logout? 472 * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi... 473 * &post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout 474 * &state=af0ifjsldkj 475 * </pre> 476 * 477 * @param httpRequest The HTTP request. Must not be {@code null}. 478 * 479 * @return The logout request. 480 * 481 * @throws ParseException If the HTTP request couldn't be parsed to a 482 * logout request. 483 */ 484 public static LogoutRequest parse(final HTTPRequest httpRequest) 485 throws ParseException { 486 487 String query = httpRequest.getQuery(); 488 489 if (query == null) 490 throw new ParseException("Missing URI query string"); 491 492 return parse(URIUtils.getBaseURI(httpRequest.getURI()), query); 493 } 494}