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