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.*; 022 023import com.nimbusds.common.contenttype.ContentType; 024import com.nimbusds.jose.JWSAlgorithm; 025import com.nimbusds.jose.JWSObject; 026import com.nimbusds.jwt.SignedJWT; 027import com.nimbusds.oauth2.sdk.ParseException; 028import com.nimbusds.oauth2.sdk.SerializeException; 029import com.nimbusds.oauth2.sdk.http.HTTPRequest; 030import com.nimbusds.oauth2.sdk.id.ClientID; 031import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 032import com.nimbusds.oauth2.sdk.util.URLUtils; 033 034 035/** 036 * Base abstract class for JSON Web Token (JWT) based client authentication at 037 * the Token endpoint. 038 * 039 * <p>Related specifications: 040 * 041 * <ul> 042 * <li>OAuth 2.0 (RFC 6749), section 3.2.1. 043 * <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and 044 * Authorization Grants (RFC 7523). 045 * <li>OpenID Connect Core 1.0, section 9. 046 * </ul> 047 */ 048public abstract class JWTAuthentication extends ClientAuthentication { 049 050 051 /** 052 * The expected client assertion type, corresponding to the 053 * {@code client_assertion_type} parameter. This is a URN string set to 054 * "urn:ietf:params:oauth:client-assertion-type:jwt-bearer". 055 */ 056 public static final String CLIENT_ASSERTION_TYPE = 057 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; 058 059 060 /** 061 * The client assertion, corresponding to the {@code client_assertion} 062 * parameter. The assertion is in the form of a signed JWT. 063 */ 064 private final SignedJWT clientAssertion; 065 066 067 /** 068 * The JWT authentication claims set for the client assertion. 069 */ 070 private final JWTAuthenticationClaimsSet jwtAuthClaimsSet; 071 072 073 /** 074 * Parses the client identifier from the specified signed JWT that 075 * represents a client assertion. 076 * 077 * @param jwt The signed JWT to parse. Must not be {@code null}. 078 * 079 * @return The parsed client identifier. 080 * 081 * @throws IllegalArgumentException If the client identifier couldn't 082 * be parsed. 083 */ 084 private static ClientID parseClientID(final SignedJWT jwt) { 085 086 String subjectValue; 087 String issuerValue; 088 089 try { 090 subjectValue = jwt.getJWTClaimsSet().getSubject(); 091 issuerValue = jwt.getJWTClaimsSet().getIssuer(); 092 093 } catch (java.text.ParseException e) { 094 095 throw new IllegalArgumentException(e.getMessage(), e); 096 } 097 098 if (subjectValue == null) 099 throw new IllegalArgumentException("Missing subject in client JWT assertion"); 100 101 if (issuerValue == null) 102 throw new IllegalArgumentException("Missing issuer in client JWT assertion"); 103 104 if (!subjectValue.equals(issuerValue)) 105 throw new IllegalArgumentException("Issuer and subject in client JWT assertion must designate the same client identifier"); 106 107 return new ClientID(subjectValue); 108 } 109 110 111 /** 112 * Creates a new JSON Web Token (JWT) based client authentication. 113 * 114 * @param method The client authentication method. Must not be 115 * {@code null}. 116 * @param clientAssertion The client assertion, corresponding to the 117 * {@code client_assertion} parameter, in the 118 * form of a signed JSON Web Token (JWT). Must 119 * be signed and not {@code null}. 120 * 121 * @throws IllegalArgumentException If the client assertion is not 122 * signed or doesn't conform to the 123 * expected format. 124 */ 125 protected JWTAuthentication(final ClientAuthenticationMethod method, 126 final SignedJWT clientAssertion) { 127 128 super(method, parseClientID(clientAssertion)); 129 130 if (! clientAssertion.getState().equals(JWSObject.State.SIGNED)) 131 throw new IllegalArgumentException("The client assertion JWT must be signed"); 132 133 this.clientAssertion = clientAssertion; 134 135 try { 136 jwtAuthClaimsSet = JWTAuthenticationClaimsSet.parse(clientAssertion.getJWTClaimsSet()); 137 138 } catch (Exception e) { 139 140 throw new IllegalArgumentException(e.getMessage(), e); 141 } 142 } 143 144 145 /** 146 * Gets the client assertion, corresponding to the 147 * {@code client_assertion} parameter. 148 * 149 * @return The client assertion, in the form of a signed JSON Web Token 150 * (JWT). 151 */ 152 public SignedJWT getClientAssertion() { 153 154 return clientAssertion; 155 } 156 157 158 /** 159 * Gets the client authentication claims set contained in the client 160 * assertion JSON Web Token (JWT). 161 * 162 * @return The client authentication claims. 163 */ 164 public JWTAuthenticationClaimsSet getJWTAuthenticationClaimsSet() { 165 166 return jwtAuthClaimsSet; 167 } 168 169 170 @Override 171 public Set<String> getFormParameterNames() { 172 173 return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("client_assertion", "client_assertion_type", "client_id"))); 174 } 175 176 177 /** 178 * Returns the parameter representation of this JSON Web Token (JWT) 179 * based client authentication. Note that the parameters are not 180 * {@code application/x-www-form-urlencoded} encoded. 181 * 182 * <p>Parameters map: 183 * 184 * <pre> 185 * "client_assertion" = [serialised-JWT] 186 * "client_assertion_type" = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 187 * </pre> 188 * 189 * @return The parameters map, with keys "client_assertion" and 190 * "client_assertion_type". 191 */ 192 public Map<String,List<String>> toParameters() { 193 194 Map<String,List<String>> params = new HashMap<>(); 195 196 try { 197 params.put("client_assertion", Collections.singletonList(clientAssertion.serialize())); 198 199 } catch (IllegalStateException e) { 200 201 throw new SerializeException("Couldn't serialize JWT to a client assertion string: " + e.getMessage(), e); 202 } 203 204 params.put("client_assertion_type", Collections.singletonList(CLIENT_ASSERTION_TYPE)); 205 206 return params; 207 } 208 209 210 @Override 211 public void applyTo(final HTTPRequest httpRequest) { 212 213 if (httpRequest.getMethod() != HTTPRequest.Method.POST) 214 throw new SerializeException("The HTTP request method must be POST"); 215 216 ContentType ct = httpRequest.getEntityContentType(); 217 218 if (ct == null) 219 throw new SerializeException("Missing HTTP Content-Type header"); 220 221 if (! ct.matches(ContentType.APPLICATION_URLENCODED)) 222 throw new SerializeException("The HTTP Content-Type header must be " + ContentType.APPLICATION_URLENCODED); 223 224 Map<String,List<String>> params = httpRequest.getQueryParameters(); 225 226 params.putAll(toParameters()); 227 228 String queryString = URLUtils.serializeParameters(params); 229 230 httpRequest.setQuery(queryString); 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 if (clientIDString == null) 311 return null; 312 313 else 314 return new ClientID(clientIDString); 315 } 316 317 318 /** 319 * Parses the specified HTTP request for a JSON Web Token (JWT) based 320 * client authentication. 321 * 322 * @param httpRequest The HTTP request to parse. Must not be {@code null}. 323 * 324 * @return The JSON Web Token (JWT) based client authentication. 325 * 326 * @throws ParseException If a JSON Web Token (JWT) based client 327 * authentication couldn't be retrieved from the 328 * HTTP request. 329 */ 330 public static JWTAuthentication parse(final HTTPRequest httpRequest) 331 throws ParseException { 332 333 httpRequest.ensureMethod(HTTPRequest.Method.POST); 334 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 335 336 String query = httpRequest.getQuery(); 337 338 if (query == null) 339 throw new ParseException("Missing HTTP POST request entity body"); 340 341 Map<String,List<String>> params = URLUtils.parseParameters(query); 342 343 JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm(); 344 345 if (ClientSecretJWT.supportedJWAs().contains(alg)) 346 return ClientSecretJWT.parse(params); 347 348 else if (PrivateKeyJWT.supportedJWAs().contains(alg)) 349 return PrivateKeyJWT.parse(params); 350 351 else 352 throw new ParseException("Unsupported signed JWT algorithm: " + alg); 353 } 354}