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