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.oauth2.sdk;
019
020
021import java.net.MalformedURLException;
022import java.net.URI;
023import java.net.URISyntaxException;
024import java.net.URL;
025import java.util.*;
026
027import com.nimbusds.oauth2.sdk.auth.ClientAuthentication;
028import com.nimbusds.oauth2.sdk.http.CommonContentTypes;
029import com.nimbusds.oauth2.sdk.http.HTTPRequest;
030import com.nimbusds.oauth2.sdk.id.ClientID;
031import com.nimbusds.oauth2.sdk.token.AccessToken;
032import com.nimbusds.oauth2.sdk.token.RefreshToken;
033import com.nimbusds.oauth2.sdk.token.Token;
034import com.nimbusds.oauth2.sdk.token.TypelessAccessToken;
035import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils;
036import com.nimbusds.oauth2.sdk.util.StringUtils;
037import com.nimbusds.oauth2.sdk.util.URLUtils;
038import net.jcip.annotations.Immutable;
039import net.minidev.json.JSONObject;
040
041
042/**
043 * Token revocation request. Used to revoke an issued access or refresh token.
044 *
045 * <p>Example token revocation request for a confidential client:
046 *
047 * <pre>
048 * POST /revoke HTTP/1.1
049 * Host: server.example.com
050 * Content-Type: application/x-www-form-urlencoded
051 * Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
052 *
053 * token=45ghiukldjahdnhzdauz&amp;token_type_hint=refresh_token
054 * </pre>
055 *
056 * <p>Example token revocation request for a public client:
057 *
058 * <pre>
059 * POST /revoke HTTP/1.1
060 * Host: server.example.com
061 * Content-Type: application/x-www-form-urlencoded
062 *
063 * token=45ghiukldjahdnhzdauz&amp;token_type_hint=refresh_token&amp;client_id=123456
064 * </pre>
065 *
066 * <p>Related specifications:
067 *
068 * <ul>
069 *     <li>OAuth 2.0 Token Revocation (RFC 7009), section 2.1.
070 * </ul>
071 */
072@Immutable
073public final class TokenRevocationRequest extends AbstractOptionallyIdentifiedRequest {
074
075
076        /**
077         * The token to revoke.
078         */
079        private final Token token;
080
081
082        /**
083         * Creates a new token revocation request for a confidential client.
084         *
085         * @param uri        The URI of the token revocation endpoint. May be
086         *                   {@code null} if the {@link #toHTTPRequest} method
087         *                   will not be used.
088         * @param clientAuth The client authentication. Must not be
089         *                   {@code null}.
090         * @param token      The access or refresh token to revoke. Must not be
091         *                   {@code null}.
092         */
093        public TokenRevocationRequest(final URI uri,
094                                      final ClientAuthentication clientAuth,
095                                      final Token token) {
096
097                super(uri, clientAuth);
098
099                if (clientAuth == null) {
100                        throw new IllegalArgumentException("The client authentication must not be null");
101                }
102
103                if (token == null)
104                        throw new IllegalArgumentException("The token must not be null");
105
106                this.token = token;
107        }
108
109
110        /**
111         * Creates a new token revocation request for a public client.
112         *
113         * @param uri      The URI of the token revocation endpoint. May be
114         *                 {@code null} if the {@link #toHTTPRequest} method
115         *                 will not be used.
116         * @param clientID The client ID. Must not be {@code null}.
117         * @param token    The access or refresh token to revoke. Must not be
118         *                 {@code null}.
119         */
120        public TokenRevocationRequest(final URI uri,
121                                      final ClientID clientID,
122                                      final Token token) {
123
124                super(uri, clientID);
125
126                if (clientID == null) {
127                        throw new IllegalArgumentException("The client ID must not be null");
128                }
129
130                if (token == null)
131                        throw new IllegalArgumentException("The token must not be null");
132
133                this.token = token;
134        }
135
136
137        /**
138         * Returns the token to revoke. The {@code instanceof} operator can be
139         * used to infer the token type. If it's neither
140         * {@link com.nimbusds.oauth2.sdk.token.AccessToken} nor
141         * {@link com.nimbusds.oauth2.sdk.token.RefreshToken} the
142         * {@code token_type_hint} has not been provided as part of the token
143         * revocation request.
144         *
145         * @return The token.
146         */
147        public Token getToken() {
148
149                return token;
150        }
151
152
153        @Override
154        public HTTPRequest toHTTPRequest() {
155
156                if (getEndpointURI() == null)
157                        throw new SerializeException("The endpoint URI is not specified");
158
159                URL url;
160
161                try {
162                        url = getEndpointURI().toURL();
163
164                } catch (MalformedURLException e) {
165
166                        throw new SerializeException(e.getMessage(), e);
167                }
168
169                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, url);
170                httpRequest.setContentType(CommonContentTypes.APPLICATION_URLENCODED);
171
172                Map<String,List<String>> params = new HashMap<>();
173
174                if (getClientID() != null) {
175                        // public client
176                        params.put("client_id", Collections.singletonList(getClientID().getValue()));
177                }
178
179                params.put("token", Collections.singletonList(token.getValue()));
180
181                if (token instanceof AccessToken) {
182                        params.put("token_type_hint", Collections.singletonList("access_token"));
183                } else if (token instanceof RefreshToken) {
184                        params.put("token_type_hint", Collections.singletonList("refresh_token"));
185                }
186
187                httpRequest.setQuery(URLUtils.serializeParameters(params));
188
189                if (getClientAuthentication() != null) {
190                        // confidential client
191                        getClientAuthentication().applyTo(httpRequest);
192                }
193
194                return httpRequest;
195        }
196
197
198        /**
199         * Parses a token revocation request from the specified HTTP request.
200         *
201         * @param httpRequest The HTTP request. Must not be {@code null}.
202         *
203         * @return The token revocation request.
204         *
205         * @throws ParseException If the HTTP request couldn't be parsed to a
206         *                        token revocation request.
207         */
208        public static TokenRevocationRequest parse(final HTTPRequest httpRequest)
209                throws ParseException {
210
211                // Only HTTP POST accepted
212                httpRequest.ensureMethod(HTTPRequest.Method.POST);
213                httpRequest.ensureContentType(CommonContentTypes.APPLICATION_URLENCODED);
214
215                Map<String,List<String>> params = httpRequest.getQueryParameters();
216
217                final String tokenValue = MultivaluedMapUtils.getFirstValue(params,"token");
218
219                if (tokenValue == null || tokenValue.isEmpty()) {
220                        throw new ParseException("Missing required token parameter");
221                }
222
223                // Detect the token type
224                Token token = null;
225
226                final String tokenTypeHint = MultivaluedMapUtils.getFirstValue(params,"token_type_hint");
227
228                if (tokenTypeHint == null) {
229
230                        // Can be both access or refresh token
231                        token = new Token() {
232
233                                @Override
234                                public String getValue() {
235
236                                        return tokenValue;
237                                }
238
239                                @Override
240                                public Set<String> getParameterNames() {
241
242                                        return Collections.emptySet();
243                                }
244
245                                @Override
246                                public JSONObject toJSONObject() {
247
248                                        return new JSONObject();
249                                }
250
251                                @Override
252                                public boolean equals(final Object other) {
253
254                                        return other instanceof Token && other.toString().equals(tokenValue);
255                                }
256                        };
257
258                } else if (tokenTypeHint.equals("access_token")) {
259
260                        token = new TypelessAccessToken(tokenValue);
261
262                } else if (tokenTypeHint.equals("refresh_token")) {
263
264                        token = new RefreshToken(tokenValue);
265                }
266
267                URI uri;
268
269                try {
270                        uri = httpRequest.getURL().toURI();
271
272                } catch (URISyntaxException e) {
273
274                        throw new ParseException(e.getMessage(), e);
275                }
276
277                // Parse client auth
278                ClientAuthentication clientAuth = ClientAuthentication.parse(httpRequest);
279
280                if (clientAuth != null) {
281                        return new TokenRevocationRequest(uri, clientAuth, token);
282                }
283
284                // Public client
285                final String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
286
287                if (StringUtils.isBlank(clientIDString)) {
288                        throw new ParseException("Invalid token revocation request: No client authentication or client_id parameter found");
289                }
290
291                return new TokenRevocationRequest(uri, new ClientID(clientIDString), token);
292        }
293}