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