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