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 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 = httpRequest.getQueryParameters(); 222 223 params.putAll(toParameters()); 224 225 String queryString = URLUtils.serializeParameters(params); 226 227 httpRequest.setQuery(queryString); 228 } 229 230 231 /** 232 * Ensures the specified parameters map contains an entry with key 233 * "client_assertion_type" pointing to a string that equals the expected 234 * {@link #CLIENT_ASSERTION_TYPE}. This method is intended to aid 235 * parsing of JSON Web Token (JWT) based client authentication objects. 236 * 237 * @param params The parameters map to check. The parameters must not be 238 * {@code null} and 239 * {@code application/x-www-form-urlencoded} encoded. 240 * 241 * @throws ParseException If expected "client_assertion_type" entry 242 * wasn't found. 243 */ 244 protected static void ensureClientAssertionType(final Map<String,List<String>> params) 245 throws ParseException { 246 247 final String clientAssertionType = MultivaluedMapUtils.getFirstValue(params, "client_assertion_type"); 248 249 if (clientAssertionType == null) 250 throw new ParseException("Missing client_assertion_type parameter"); 251 252 if (! clientAssertionType.equals(CLIENT_ASSERTION_TYPE)) 253 throw new ParseException("Invalid client_assertion_type parameter, must be " + CLIENT_ASSERTION_TYPE); 254 } 255 256 257 /** 258 * Parses the specified parameters map for a client assertion. This 259 * method is intended to aid parsing of JSON Web Token (JWT) based 260 * client authentication objects. 261 * 262 * @param params The parameters map to parse. It must contain an entry 263 * with key "client_assertion" pointing to a string that 264 * represents a signed serialised JSON Web Token (JWT). 265 * The parameters must not be {@code null} and 266 * {@code application/x-www-form-urlencoded} encoded. 267 * 268 * @return The client assertion as a signed JSON Web Token (JWT). 269 * 270 * @throws ParseException If a "client_assertion" entry couldn't be 271 * retrieved from the parameters map. 272 */ 273 protected static SignedJWT parseClientAssertion(final Map<String,List<String>> params) 274 throws ParseException { 275 276 final String clientAssertion = MultivaluedMapUtils.getFirstValue(params, "client_assertion"); 277 278 if (clientAssertion == null) 279 throw new ParseException("Missing client_assertion parameter"); 280 281 try { 282 return SignedJWT.parse(clientAssertion); 283 284 } catch (java.text.ParseException e) { 285 286 throw new ParseException("Invalid client_assertion JWT: " + e.getMessage(), e); 287 } 288 } 289 290 /** 291 * Parses the specified parameters map for an optional client 292 * identifier. This method is intended to aid parsing of JSON Web Token 293 * (JWT) based client authentication objects. 294 * 295 * @param params The parameters map to parse. It may contain an entry 296 * with key "client_id" pointing to a string that 297 * represents the client identifier. The parameters must 298 * not be {@code null} and 299 * {@code application/x-www-form-urlencoded} encoded. 300 * 301 * @return The client identifier, {@code null} if not specified. 302 */ 303 protected static ClientID parseClientID(final Map<String,List<String>> params) { 304 305 String clientIDString = MultivaluedMapUtils.getFirstValue(params, "client_id"); 306 307 return clientIDString != null ? new ClientID(clientIDString) : null; 308 } 309 310 311 /** 312 * Parses the specified HTTP request for a JSON Web Token (JWT) based 313 * client authentication. 314 * 315 * @param httpRequest The HTTP request to parse. Must not be {@code null}. 316 * 317 * @return The JSON Web Token (JWT) based client authentication. 318 * 319 * @throws ParseException If a JSON Web Token (JWT) based client 320 * authentication couldn't be retrieved from the 321 * HTTP request. 322 */ 323 public static JWTAuthentication parse(final HTTPRequest httpRequest) 324 throws ParseException { 325 326 httpRequest.ensureMethod(HTTPRequest.Method.POST); 327 httpRequest.ensureEntityContentType(ContentType.APPLICATION_URLENCODED); 328 329 String query = httpRequest.getQuery(); 330 331 if (query == null) 332 throw new ParseException("Missing HTTP POST request entity body"); 333 334 Map<String,List<String>> params = URLUtils.parseParameters(query); 335 336 JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm(); 337 338 if (ClientSecretJWT.supportedJWAs().contains(alg)) 339 return ClientSecretJWT.parse(params); 340 341 else if (PrivateKeyJWT.supportedJWAs().contains(alg)) 342 return PrivateKeyJWT.parse(params); 343 344 else 345 throw new ParseException("Unsupported signed JWT algorithm: " + alg); 346 } 347}