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.SignedJWT; 025import com.nimbusds.oauth2.sdk.ParseException; 026import com.nimbusds.oauth2.sdk.SerializeException; 027import com.nimbusds.oauth2.sdk.http.HTTPRequest; 028import com.nimbusds.oauth2.sdk.id.ClientID; 029import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 030import com.nimbusds.oauth2.sdk.util.URLUtils; 031 032import java.util.*; 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 return new ClientID(subjectValue); 105 } 106 107 108 /** 109 * Creates a new JSON Web Token (JWT) based client authentication. 110 * 111 * @param method The client authentication method. Must not be 112 * {@code null}. 113 * @param clientAssertion The client assertion, corresponding to the 114 * {@code client_assertion} parameter, in the 115 * form of a signed JSON Web Token (JWT). Must 116 * be signed and not {@code null}. 117 * 118 * @throws IllegalArgumentException If the client assertion is not 119 * signed or doesn't conform to the 120 * expected format. 121 */ 122 protected JWTAuthentication(final ClientAuthenticationMethod method, 123 final SignedJWT clientAssertion) { 124 125 super(method, parseClientID(clientAssertion)); 126 127 if (! clientAssertion.getState().equals(JWSObject.State.SIGNED)) 128 throw new IllegalArgumentException("The client assertion JWT must be signed"); 129 130 this.clientAssertion = clientAssertion; 131 132 try { 133 jwtAuthClaimsSet = JWTAuthenticationClaimsSet.parse(clientAssertion.getJWTClaimsSet()); 134 135 } catch (Exception e) { 136 137 throw new IllegalArgumentException(e.getMessage(), e); 138 } 139 } 140 141 142 /** 143 * Gets the client assertion, corresponding to the 144 * {@code client_assertion} parameter. 145 * 146 * @return The client assertion, in the form of a signed JSON Web Token 147 * (JWT). 148 */ 149 public SignedJWT getClientAssertion() { 150 151 return clientAssertion; 152 } 153 154 155 /** 156 * Gets the client authentication claims set contained in the client 157 * assertion JSON Web Token (JWT). 158 * 159 * @return The client authentication claims. 160 */ 161 public JWTAuthenticationClaimsSet getJWTAuthenticationClaimsSet() { 162 163 return jwtAuthClaimsSet; 164 } 165 166 167 @Override 168 public Set<String> getFormParameterNames() { 169 170 return Collections.unmodifiableSet(new HashSet<>(Arrays.asList("client_assertion", "client_assertion_type", "client_id"))); 171 } 172 173 174 /** 175 * Returns the parameter representation of this JSON Web Token (JWT) 176 * based client authentication. Note that the parameters are not 177 * {@code application/x-www-form-urlencoded} encoded. 178 * 179 * <p>Parameters map: 180 * 181 * <pre> 182 * "client_assertion" = [serialised-JWT] 183 * "client_assertion_type" = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" 184 * </pre> 185 * 186 * @return The parameters map, with keys "client_assertion" and 187 * "client_assertion_type". 188 */ 189 public Map<String,List<String>> toParameters() { 190 191 Map<String,List<String>> params = new HashMap<>(); 192 193 try { 194 params.put("client_assertion", Collections.singletonList(clientAssertion.serialize())); 195 196 } catch (IllegalStateException e) { 197 198 throw new SerializeException("Couldn't serialize JWT to a client assertion string: " + e.getMessage(), e); 199 } 200 201 params.put("client_assertion_type", Collections.singletonList(CLIENT_ASSERTION_TYPE)); 202 203 return params; 204 } 205 206 207 @Override 208 public void applyTo(final HTTPRequest httpRequest) { 209 210 if (httpRequest.getMethod() != HTTPRequest.Method.POST) 211 throw new SerializeException("The HTTP request method must be POST"); 212 213 ContentType ct = httpRequest.getEntityContentType(); 214 215 if (ct == null) 216 throw new SerializeException("Missing HTTP Content-Type header"); 217 218 if (! ct.matches(ContentType.APPLICATION_URLENCODED)) 219 throw new SerializeException("The HTTP Content-Type header must be " + ContentType.APPLICATION_URLENCODED); 220 221 Map<String, List<String>> params; 222 try { 223 params = new LinkedHashMap<>(httpRequest.getBodyAsFormParameters()); 224 } catch (ParseException e) { 225 throw new SerializeException(e.getMessage(), e); 226 } 227 params.putAll(toParameters()); 228 229 httpRequest.setBody(URLUtils.serializeParameters(params)); 230 } 231 232 233 /** 234 * Ensures the specified parameters map contains an entry with key 235 * "client_assertion_type" pointing to a string that equals the expected 236 * {@link #CLIENT_ASSERTION_TYPE}. This method is intended to aid 237 * parsing of JSON Web Token (JWT) based client authentication objects. 238 * 239 * @param params The parameters map to check. The parameters must not be 240 * {@code null} and 241 * {@code application/x-www-form-urlencoded} encoded. 242 * 243 * @throws ParseException If expected "client_assertion_type" entry 244 * wasn't found. 245 */ 246 protected static void ensureClientAssertionType(final Map<String,List<String>> params) 247 throws ParseException { 248 249 final String clientAssertionType = MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"); 250 251 if (clientAssertionType == null) 252 throw new ParseException("Missing client_assertion_type parameter"); 253 254 if (! clientAssertionType.equals(CLIENT_ASSERTION_TYPE)) 255 throw new ParseException("Invalid client_assertion_type parameter, must be " + CLIENT_ASSERTION_TYPE); 256 } 257 258 259 /** 260 * Parses the specified parameters map for a client assertion. This 261 * method is intended to aid parsing of JSON Web Token (JWT) based 262 * client authentication objects. 263 * 264 * @param params The parameters map to parse. It must contain an entry 265 * with key "client_assertion" pointing to a string that 266 * represents a signed serialised JSON Web Token (JWT). 267 * The parameters must not be {@code null} and 268 * {@code application/x-www-form-urlencoded} encoded. 269 * 270 * @return The client assertion as a signed JSON Web Token (JWT). 271 * 272 * @throws ParseException If a "client_assertion" entry couldn't be 273 * retrieved from the parameters map. 274 */ 275 protected static SignedJWT parseClientAssertion(final Map<String,List<String>> params) 276 throws ParseException { 277 278 final String clientAssertion = MultivaluedMapUtils.getFirstValue(params, "client_assertion"); 279 280 if (clientAssertion == null) 281 throw new ParseException("Missing client_assertion parameter"); 282 283 try { 284 return SignedJWT.parse(clientAssertion); 285 286 } catch (java.text.ParseException e) { 287 288 throw new ParseException("Invalid client_assertion JWT: " + e.getMessage(), e); 289 } 290 } 291 292 /** 293 * Parses the specified parameters map for an optional client 294 * identifier. This method is intended to aid parsing of JSON Web Token 295 * (JWT) based client authentication objects. 296 * 297 * @param params The parameters map to parse. It may contain an entry 298 * with key "client_id" pointing to a string that 299 * represents the client identifier. The parameters must 300 * not be {@code null} and 301 * {@code application/x-www-form-urlencoded} encoded. 302 * 303 * @return The client identifier, {@code null} if not specified. 304 */ 305 protected static ClientID parseClientID(final Map<String,List<String>> params) { 306 307 String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id"); 308 309 return clientIDString != null ? new ClientID(clientIDString) : null; 310 } 311 312 313 /** 314 * Parses the specified HTTP request for a JSON Web Token (JWT) based 315 * client authentication. 316 * 317 * @param httpRequest The HTTP request to parse. Must not be {@code null}. 318 * 319 * @return The JSON Web Token (JWT) based client authentication. 320 * 321 * @throws ParseException If a JSON Web Token (JWT) based client 322 * authentication couldn't be retrieved from the 323 * HTTP request. 324 */ 325 public static JWTAuthentication parse(final HTTPRequest httpRequest) 326 throws ParseException { 327 328 httpRequest.ensureMethod(HTTPRequest.Method.POST); 329 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 330 331 String query = httpRequest.getQuery(); 332 333 if (query == null) 334 throw new ParseException("Missing HTTP POST request entity body"); 335 336 Map<String,List<String>> params = URLUtils.parseParameters(query); 337 338 JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm(); 339 340 if (ClientSecretJWT.supportedJWAs().contains(alg)) 341 return ClientSecretJWT.parse(params); 342 343 else if (PrivateKeyJWT.supportedJWAs().contains(alg)) 344 return PrivateKeyJWT.parse(params); 345 346 else 347 throw new ParseException("Unsupported signed JWT algorithm: " + alg); 348 } 349}