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