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 net.jcip.annotations.Immutable;
031
032import com.nimbusds.common.contenttype.ContentType;
033import com.nimbusds.jwt.JWT;
034import com.nimbusds.jwt.JWTParser;
035import com.nimbusds.jwt.PlainJWT;
036import com.nimbusds.oauth2.sdk.AbstractRequest;
037import com.nimbusds.oauth2.sdk.ParseException;
038import com.nimbusds.oauth2.sdk.SerializeException;
039import com.nimbusds.oauth2.sdk.http.HTTPRequest;
040import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
041import com.nimbusds.oauth2.sdk.util.URIUtils;
042import com.nimbusds.oauth2.sdk.util.URLUtils;
043
044
045/**
046 * Back-channel logout request initiated by an OpenID provider (OP).
047 *
048 * <p>Example HTTP request:
049 *
050 * <pre>
051 * POST /backchannel_logout HTTP/1.1
052 * Host: rp.example.org
053 * Content-Type: application/x-www-form-urlencoded
054 *
055 * logout_token=eyJhbGci ... .eyJpc3Mi ... .T3BlbklE ...
056 * </pre>
057 *
058 * <p>Related specifications:
059 *
060 * <ul>
061 *     <li>OpenID Connect Back-Channel Logout 1.0, section 2.5 (draft 04).
062 * </ul>
063 */
064@Immutable
065public class BackChannelLogoutRequest extends AbstractRequest {
066        
067        
068        /**
069         * The logout token.
070         */
071        private final JWT logoutToken;
072        
073        
074        /**
075         * Creates a new back-channel logout request.
076         *
077         * @param uri         The back-channel logout URI. May be {@code null}
078         *                    if the {@link #toHTTPRequest} method will not be
079         *                    used.
080         * @param logoutToken The logout token. Must be signed, or signed and
081         *                    encrypted. Must not be {@code null}.
082         */
083        public BackChannelLogoutRequest(final URI uri,
084                                        final JWT logoutToken) {
085                
086                super(uri);
087                
088                if (logoutToken == null) {
089                        throw new IllegalArgumentException("The logout token must not be null");
090                }
091                
092                if (logoutToken instanceof PlainJWT) {
093                        throw new IllegalArgumentException("The logout token must not be unsecured (plain)");
094                }
095                
096                this.logoutToken = logoutToken;
097        }
098        
099        
100        /**
101         * Returns the logout token.
102         *
103         * @return The logout token.
104         */
105        public JWT getLogoutToken() {
106                
107                return logoutToken;
108        }
109        
110        
111        /**
112         * Returns the parameters for this back-channel logout request.
113         *
114         * <p>Example parameters:
115         *
116         * <pre>
117         * logout_token=eyJhbGci ... .eyJpc3Mi ... .T3BlbklE ...
118         * </pre>
119         *
120         * @return The parameters.
121         */
122        public Map<String,List<String>> toParameters() {
123                
124                Map <String,List<String>> params = new LinkedHashMap<>();
125                
126                try {
127                        params.put("logout_token", Collections.singletonList(logoutToken.serialize()));
128                } catch (IllegalStateException e) {
129                        throw new SerializeException("Couldn't serialize logout token: " + e.getMessage(), e);
130                }
131                
132                return params;
133        }
134        
135        
136        @Override
137        public HTTPRequest toHTTPRequest() {
138                
139                if (getEndpointURI() == null)
140                        throw new SerializeException("The endpoint URI is not specified");
141                
142                HTTPRequest httpRequest;
143                
144                URL endpointURL;
145                
146                try {
147                        endpointURL = getEndpointURI().toURL();
148                        
149                } catch (MalformedURLException e) {
150                        
151                        throw new SerializeException(e.getMessage(), e);
152                }
153                
154                httpRequest = new HTTPRequest(HTTPRequest.Method.POST, endpointURL);
155                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
156                httpRequest.setQuery(URLUtils.serializeParameters(toParameters()));
157                
158                return httpRequest;
159        }
160        
161        
162        /**
163         * Parses a back-channel logout request from the specified request body
164         * parameters.
165         *
166         * <p>Example parameters:
167         *
168         * <pre>
169         * logout_token = eyJhbGci ... .eyJpc3Mi ... .T3BlbklE ...
170         * </pre>
171         *
172         * @param params The parameters. Must not be {@code null}.
173         *
174         * @return The back-channel logout request.
175         *
176         * @throws ParseException If the parameters couldn't be parsed to a
177         *                        back-channel logout request.
178         */
179        public static BackChannelLogoutRequest parse(final Map<String,List<String>> params)
180                throws ParseException {
181                
182                return parse(null, params);
183        }
184        
185        
186        /**
187         * Parses a back-channel logout request from the specified URI and
188         * request body parameters.
189         *
190         * <p>Example parameters:
191         *
192         * <pre>
193         * logout_token = eyJhbGci ... .eyJpc3Mi ... .T3BlbklE ...
194         * </pre>
195         *
196         * @param uri    The back-channel logout URI. May be {@code null} if
197         *               the {@link #toHTTPRequest()} method will not be used.
198         * @param params The parameters. Must not be {@code null}.
199         *
200         * @return The back-channel logout request.
201         *
202         * @throws ParseException If the parameters couldn't be parsed to a
203         *                        back-channel logout request.
204         */
205        public static BackChannelLogoutRequest parse(final URI uri, Map<String,List<String>> params)
206                throws ParseException {
207                
208                String logoutTokenString = MultivaluedMapUtils.getFirstValue(params, "logout_token");
209                
210                if (logoutTokenString == null) {
211                        throw new ParseException("Missing logout_token parameter");
212                }
213                
214                JWT logoutToken;
215                
216                try {
217                        logoutToken = JWTParser.parse(logoutTokenString);
218                } catch (java.text.ParseException e) {
219                        throw new ParseException("Invalid logout token: " + e.getMessage(), e);
220                }
221                
222                try {
223                        return new BackChannelLogoutRequest(uri, logoutToken);
224                } catch (IllegalArgumentException e) {
225                        throw new ParseException(e.getMessage(), e);
226                }
227        }
228        
229        
230        /**
231         * Parses a back-channel logout request from the specified HTTP request.
232         *
233         * <p>Example HTTP request (POST):
234         *
235         * <pre>
236         * POST /backchannel_logout HTTP/1.1
237         * Host: rp.example.org
238         * Content-Type: application/x-www-form-urlencoded
239         *
240         * logout_token=eyJhbGci ... .eyJpc3Mi ... .T3BlbklE ...
241         * </pre>
242         *
243         * @param httpRequest The HTTP request. Must not be {@code null}.
244         *
245         * @return The back-channel logout request.
246         *
247         * @throws ParseException If the HTTP request couldn't be parsed to a
248         *                        back-channel logout request.
249         */
250        public static BackChannelLogoutRequest parse(final HTTPRequest httpRequest)
251                throws ParseException {
252                
253                if (! HTTPRequest.Method.POST.equals(httpRequest.getMethod())) {
254                        throw new ParseException("HTTP POST required");
255                }
256                
257                // Lenient on content-type
258                
259                String query = httpRequest.getQuery();
260                
261                if (query == null)
262                        throw new ParseException("Missing URI query string");
263                
264                Map<String,List<String>> params = URLUtils.parseParameters(query);
265                
266                try {
267                        return parse(URIUtils.getBaseURI(httpRequest.getURL().toURI()), params);
268                        
269                } catch (URISyntaxException e) {
270                        
271                        throw new ParseException(e.getMessage(), e);
272                }
273        }
274}