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