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