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