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        public Map<String,String> toParameters() {
168
169                Map <String,String> params = new LinkedHashMap<>();
170
171                try {
172                        params.put("id_token_hint", idTokenHint.serialize());
173                } catch (IllegalStateException e) {
174                        throw new SerializeException("Couldn't serialize ID token: " + e.getMessage(), e);
175                }
176
177                if (postLogoutRedirectURI != null) {
178                        params.put("post_logout_redirect_uri", postLogoutRedirectURI.toString());
179                }
180
181                if (state != null) {
182                        params.put("state", state.getValue());
183                }
184
185                return params;
186        }
187
188
189        /**
190         * Returns the URI query string for this logout request.
191         *
192         * <p>Note that the '?' character preceding the query string in an URI
193         * is not included in the returned string.
194         *
195         * <p>Example URI query string:
196         *
197         * <pre>
198         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
199         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
200         * &amp;state=af0ifjsldkj
201         * </pre>
202         *
203         * @return The URI query string.
204         */
205        public String toQueryString() {
206
207                return URLUtils.serializeParameters(toParameters());
208        }
209
210
211        /**
212         * Returns the complete URI representation for this logout request,
213         * consisting of the {@link #getEndpointURI end-session endpoint URI}
214         * with the {@link #toQueryString query string} appended.
215         *
216         * <p>Example URI:
217         *
218         * <pre>
219         * https://server.example.com/logout?
220         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
221         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
222         * &amp;state=af0ifjsldkj
223         * </pre>
224         *
225         * @return The URI representation.
226         */
227        public URI toURI() {
228
229                if (getEndpointURI() == null)
230                        throw new SerializeException("The end-session endpoint URI is not specified");
231
232                StringBuilder sb = new StringBuilder(getEndpointURI().toString());
233                sb.append('?');
234                sb.append(toQueryString());
235                try {
236                        return new URI(sb.toString());
237                } catch (URISyntaxException e) {
238                        throw new SerializeException("Couldn't append query string: " + e.getMessage(), e);
239                }
240        }
241
242
243        @Override
244        public HTTPRequest toHTTPRequest() {
245
246                if (getEndpointURI() == null)
247                        throw new SerializeException("The endpoint URI is not specified");
248
249                HTTPRequest httpRequest;
250
251                URL endpointURL;
252
253                try {
254                        endpointURL = getEndpointURI().toURL();
255
256                } catch (MalformedURLException e) {
257
258                        throw new SerializeException(e.getMessage(), e);
259                }
260
261                httpRequest = new HTTPRequest(HTTPRequest.Method.GET, endpointURL);
262
263                httpRequest.setQuery(toQueryString());
264
265                return httpRequest;
266        }
267
268
269        /**
270         * Parses a logout request from the specified parameters.
271         *
272         * <p>Example parameters:
273         *
274         * <pre>
275         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
276         * post_logout_redirect_uri = https://client.example.com/post-logout
277         * state = af0ifjsldkj
278         * </pre>
279         *
280         * @param params The parameters. Must not be {@code null}.
281         *
282         * @return The logout request.
283         *
284         * @throws ParseException If the parameters couldn't be parsed to a
285         *                        logout request.
286         */
287        public static LogoutRequest parse(final Map<String,String> params)
288                throws ParseException {
289
290                return parse(null, params);
291        }
292
293
294        /**
295         * Parses a logout request from the specified parameters.
296         *
297         * <p>Example parameters:
298         *
299         * <pre>
300         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
301         * post_logout_redirect_uri = https://client.example.com/post-logout
302         * state = af0ifjsldkj
303         * </pre>
304         *
305         * @param uri    The URI of the end-session endpoint. May be
306         *               {@code null} if the {@link #toHTTPRequest()} method
307         *               will not be used.
308         * @param params The parameters. Must not be {@code null}.
309         *
310         * @return The logout request.
311         *
312         * @throws ParseException If the parameters couldn't be parsed to a
313         *                        logout request.
314         */
315        public static LogoutRequest parse(final URI uri, final Map<String,String> params)
316                throws ParseException {
317
318                String v = params.get("id_token_hint");
319
320                if (StringUtils.isBlank(v))
321                        throw new ParseException("Missing \"id_token_hint\" parameter");
322
323                JWT idTokenHint;
324
325                try {
326                        idTokenHint = JWTParser.parse(v);
327                } catch (java.text.ParseException e) {
328                        throw new ParseException("Invalid ID token hint: " + e.getMessage(), e);
329                }
330
331                v = params.get("post_logout_redirect_uri");
332
333                URI postLogoutRedirectURI = null;
334
335                if (StringUtils.isNotBlank(v)) {
336
337                        try {
338                                postLogoutRedirectURI = new URI(v);
339                        } catch (URISyntaxException e) {
340                                throw new ParseException("Invalid \"post_logout_redirect_uri\" parameter: " + e.getMessage(),  e);
341                        }
342                }
343
344                State state = null;
345
346                v = params.get("state");
347
348                if (postLogoutRedirectURI != null && StringUtils.isNotBlank(v)) {
349                        state = new State(v);
350                }
351
352                return new LogoutRequest(uri, idTokenHint, postLogoutRedirectURI, state);
353        }
354
355
356        /**
357         * Parses a logout request from the specified URI query string.
358         *
359         * <p>Example URI query string:
360         *
361         * <pre>
362         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
363         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
364         * &amp;state=af0ifjsldkj
365         * </pre>
366         *
367         * @param query The URI query string. Must not be {@code null}.
368         *
369         * @return The logout request.
370         *
371         * @throws ParseException If the query string couldn't be parsed to a
372         *                        logout request.
373         */
374        public static LogoutRequest parse(final String query)
375                throws ParseException {
376
377                return parse(null, URLUtils.parseParameters(query));
378        }
379
380
381        /**
382         * Parses a logout request from the specified URI query string.
383         *
384         * <p>Example URI query string:
385         *
386         * <pre>
387         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
388         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
389         * &amp;state=af0ifjsldkj
390         * </pre>
391         *
392         * @param uri   The URI of the end-session endpoint. May be
393         *              {@code null} if the {@link #toHTTPRequest()} method
394         *              will not be used.
395         * @param query The URI query string. Must not be {@code null}.
396         *
397         * @return The logout request.
398         *
399         * @throws ParseException If the query string couldn't be parsed to a
400         *                        logout request.
401         */
402        public static LogoutRequest parse(final URI uri, final String query)
403                throws ParseException {
404
405                return parse(uri, URLUtils.parseParameters(query));
406        }
407
408
409        /**
410         * Parses a logout request from the specified URI.
411         *
412         * <p>Example URI:
413         *
414         * <pre>
415         * https://server.example.com/logout?
416         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
417         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
418         * &amp;state=af0ifjsldkj
419         * </pre>
420         *
421         * @param uri The URI. Must not be {@code null}.
422         *
423         * @return The logout request.
424         *
425         * @throws ParseException If the URI couldn't be parsed to a logout
426         *                        request.
427         */
428        public static LogoutRequest parse(final URI uri)
429                throws ParseException {
430
431                return parse(URIUtils.getBaseURI(uri), URLUtils.parseParameters(uri.getQuery()));
432        }
433
434
435        /**
436         * Parses a logout request from the specified HTTP request.
437         *
438         * <p>Example HTTP request (GET):
439         *
440         * <pre>
441         * https://server.example.com/logout?
442         * id_token_hint = eyJhbGciOiJSUzI1NiJ9.eyJpc3Mi...
443         * &amp;post_logout_redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fpost-logout
444         * &amp;state=af0ifjsldkj
445         * </pre>
446         *
447         * @param httpRequest The HTTP request. Must not be {@code null}.
448         *
449         * @return The logout request.
450         *
451         * @throws ParseException If the HTTP request couldn't be parsed to a
452         *                        logout request.
453         */
454        public static LogoutRequest parse(final HTTPRequest httpRequest)
455                throws ParseException {
456
457                String query = httpRequest.getQuery();
458
459                if (query == null)
460                        throw new ParseException("Missing URI query string");
461
462                try {
463                        return parse(URIUtils.getBaseURI(httpRequest.getURL().toURI()), query);
464
465                } catch (URISyntaxException e) {
466
467                        throw new ParseException(e.getMessage(), e);
468                }
469        }
470}