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.CollectionUtils; 027import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 028import com.nimbusds.oauth2.sdk.util.StringUtils; 029import com.nimbusds.oauth2.sdk.util.URLUtils; 030import com.nimbusds.openid.connect.sdk.nativesso.DeviceSecret; 031import com.nimbusds.openid.connect.sdk.nativesso.DeviceSecretToken; 032import net.jcip.annotations.Immutable; 033 034import java.net.URI; 035import java.util.*; 036 037 038/** 039 * Token revocation request. Used to revoke an issued access token, refresh 040 * token or device secret. 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&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&token_type_hint=refresh_token&client_id=123456 061 * </pre> 062 * 063 * <p>Related specifications: 064 * 065 * <ul> 066 * <li>OAuth 2.0 Token Revocation (RFC 7009) 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 * Optional additional parameters. 081 */ 082 private final Map<String,List<String>> customParams; 083 084 085 /** 086 * Creates a new token revocation request for a confidential client. 087 * 088 * @param endpoint The URI of the token revocation endpoint. May be 089 * {@code null} if the {@link #toHTTPRequest} method 090 * is not going to be used. 091 * @param clientAuth The client authentication. Must not be 092 * {@code null}. 093 * @param token The access token, refresh token or device secret 094 * to revoke. Must not be {@code null}. 095 */ 096 public TokenRevocationRequest(final URI endpoint, 097 final ClientAuthentication clientAuth, 098 final Token token) { 099 100 this(endpoint, Collections.singletonList(Objects.requireNonNull(clientAuth)), token); 101 } 102 103 104 /** 105 * Creates a new token revocation request for a confidential client. 106 * 107 * @param endpoint The URI of the token revocation 108 * endpoint. May be {@code null} if the 109 * {@link #toHTTPRequest} method is not 110 * going to be used. 111 * @param clientAuthCandidates The client authentication candidates. 112 * Must not be empty. 113 * @param token The access token, refresh token or 114 * device secret to revoke. Must not be 115 * {@code null}. 116 */ 117 public TokenRevocationRequest(final URI endpoint, 118 final List<ClientAuthentication> clientAuthCandidates, 119 final Token token) { 120 121 this(endpoint, clientAuthCandidates, token, null); 122 } 123 124 125 /** 126 * Creates a new token revocation request for a confidential client. 127 * 128 * @param endpoint The URI of the token revocation 129 * endpoint. May be {@code null} if the 130 * {@link #toHTTPRequest} method is not 131 * going to be used. 132 * @param clientAuthCandidates The client authentication candidates. 133 * Must not be empty. 134 * @param token The access token, refresh token or 135 * device secret to revoke. Must not be 136 * {@code null}. 137 * @param customParams Optional custom parameters, {@code null} 138 * if none. 139 */ 140 public TokenRevocationRequest(final URI endpoint, 141 final List<ClientAuthentication> clientAuthCandidates, 142 final Token token, 143 final Map<String,List<String>> customParams) { 144 145 super(endpoint, clientAuthCandidates); 146 if (clientAuthCandidates.isEmpty()) { 147 throw new IllegalArgumentException("The client authentication candidates must not be empty"); 148 } 149 this.token = Objects.requireNonNull(token); 150 this.customParams = customParams != null ? customParams : Collections.<String,List<String>>emptyMap(); 151 } 152 153 154 /** 155 * Creates a new token revocation request for a public client. 156 * 157 * @param endpoint The URI of the token revocation endpoint. May be 158 * {@code null} if the {@link #toHTTPRequest} method 159 * is not going to be used. 160 * @param clientID The client ID. Must not be {@code null}. 161 * @param token The access token, refresh token or device secret to 162 * revoke. Must not be {@code null}. 163 */ 164 public TokenRevocationRequest(final URI endpoint, 165 final ClientID clientID, 166 final Token token) { 167 168 this(endpoint, clientID, token, null); 169 } 170 171 172 /** 173 * Creates a new token revocation request for a public client. 174 * 175 * @param endpoint The URI of the token revocation endpoint. May be 176 * {@code null} if the {@link #toHTTPRequest} 177 * method is not going to be used. 178 * @param clientID The client ID. Must not be {@code null}. 179 * @param token The access token, refresh token or device secret 180 * to revoke. Must not be {@code null}. 181 * @param customParams Optional custom parameters, {@code null} if 182 * none. 183 */ 184 public TokenRevocationRequest(final URI endpoint, 185 final ClientID clientID, 186 final Token token, 187 final Map<String,List<String>> customParams) { 188 189 super(endpoint, Objects.requireNonNull(clientID)); 190 this.token = Objects.requireNonNull(token); 191 this.customParams = customParams != null ? customParams : Collections.<String,List<String>>emptyMap(); 192 } 193 194 195 /** 196 * Returns the token to revoke. The {@code instanceof} operator can be 197 * used to infer the token type. If it's neither 198 * {@link com.nimbusds.oauth2.sdk.token.AccessToken} nor 199 * {@link com.nimbusds.oauth2.sdk.token.RefreshToken} or 200 * {@link com.nimbusds.openid.connect.sdk.nativesso.DeviceSecretToken} 201 * the {@code token_type_hint} has not been provided as part of the 202 * token revocation request. 203 * 204 * @return The token. 205 */ 206 public Token getToken() { 207 208 return token; 209 } 210 211 212 /** 213 * Returns the custom request parameters. 214 * 215 * @return The custom request parameters, empty map if none. 216 */ 217 public Map<String,List<String>> getCustomParameters() { 218 219 return customParams; 220 } 221 222 223 @Override 224 public HTTPRequest toHTTPRequest() { 225 226 if (getEndpointURI() == null) 227 throw new SerializeException("The endpoint URI is not specified"); 228 229 HTTPRequest httpRequest = new HTTPRequest(HTTPRequest.Method.POST, getEndpointURI()); 230 httpRequest.setEntityContentType(ContentType.APPLICATION_URLENCODED); 231 232 Map<String,List<String>> params = new HashMap<>(); 233 234 if (getClientID() != null) { 235 // public client 236 params.put("client_id", Collections.singletonList(getClientID().getValue())); 237 } 238 239 params.put("token", Collections.singletonList(token.getValue())); 240 241 if (token instanceof AccessToken) { 242 params.put("token_type_hint", Collections.singletonList("access_token")); 243 } else if (token instanceof RefreshToken) { 244 params.put("token_type_hint", Collections.singletonList("refresh_token")); 245 } else if (token instanceof DeviceSecretToken) { 246 params.put("token_type_hint", Collections.singletonList("device_secret")); 247 } 248 249 params.putAll(customParams); 250 251 httpRequest.setBody(URLUtils.serializeParameters(params)); 252 253 if (getClientAuthentication() != null) { 254 // confidential client 255 for (ClientAuthentication ca: getClientAuthenticationCandidates()) { 256 ca.applyTo(httpRequest); 257 } 258 } 259 260 return httpRequest; 261 } 262 263 264 /** 265 * Parses a token revocation request from the specified HTTP request. 266 * 267 * @param httpRequest The HTTP request. Must not be {@code null}. 268 * 269 * @return The token revocation request. 270 * 271 * @throws ParseException If the HTTP request couldn't be parsed to a 272 * token revocation request. 273 */ 274 public static TokenRevocationRequest parse(final HTTPRequest httpRequest) 275 throws ParseException { 276 277 // Only HTTP POST accepted 278 httpRequest.ensureMethod(HTTPRequest.Method.POST); 279 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 280 281 Map<String,List<String>> params = httpRequest.getBodyAsFormParameters(); 282 283 final String tokenValue = MultivaluedMapUtils.removeAndReturnFirstValue(params,"token"); 284 285 if (StringUtils.isBlank(tokenValue)) { 286 throw new ParseException("Missing required token parameter"); 287 } 288 289 // Detect the token type 290 final String tokenTypeHint = MultivaluedMapUtils.removeAndReturnFirstValue(params,"token_type_hint"); 291 292 Token token; 293 if ("access_token".equals(tokenTypeHint)) { 294 token = new TypelessAccessToken(tokenValue); 295 } else if ("refresh_token".equals(tokenTypeHint)) { 296 token = new RefreshToken(tokenValue); 297 } else if ("device_secret".equals(tokenTypeHint)) { 298 token = new DeviceSecretToken(new DeviceSecret(tokenValue)); 299 } else { 300 // Any token 301 token = new TypelessToken(tokenValue); 302 } 303 304 URI uri = httpRequest.getURI(); 305 306 // Parse client authentication candidates, if any 307 List<ClientAuthentication> clientAuthCandidates; 308 try { 309 clientAuthCandidates = ClientAuthentication.parseCandidates(httpRequest); 310 } catch (ParseException e) { 311 throw new ParseException(e.getMessage(), OAuth2Error.INVALID_REQUEST.appendDescription(": " + e.getMessage())); 312 } 313 314 if (CollectionUtils.isNotEmpty(clientAuthCandidates)) { 315 316 for (ClientAuthentication ca: clientAuthCandidates) { 317 for (String formParam: ca.getFormParameterNames()) { 318 MultivaluedMapUtils.removeAndReturnFirstValue(params, formParam); 319 } 320 } 321 322 return new TokenRevocationRequest(uri, clientAuthCandidates, token, params); 323 } 324 325 // Public client 326 final String clientIDString = MultivaluedMapUtils.removeAndReturnFirstValue(params, "client_id"); 327 328 if (StringUtils.isBlank(clientIDString)) { 329 throw new ParseException("No client authentication or client_id parameter found"); 330 } 331 332 return new TokenRevocationRequest(uri, new ClientID(clientIDString), token, params); 333 } 334}