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.Collections; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025import javax.mail.internet.ContentType; 026 027import com.nimbusds.jose.JWSAlgorithm; 028import com.nimbusds.jose.JWSObject; 029import com.nimbusds.jwt.SignedJWT; 030import com.nimbusds.oauth2.sdk.ParseException; 031import com.nimbusds.oauth2.sdk.SerializeException; 032import com.nimbusds.oauth2.sdk.http.CommonContentTypes; 033import com.nimbusds.oauth2.sdk.http.HTTPRequest; 034import com.nimbusds.oauth2.sdk.id.ClientID; 035import com.nimbusds.oauth2.sdk.util.MultivaluedMapUtils; 036import com.nimbusds.oauth2.sdk.util.URLUtils; 037 038 039/** 040 * Base abstract class for JSON Web Token (JWT) based client authentication at 041 * the Token endpoint. 042 * 043 * <p>Related specifications: 044 * 045 * <ul> 046 * <li>OAuth 2.0 (RFC 6749), section 3.2.1. 047 * <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and 048 * Authorization Grants (RFC 7523). 049 * <li>OpenID Connect Core 1.0, section 9. 050 * </ul> 051 */ 052public abstract class JWTAuthentication extends ClientAuthentication { 053 054 055 /** 056 * The expected client assertion type, corresponding to the 057 * {@code client_assertion_type} parameter. This is a URN string set to 058 * "urn:ietf:params:oauth:client-assertion-type:jwt-bearer". 059 */ 060 public static final String CLIENT_ASSERTION_TYPE = 061 "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"; 062 063 064 /** 065 * The client assertion, corresponding to the {@code client_assertion} 066 * parameter. The assertion is in the form of a signed JWT. 067 */ 068 private final SignedJWT clientAssertion; 069 070 071 /** 072 * The JWT authentication claims set for the client assertion. 073 */ 074 private final JWTAuthenticationClaimsSet jwtAuthClaimsSet; 075 076 077 /** 078 * Parses the client identifier from the specified signed JWT that 079 * represents a client assertion. 080 * 081 * @param jwt The signed JWT to parse. Must not be {@code null}. 082 * 083 * @return The parsed client identifier. 084 * 085 * @throws IllegalArgumentException If the client identifier couldn't 086 * be parsed. 087 */ 088 private static ClientID parseClientID(final SignedJWT jwt) { 089 090 String subjectValue; 091 String issuerValue; 092 093 try { 094 subjectValue = jwt.getJWTClaimsSet().getSubject(); 095 issuerValue = jwt.getJWTClaimsSet().getIssuer(); 096 097 } catch (java.text.ParseException e) { 098 099 throw new IllegalArgumentException(e.getMessage(), e); 100 } 101 102 if (subjectValue == null) 103 throw new IllegalArgumentException("Missing subject in client JWT assertion"); 104 105 if (issuerValue == null) 106 throw new IllegalArgumentException("Missing issuer in client JWT assertion"); 107 108 if (!subjectValue.equals(issuerValue)) 109 throw new IllegalArgumentException("Issuer and subject in client JWT assertion must designate the same client identifier"); 110 111 return new ClientID(subjectValue); 112 } 113 114 115 /** 116 * Creates a new JSON Web Token (JWT) based client authentication. 117 * 118 * @param method The client authentication method. Must not be 119 * {@code null}. 120 * @param clientAssertion The client assertion, corresponding to the 121 * {@code client_assertion} parameter, in the 122 * form of a signed JSON Web Token (JWT). Must 123 * be signed and not {@code null}. 124 * 125 * @throws IllegalArgumentException If the client assertion is not 126 * signed or doesn't conform to the 127 * expected format. 128 */ 129 protected JWTAuthentication(final ClientAuthenticationMethod method, 130 final SignedJWT clientAssertion) { 131 132 super(method, parseClientID(clientAssertion)); 133 134 if (! clientAssertion.getState().equals(JWSObject.State.SIGNED)) 135 throw new IllegalArgumentException("The client assertion JWT must be signed"); 136 137 this.clientAssertion = clientAssertion; 138 139 try { 140 jwtAuthClaimsSet = JWTAuthenticationClaimsSet.parse(clientAssertion.getJWTClaimsSet()); 141 142 } catch (Exception e) { 143 144 throw new IllegalArgumentException(e.getMessage(), e); 145 } 146 } 147 148 149 /** 150 * Gets the client assertion, corresponding to the 151 * {@code client_assertion} parameter. 152 * 153 * @return The client assertion, in the form of a signed JSON Web Token 154 * (JWT). 155 */ 156 public SignedJWT getClientAssertion() { 157 158 return clientAssertion; 159 } 160 161 162 /** 163 * Gets the client authentication claims set contained in the client 164 * assertion JSON Web Token (JWT). 165 * 166 * @return The client authentication claims. 167 */ 168 public JWTAuthenticationClaimsSet getJWTAuthenticationClaimsSet() { 169 170 return jwtAuthClaimsSet; 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", 187 * "client_assertion_type" and "client_id". 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.getContentType(); 214 215 if (ct == null) 216 throw new SerializeException("Missing HTTP Content-Type header"); 217 218 if (! ct.match(CommonContentTypes.APPLICATION_URLENCODED)) 219 throw new SerializeException("The HTTP Content-Type header must be " + CommonContentTypes.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 if (clientIDString == null) 308 return null; 309 310 else 311 return new ClientID(clientIDString); 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 {@code null}. 320 * 321 * @return The JSON Web Token (JWT) based client authentication. 322 * 323 * @throws ParseException If a JSON Web Token (JWT) based client 324 * authentication couldn't be retrieved from the 325 * HTTP request. 326 */ 327 public static JWTAuthentication parse(final HTTPRequest httpRequest) 328 throws ParseException { 329 330 httpRequest.ensureMethod(HTTPRequest.Method.POST); 331 httpRequest.ensureContentType(CommonContentTypes.APPLICATION_URLENCODED); 332 333 String query = httpRequest.getQuery(); 334 335 if (query == null) 336 throw new ParseException("Missing HTTP POST request entity body"); 337 338 Map<String,List<String>> params = URLUtils.parseParameters(query); 339 340 JWSAlgorithm alg = parseClientAssertion(params).getHeader().getAlgorithm(); 341 342 if (ClientSecretJWT.supportedJWAs().contains(alg)) 343 return ClientSecretJWT.parse(params); 344 345 else if (PrivateKeyJWT.supportedJWAs().contains(alg)) 346 return PrivateKeyJWT.parse(params); 347 348 else 349 throw new ParseException("Unsupported signed JWT algorithm: " + alg); 350 } 351}