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