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