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