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