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