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