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