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                super(endpoint, Objects.requireNonNull(clientAuth));
094                this.token = Objects.requireNonNull(token);
095        }
096
097
098        /**
099         * Creates a new token revocation request for a public client.
100         *
101         * @param endpoint The URI of the token revocation endpoint. May be
102         *                 {@code null} if the {@link #toHTTPRequest} method
103         *                 is not going to be used.
104         * @param clientID The client ID. Must not be {@code null}.
105         * @param token    The access token, refresh token or device secret to
106         *                 revoke. Must not be {@code null}.
107         */
108        public TokenRevocationRequest(final URI endpoint,
109                                      final ClientID clientID,
110                                      final Token token) {
111
112                super(endpoint, Objects.requireNonNull(clientID));
113                this.token = Objects.requireNonNull(token);
114        }
115
116
117        /**
118         * Returns the token to revoke. The {@code instanceof} operator can be
119         * used to infer the token type. If it's neither
120         * {@link com.nimbusds.oauth2.sdk.token.AccessToken} nor
121         * {@link com.nimbusds.oauth2.sdk.token.RefreshToken} or
122         * {@link com.nimbusds.openid.connect.sdk.nativesso.DeviceSecretToken}
123         * the {@code token_type_hint} has not been provided as part of the
124         * token revocation request.
125         *
126         * @return The token.
127         */
128        public Token getToken() {
129
130                return token;
131        }
132
133
134        @Override
135        public HTTPRequest toHTTPRequest() {
136
137                if (getEndpointURI() == null)
138                        throw new SerializeException("The endpoint URI is not specified");
139
140                HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI());
141                httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED);
142
143                Map<String,List<String>> params = new HashMap<>();
144
145                if (getClientID() != null) {
146                        // public client
147                        params.put("client_id", Collections.singletonList(getClientID().getValue()));
148                }
149
150                params.put("token", Collections.singletonList(token.getValue()));
151
152                if (token instanceof AccessToken) {
153                        params.put("token_type_hint", Collections.singletonList("access_token"));
154                } else if (token instanceof RefreshToken) {
155                        params.put("token_type_hint", Collections.singletonList("refresh_token"));
156                } else if (token instanceof DeviceSecretToken) {
157                        params.put("token_type_hint", Collections.singletonList("device_secret"));
158                }
159
160                httpRequest.setBody(URLUtils.serializeParameters(params));
161
162                if (getClientAuthentication() != null) {
163                        // confidential client
164                        getClientAuthentication().applyTo(httpRequest);
165                }
166
167                return httpRequest;
168        }
169
170
171        /**
172         * Parses a token revocation request from the specified HTTP request.
173         *
174         * @param httpRequest The HTTP request. Must not be {@code null}.
175         *
176         * @return The token revocation request.
177         *
178         * @throws ParseException If the HTTP request couldn't be parsed to a
179         *                        token revocation request.
180         */
181        public static TokenRevocationRequest parse(final HTTPRequest httpRequest)
182                throws ParseException {
183
184                // Only HTTP POST accepted
185                httpRequest.ensureMethod(HTTPRequest.Method.POST);
186                httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED);
187
188                Map<String,List<String>> params = httpRequest.getBodyAsFormParameters();
189
190                final String tokenValue = MultivaluedMapUtils.getFirstValue(params,"token");
191
192                if (StringUtils.isBlank(tokenValue)) {
193                        throw new ParseException("Missing required token parameter");
194                }
195
196                // Detect the token type
197                final String tokenTypeHint = MultivaluedMapUtils.getFirstValue(params,"token_type_hint");
198
199                Token token;
200                if ("access_token".equals(tokenTypeHint)) {
201                        token = new TypelessAccessToken(tokenValue);
202                } else if ("refresh_token".equals(tokenTypeHint)) {
203                        token = new RefreshToken(tokenValue);
204                } else if ("device_secret".equals(tokenTypeHint)) {
205                        token = new DeviceSecretToken(new DeviceSecret(tokenValue));
206                } else {
207                        // Any token
208                        token = new TypelessToken(tokenValue);
209                }
210
211                URI uri = httpRequest.getURI();
212
213                // Parse client auth
214                ClientAuthentication clientAuth = ClientAuthentication.parse(httpRequest);
215
216                if (clientAuth != null) {
217                        return new TokenRevocationRequest(uri, clientAuth, token);
218                }
219
220                // Public client
221                final String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id");
222
223                if (StringUtils.isBlank(clientIDString)) {
224                        throw new ParseException("Invalid token revocation request: No client authentication or client_id parameter found");
225                }
226
227                return new TokenRevocationRequest(uri, new ClientID(clientIDString), token);
228        }
229}