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