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
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 endpoint              The URI of the end-session endpoint.
122         *                              May be {@code null} if the
123         *                              {@link #toHTTPRequest} method is not
124         *                              going to 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 endpoint,
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(endpoint);
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 endpoint              The URI of the end-session endpoint.
164         *                              May be {@code null} if the
165         *                              {@link #toHTTPRequest} method is not
166         *                              going to 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 endpoint,
176                             final JWT idTokenHint,
177                             final URI postLogoutRedirectURI,
178                             final State state) {
179                this(endpoint, 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 endpoint    The URI of the end-session endpoint. May be
188         *                    {@code null} if the {@link #toHTTPRequest} method
189         *                    is not going to be used.
190         * @param idTokenHint The ID token hint (recommended), {@code null} if
191         *                    not specified.
192         */
193        public LogoutRequest(final URI endpoint,
194                             final JWT idTokenHint) {
195                this(endpoint, idTokenHint, null, null);
196        }
197        
198        
199        /**
200         * Creates a new OpenID Connect logout request without a post-logout
201         * redirection.
202         *
203         * @param endpoint The URI of the end-session endpoint. May be
204         *                 {@code null} if the {@link #toHTTPRequest} method is
205         *                 not going to be used.
206         */
207        public LogoutRequest(final URI endpoint) {
208                this(endpoint, null, null, null);
209        }
210
211
212        /**
213         * Returns the ID token hint. Corresponds to the optional
214         * {@code id_token_hint} parameter.
215         *
216         * @return The ID token hint, {@code null} if not specified.
217         */
218        public JWT getIDTokenHint() {
219                return idTokenHint;
220        }
221        
222        
223        /**
224         * Returns the logout hint. Corresponds to the optional
225         * {@code logout_hint}  parameter.
226         *
227         * @return The logout hint, {@code null} if not specified.
228         */
229        public String getLogoutHint() {
230                return logoutHint;
231        }
232        
233        
234        /**
235         * Returns the client ID. Corresponds to the optional {@code client_id}
236         * parameter.
237         *
238         * @return The client ID, {@code null} if not specified.
239         */
240        public ClientID getClientID() {
241                return clientID;
242        }
243        
244        
245        /**
246         * Return the post-logout redirection URI.
247         *
248         * @return The post-logout redirection URI, {@code null} if not
249         *         specified.
250         */
251        public URI getPostLogoutRedirectionURI() {
252                return postLogoutRedirectURI;
253        }
254
255
256        /**
257         * Returns the state parameter for a post-logout redirection URI.
258         * Corresponds to the optional {@code state} parameter.
259         *
260         * @return The state parameter, {@code null} if not specified.
261         */
262        public State getState() {
263                return state;
264        }
265        
266        
267        /**
268         * Returns the end-user's preferred languages and scripts for the user
269         * interface, ordered by preference. Corresponds to the optional
270         * {@code ui_locales} parameter.
271         *
272         * @return The preferred UI locales, {@code null} if not specified.
273         */
274        public List<LangTag> getUILocales() {
275                return uiLocales;
276        }
277        
278        
279        /**
280         * Returns the parameters for this logout request.
281         *
282         * <p>Example parameters:
283         *
284         * <pre>
285         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
286         * post_logout_redirect_uri = https://client.example.com/post-logout
287         * state = af0ifjsldkj
288         * </pre>
289         *
290         * @return The parameters.
291         */
292        public Map<String,List<String>> toParameters() {
293
294                Map <String,List<String>> params = new LinkedHashMap<>();
295                
296                if (getIDTokenHint() != null) {
297                        try {
298                                params.put("id_token_hint", Collections.singletonList(getIDTokenHint().serialize()));
299                        } catch (IllegalStateException e) {
300                                throw new SerializeException("Couldn't serialize ID token: " + e.getMessage(), e);
301                        }
302                }
303                
304                if (getLogoutHint() != null) {
305                        params.put("logout_hint", Collections.singletonList(getLogoutHint()));
306                }
307                
308                if (getClientID() != null) {
309                        params.put("client_id", Collections.singletonList(getClientID().getValue()));
310                }
311
312                if (getPostLogoutRedirectionURI() != null) {
313                        params.put("post_logout_redirect_uri", Collections.singletonList(getPostLogoutRedirectionURI().toString()));
314                }
315
316                if (getState() != null) {
317                        params.put("state", Collections.singletonList(getState().getValue()));
318                }
319                
320                if (getUILocales() != null) {
321                        params.put("ui_locales", Collections.singletonList(LangTagUtils.concat(getUILocales())));
322                }
323
324                return params;
325        }
326
327
328        /**
329         * Returns the URI query string for this logout request.
330         *
331         * <p>Note that the '?' character preceding the query string in a URI
332         * is not included in the returned string.
333         *
334         * <p>Example URI query string:
335         *
336         * <pre>
337         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
338         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
339         * &amp;state=af0ifjsldkj
340         * </pre>
341         *
342         * @return The URI query string.
343         */
344        public String toQueryString() {
345                return URLUtils.serializeParameters(toParameters());
346        }
347
348
349        /**
350         * Returns the complete URI representation for this logout request,
351         * consisting of the {@link #getEndpointURI end-session endpoint URI}
352         * with the {@link #toQueryString query string} appended.
353         *
354         * <p>Example URI:
355         *
356         * <pre>
357         * https://server.example.com/logout?
358         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
359         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
360         * &amp;state=af0ifjsldkj
361         * </pre>
362         *
363         * @return The URI representation.
364         */
365        public URI toURI() {
366
367                if (getEndpointURI() == null)
368                        throw new SerializeException("The end-session endpoint URI is not specified");
369
370                final Map<String, List<String>> mergedQueryParams = new HashMap<>(URLUtils.parseParameters(getEndpointURI().getQuery()));
371                mergedQueryParams.putAll(toParameters());
372                String query = URLUtils.serializeParameters(mergedQueryParams);
373                if (StringUtils.isNotBlank(query)) {
374                        query = '?' + query;
375                }
376                try {
377                        return new URI(URIUtils.getBaseURI(getEndpointURI()) + query);
378                } catch (URISyntaxException e) {
379                        throw new SerializeException(e.getMessage(), e);
380                }
381        }
382
383
384        @Override
385        public HTTPRequest toHTTPRequest() {
386
387                if (getEndpointURI() == null)
388                        throw new SerializeException("The endpoint URI is not specified");
389                
390                Map<String, List<String>> mergedQueryParams = new LinkedHashMap<>(URLUtils.parseParameters(getEndpointURI().getQuery()));
391                mergedQueryParams.putAll(toParameters());
392
393                URL baseURL;
394                try {
395                        baseURL = URLUtils.getBaseURL(getEndpointURI().toURL());
396                } catch (MalformedURLException e) {
397                        throw new SerializeException(e.getMessage(), e);
398                }
399
400                HTTPRequest httpRequest;
401                httpRequest = new HTTPRequest(HTTPRequest.Method.POST, baseURL);
402                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
403                httpRequest.setBody(URLUtils.serializeParameters(mergedQueryParams));
404                return httpRequest;
405        }
406
407
408        /**
409         * Parses a logout request from the specified parameters.
410         *
411         * <p>Example parameters:
412         *
413         * <pre>
414         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
415         * post_logout_redirect_uri = https://client.example.com/post-logout
416         * state = af0ifjsldkj
417         * </pre>
418         *
419         * @param params The parameters, empty map if none. Must not be
420         *               {@code null}.
421         *
422         * @return The logout request.
423         *
424         * @throws ParseException If the parameters couldn't be parsed to a
425         *                        logout request.
426         */
427        public static LogoutRequest parse(final Map<String,List<String>> params)
428                throws ParseException {
429
430                return parse(null, params);
431        }
432
433
434        /**
435         * Parses a logout request from the specified URI and query parameters.
436         *
437         * <p>Example parameters:
438         *
439         * <pre>
440         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
441         * post_logout_redirect_uri = https://client.example.com/post-logout
442         * state = af0ifjsldkj
443         * </pre>
444         *
445         * @param uri    The URI of the end-session endpoint. May be
446         *               {@code null} if the {@link #toHTTPRequest()} method
447         *               will not be used.
448         * @param params The parameters, empty map if none. Must not be
449         *               {@code null}.
450         *
451         * @return The logout request.
452         *
453         * @throws ParseException If the parameters couldn't be parsed to a
454         *                        logout request.
455         */
456        public static LogoutRequest parse(final URI uri, final Map<String,List<String>> params)
457                throws ParseException {
458
459                String v = MultivaluedMapUtils.getFirstValue(params, "id_token_hint");
460
461                JWT idTokenHint = null;
462                
463                if (StringUtils.isNotBlank(v)) {
464                        
465                        try {
466                                idTokenHint = JWTParser.parse(v);
467                        } catch (java.text.ParseException e) {
468                                throw new ParseException("Invalid id_token_hint: " + e.getMessage(), e);
469                        }
470                }
471                
472                String logoutHint = MultivaluedMapUtils.getFirstValue(params, "logout_hint");
473                
474                ClientID clientID = null;
475                
476                v = MultivaluedMapUtils.getFirstValue(params, "client_id");
477                
478                if (StringUtils.isNotBlank(v)) {
479                        clientID = new ClientID(v);
480                }
481
482                v = MultivaluedMapUtils.getFirstValue(params, "post_logout_redirect_uri");
483
484                URI postLogoutRedirectURI = null;
485
486                if (StringUtils.isNotBlank(v)) {
487                        try {
488                                postLogoutRedirectURI = new URI(v);
489                        } catch (URISyntaxException e) {
490                                throw new ParseException("Invalid post_logout_redirect_uri parameter: " + e.getMessage(),  e);
491                        }
492                }
493
494                State state = null;
495
496                v = MultivaluedMapUtils.getFirstValue(params, "state");
497
498                if (postLogoutRedirectURI != null && StringUtils.isNotBlank(v)) {
499                        state = new State(v);
500                }
501                
502                List<LangTag> uiLocales;
503                try {
504                        uiLocales = LangTagUtils.parseLangTagList(MultivaluedMapUtils.getFirstValue(params, "ui_locales"));
505                } catch (LangTagException e) {
506                        throw new ParseException("Invalid ui_locales parameter: " + e.getMessage(), e);
507                }
508
509                return new LogoutRequest(uri, idTokenHint, logoutHint, clientID, postLogoutRedirectURI, state, uiLocales);
510        }
511
512
513        /**
514         * Parses a logout request from the specified URI query string.
515         *
516         * <p>Example URI query string:
517         *
518         * <pre>
519         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
520         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
521         * &amp;state=af0ifjsldkj
522         * </pre>
523         *
524         * @param query The URI query string, {@code null} if none.
525         *
526         * @return The logout request.
527         *
528         * @throws ParseException If the query string couldn't be parsed to a
529         *                        logout request.
530         */
531        public static LogoutRequest parse(final String query)
532                throws ParseException {
533
534                return parse(null, URLUtils.parseParameters(query));
535        }
536
537
538        /**
539         * Parses a logout request from the specified URI query string.
540         *
541         * <p>Example URI query string:
542         *
543         * <pre>
544         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
545         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
546         * &amp;state=af0ifjsldkj
547         * </pre>
548         *
549         * @param uri   The URI of the end-session endpoint. May be
550         *              {@code null} if the {@link #toHTTPRequest()} method
551         *              will not be used.
552         * @param query The URI query string, {@code null} if none.
553         *
554         * @return The logout request.
555         *
556         * @throws ParseException If the query string couldn't be parsed to a
557         *                        logout request.
558         */
559        public static LogoutRequest parse(final URI uri, final String query)
560                throws ParseException {
561
562                return parse(uri, URLUtils.parseParameters(query));
563        }
564
565
566        /**
567         * Parses a logout request from the specified URI.
568         *
569         * <p>Example URI:
570         *
571         * <pre>
572         * https://server.example.com/logout?
573         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
574         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
575         * &amp;state=af0ifjsldkj
576         * </pre>
577         *
578         * @param uri The URI. Must not be {@code null}.
579         *
580         * @return The logout request.
581         *
582         * @throws ParseException If the URI couldn't be parsed to a logout
583         *                        request.
584         */
585        public static LogoutRequest parse(final URI uri)
586                throws ParseException {
587
588                return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getRawQuery()));
589        }
590
591
592        /**
593         * Parses a logout request from the specified HTTP GET or POST request.
594         *
595         * <p>Example HTTP POST request:
596         *
597         * <pre>
598         * POST /op/logout HTTP/1.1
599         * Host: server.example.com
600         * Content-Type: application/x-www-form-urlencoded
601         *
602         * id_token_hint=eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
603         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient.example.org%2Fpost-logout
604         * &amp;state=af0ifjsldkj
605         * </pre>
606         *
607         * @param httpRequest The HTTP request. Must not be {@code null}.
608         *
609         * @return The logout request.
610         *
611         * @throws ParseException If the HTTP request couldn't be parsed to a
612         *                        logout request.
613         */
614        public static LogoutRequest parse(final HTTPRequest httpRequest)
615                throws ParseException {
616
617                if (HTTPRequest.Method.POST.equals(httpRequest.getMethod())) {
618                        httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
619                        return LogoutRequest.parse(httpRequest.getURI(), httpRequest.getBodyAsFormParameters());
620                }
621
622                if (HTTPRequest.Method.GET.equals(httpRequest.getMethod())) {
623                        return LogoutRequest.parse(httpRequest.getURI());
624                }
625
626                throw new ParseException("The HTTP request method must be POST or GET");
627        }
628}