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