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