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