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