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