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