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