001 package com.nimbusds.oauth2.sdk.auth; 002 003 004 import java.util.HashMap; 005 import java.util.Map; 006 007 import javax.mail.internet.ContentType; 008 009 import net.minidev.json.JSONObject; 010 011 import com.nimbusds.jose.JWSAlgorithm; 012 import com.nimbusds.jwt.SignedJWT; 013 014 import com.nimbusds.oauth2.sdk.ParseException; 015 import com.nimbusds.oauth2.sdk.SerializeException; 016 import com.nimbusds.oauth2.sdk.id.ClientID; 017 import com.nimbusds.oauth2.sdk.http.CommonContentTypes; 018 import com.nimbusds.oauth2.sdk.http.HTTPRequest; 019 import com.nimbusds.oauth2.sdk.util.URLUtils; 020 021 022 /** 023 * Base abstract class for JSON Web Token (JWT) based client authentication at 024 * the Token endpoint. 025 * 026 * <p>Related specifications: 027 * 028 * <ul> 029 * <li>OAuth 2.0 (RFC 6749), section-3.2.1. 030 * <li>JSON Web Token (JWT) Bearer Token Profiles for OAuth 2.0 031 * (draft-ietf-oauth-jwt-bearer-04) 032 * </ul> 033 * 034 * @author Vladimir Dzhuvinov 035 */ 036 public abstract class JWTAuthentication extends ClientAuthentication { 037 038 039 /** 040 * The expected client assertion type, corresponding to the 041 * {@code client_assertion_type} parameter. This is a URN string set to 042 * "urn:ietf:params:oauth:client-assertion-type:jwt-bearer". 043 */ 044 public static final String CLIENT_ASSERTION_TYPE = 045 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; 046 047 048 /** 049 * The client assertion, corresponding to the {@link client_assertion} 050 * parameter. The assertion is in the form of a signed JWT. 051 */ 052 private final SignedJWT clientAssertion; 053 054 055 /** 056 * Optional client identifier, corresponding to the {@link client_id} 057 * parameter. 058 */ 059 private final ClientID clientID; 060 061 062 /** 063 * Creates a new JSON Web Token (JWT) based client authentication. 064 * 065 * @param method The client authentication method. Must not be 066 * {@code null}. 067 * @param clientAssertion The client assertion, corresponding to the 068 * {@code client_assertion} parameter, in the 069 * form of a signed JSON Web Token (JWT). Must 070 * not be {@code null}. 071 * @param clientID Optional client identifier, corresponding to 072 * the {@code client_id} parameter. {@code null} 073 * if not specified. 074 */ 075 protected JWTAuthentication(final ClientAuthenticationMethod method, 076 final SignedJWT clientAssertion, 077 final ClientID clientID) { 078 079 super(method); 080 081 if (clientAssertion == null) 082 throw new IllegalArgumentException("The client assertion JWT must not be null"); 083 084 this.clientAssertion = clientAssertion; 085 086 this.clientID = clientID; 087 } 088 089 090 /** 091 * Gets the client assertion, corresponding to the 092 * {@code client_assertion} parameter. 093 * 094 * @return The client assertion, in the form of a signed JSON Web Token 095 * (JWT). 096 */ 097 public SignedJWT getClientAssertion() { 098 099 return clientAssertion; 100 } 101 102 103 /** 104 * Gets the optional client identifier, corresponding to the 105 * {@code client_id} parameter. 106 * 107 * @return The client identifier, {@code null} if not specified. 108 */ 109 public ClientID getClientID() { 110 111 return clientID; 112 } 113 114 115 /** 116 * Gets the client authentication claims set contained in the client 117 * assertion JSON Web Token (JWT). 118 * 119 * @return The client authentication claims. 120 * 121 * @throws ParseException If the client assertion JSON Web Token (JWT) 122 * doesn't contain a client authentication 123 * claims set. 124 */ 125 public JWTAuthenticationClaimsSet getJWTAuthenticationClaimsSet() 126 throws ParseException { 127 128 JSONObject claimsSet = null; 129 130 try { 131 claimsSet = clientAssertion.getJWTClaimsSet().toJSONObject(); 132 133 } catch (java.text.ParseException e) { 134 135 throw new ParseException("Couldn't retrieve JSON object from the client assertion JWT"); 136 } 137 138 if (claimsSet == null) 139 throw new ParseException("Couldn't retrieve JSON object from the client assertion JWT"); 140 141 return JWTAuthenticationClaimsSet.parse(claimsSet); 142 } 143 144 145 /** 146 * Returns the parameter representation of this JSON Web Token (JWT) 147 * based client authentication. Note that the parameters are not 148 * {@code application/x-www-form-urlencoded} encoded. 149 * 150 * <p>Parameters map: 151 * 152 * <pre> 153 * "client_assertion" -> [serialised-JWT] 154 * "client_assertion_type" -> "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 155 * "client_id" -> [optional-client-id] 156 * </pre> 157 * 158 * @return The parameters map, with keys "client_assertion", 159 * "client_assertion_type" and "client_id". 160 * 161 * @throws SerializeException If the signed JWT couldn't be serialised 162 * to a client assertion string. 163 */ 164 public Map<String,String> toParameters() 165 throws SerializeException { 166 167 Map<String,String> params = new HashMap<String,String>(); 168 169 try { 170 params.put("client_assertion", clientAssertion.serialize()); 171 172 } catch (IllegalStateException e) { 173 174 throw new SerializeException("Couldn't serialize JWT to a client assertion string: " + e.getMessage(), e); 175 } 176 177 params.put("client_assertion_type", CLIENT_ASSERTION_TYPE); 178 179 if (clientID != null) 180 params.put("client_id", clientID.getValue()); 181 182 return params; 183 } 184 185 186 @Override 187 public void applyTo(final HTTPRequest httpRequest) 188 throws SerializeException { 189 190 if (httpRequest.getMethod() != HTTPRequest.Method.POST) 191 throw new SerializeException("The HTTP request method must be POST"); 192 193 ContentType ct = httpRequest.getContentType(); 194 195 if (ct == null) 196 throw new SerializeException("Missing HTTP Content-Type header"); 197 198 if (! ct.match(CommonContentTypes.APPLICATION_URLENCODED)) 199 throw new SerializeException("The HTTP Content-Type header must be " + CommonContentTypes.APPLICATION_URLENCODED); 200 201 Map <String,String> params = httpRequest.getQueryParameters(); 202 203 params.putAll(toParameters()); 204 205 String queryString = URLUtils.serializeParameters(params); 206 207 httpRequest.setQuery(queryString); 208 } 209 210 211 /** 212 * Ensures the specified parameters map contains an entry with key 213 * "client_assertion_type" pointing to a string that equals the expected 214 * {@link #CLIENT_ASSERTION_TYPE}. This method is intended to aid 215 * parsing of JSON Web Token (JWT) based client authentication objects. 216 * 217 * @param params The parameters map to check. The parameters must not be 218 * {@code null} and 219 * {@code application/x-www-form-urlencoded} encoded. 220 * 221 * @throws ParseException If expected "client_assertion_type" entry 222 * wasn't found. 223 */ 224 protected static void ensureClientAssertionType(final Map<String,String> params) 225 throws ParseException { 226 227 final String clientAssertionType = params.get("client_assertion_type"); 228 229 if (clientAssertionType == null) 230 throw new ParseException("Missing \"client_assertion_type\" parameter"); 231 232 if (! clientAssertionType.equals(CLIENT_ASSERTION_TYPE)) 233 throw new ParseException("Invalid \"client_assertion_type\" parameter, must be " + CLIENT_ASSERTION_TYPE); 234 } 235 236 237 /** 238 * Parses the specified parameters map for a client assertion. This 239 * method is intended to aid parsing of JSON Web Token (JWT) based 240 * client authentication objects. 241 * 242 * @param params The parameters map to parse. It must contain an entry 243 * with key "client_assertion" pointing to a string that 244 * represents a signed serialised JSON Web Token (JWT). 245 * The parameters must not be {@code null} and 246 * {@code application/x-www-form-urlencoded} encoded. 247 * 248 * @return The client assertion as a signed JSON Web Token (JWT). 249 * 250 * @throws ParseException If a "client_assertion" entry couldn't be 251 * retrieved from the parameters map. 252 */ 253 protected static SignedJWT parseClientAssertion(final Map<String,String> params) 254 throws ParseException { 255 256 final String clientAssertion = params.get("client_assertion"); 257 258 if (clientAssertion == null) 259 throw new ParseException("Missing \"client_assertion\" parameter"); 260 261 try { 262 return SignedJWT.parse(clientAssertion); 263 264 } catch (java.text.ParseException e) { 265 266 throw new ParseException("Invalid \"client_assertion\" JWT: " + e.getMessage(), e); 267 } 268 } 269 270 /** 271 * Parses the specified parameters map for an optional client 272 * identifier. This method is intended to aid parsing of JSON Web Token 273 * (JWT) based client authentication objects. 274 * 275 * @param params The parameters map to parse. It may contain an entry 276 * with key "client_id" pointing to a string that 277 * represents the client identifier. The parameters must 278 * not be {@code null} and 279 * {@code application/x-www-form-urlencoded} encoded. 280 * 281 * @return The client identifier, {@code null} if not specified. 282 */ 283 protected static ClientID parseClientID(final Map<String,String> params) { 284 285 String clientIDString = params.get("client_id"); 286 287 if (clientIDString == null) 288 return null; 289 290 else 291 return new ClientID(clientIDString); 292 } 293 294 295 /** 296 * Parses the specified HTTP request for a JSON Web Token (JWT) based 297 * client authentication. 298 * 299 * @param httpRequest The HTTP request to parse. Must not be {@code null}. 300 * 301 * @return The JSON Web Token (JWT) based client authentication. 302 * 303 * @throws ParseException If a JSON Web Token (JWT) based client 304 * authentication couldn't be retrieved from the 305 * HTTP request. 306 */ 307 public static JWTAuthentication parse(final HTTPRequest httpRequest) 308 throws ParseException { 309 310 httpRequest.ensureMethod(HTTPRequest.Method.POST); 311 httpRequest.ensureContentType(CommonContentTypes.APPLICATION_URLENCODED); 312 313 String query = httpRequest.getQuery(); 314 315 if (query == null) 316 throw new ParseException("Missing HTTP POST request entity body"); 317 318 Map<String,String> params = URLUtils.parseParameters(query); 319 320 JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm(); 321 322 if (ClientSecretJWT.getSupportedJWAs().contains(alg)) 323 return ClientSecretJWT.parse(params); 324 325 else if (PrivateKeyJWT.getSupportedJWAs().contains(alg)) 326 return PrivateKeyJWT.parse(params); 327 328 else 329 throw new ParseException("Unsupported signed JWT algorithm: " + alg); 330 } 331 }