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