001package com.nimbusds.oauth2.sdk.auth; 002 003 004import java.util.Collections; 005import java.util.Date; 006import java.util.LinkedHashSet; 007import java.util.LinkedList; 008import java.util.List; 009import java.util.Set; 010 011import net.minidev.json.JSONObject; 012 013import com.nimbusds.jwt.JWTClaimsSet; 014import com.nimbusds.jwt.ReadOnlyJWTClaimsSet; 015 016import com.nimbusds.oauth2.sdk.ParseException; 017import com.nimbusds.oauth2.sdk.id.Audience; 018import com.nimbusds.oauth2.sdk.id.ClientID; 019import com.nimbusds.oauth2.sdk.id.Issuer; 020import com.nimbusds.oauth2.sdk.id.JWTID; 021import com.nimbusds.oauth2.sdk.id.Subject; 022import com.nimbusds.oauth2.sdk.util.DateUtils; 023import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 024 025 026/** 027 * JWT client authentication claims set, serialisable to a JSON object and JWT 028 * claims set. This class is immutable. 029 * 030 * <p>Used for {@link ClientSecretJWT client secret JWT} and 031 * {@link PrivateKeyJWT private key JWT} authentication at the Token endpoint. 032 * 033 * <p>Example client authentication claims set: 034 * 035 * <pre> 036 * { 037 * "iss" : "http://client.example.com", 038 * "sub" : "http://client.example.com", 039 * "aud" : [ "http://idp.example.com/token" ], 040 * "jti" : "d396036d-c4d9-40d8-8e98-f7e8327002d9", 041 * "exp" : 1311281970, 042 * "iat" : 1311280970 043 * } 044 * </pre> 045 * 046 * <p>Related specifications: 047 * 048 * <ul> 049 * <li>OAuth 2.0 (RFC 6749), section-3.2.1. 050 * <li>JSON Web Token (JWT) Bearer Token Profiles for OAuth 2.0 051 * (draft-ietf-oauth-jwt-bearer-06) 052 * </ul> 053 * 054 * @author Vladimir Dzhuvinov 055 */ 056public class JWTAuthenticationClaimsSet { 057 058 059 /** 060 * The names of the reserved client authentication claims. 061 */ 062 private static final Set<String> reservedClaimNames = new LinkedHashSet<String>(); 063 064 065 static { 066 reservedClaimNames.add("iss"); 067 reservedClaimNames.add("sub"); 068 reservedClaimNames.add("aud"); 069 reservedClaimNames.add("exp"); 070 reservedClaimNames.add("nbf"); 071 reservedClaimNames.add("iat"); 072 reservedClaimNames.add("jti"); 073 } 074 075 076 /** 077 * Gets the names of the reserved client authentication claims. 078 * 079 * @return The names of the reserved client authentication claims 080 * (read-only set). 081 */ 082 public static Set<String> getReservedClaimNames() { 083 084 return Collections.unmodifiableSet(reservedClaimNames); 085 } 086 087 088 /** 089 * The issuer (required). 090 */ 091 private final Issuer iss; 092 093 094 /** 095 * The subject (required). 096 */ 097 private final Subject sub; 098 099 100 /** 101 * The audience that this token is intended for (required). 102 */ 103 private final Audience aud; 104 105 106 /** 107 * The expiration time that limits the time window during which the JWT 108 * can be used (required). The serialised value is number of seconds 109 * from 1970-01-01T0:0:0Z as measured in UTC until the desired 110 * date/time. 111 */ 112 private final Date exp; 113 114 115 /** 116 * The time before which this token must not be accepted for 117 * processing (optional). The serialised value is number of seconds 118 * from 1970-01-01T0:0:0Z as measured in UTC until the desired 119 * date/time. 120 */ 121 private final Date nbf; 122 123 124 /** 125 * The time at which this token was issued (optional). The serialised 126 * value is number of seconds from 1970-01-01T0:0:0Z as measured in UTC 127 * until the desired date/time. 128 */ 129 private final Date iat; 130 131 132 /** 133 * Unique identifier for the JWT (optional). The JWT ID may be used by 134 * implementations requiring message de-duplication for one-time use 135 * assertions. 136 */ 137 private final JWTID jti; 138 139 140 /** 141 * Creates a new JWT client authentication claims set. 142 * 143 * @param clientID The client identifier. Used to specify the issuer 144 * and the subject. Must not be {@code null}. 145 * @param aud The audience identifier, typically the URL of the 146 * authorisation server's Token endpoint. Must not be 147 * {@code null}. 148 * @param exp The expiration time. Must not be {@code null}. 149 * @param nbf The time before which the token must not be 150 * accepted for processing, {@code null} if not 151 * specified. 152 * @param iat The time at which the token was issued, 153 * {@code null} if not specified. 154 * @param jti Unique identifier for the JWT, {@code null} if 155 * not specified. 156 */ 157 public JWTAuthenticationClaimsSet(final ClientID clientID, 158 final Audience aud, 159 final Date exp, 160 final Date nbf, 161 final Date iat, 162 final JWTID jti) { 163 164 if (clientID == null) 165 throw new IllegalArgumentException("The client ID must not be null"); 166 167 iss = new Issuer(clientID.getValue()); 168 169 sub = new Subject(clientID.getValue()); 170 171 172 if (aud == null) 173 throw new IllegalArgumentException("The audience must not be null"); 174 175 this.aud = aud; 176 177 178 if (exp == null) 179 throw new IllegalArgumentException("The expiration time must not be null"); 180 181 this.exp = exp; 182 183 184 this.nbf = nbf; 185 this.iat = iat; 186 this.jti = jti; 187 } 188 189 190 /** 191 * Gets the client identifier. Corresponds to the {@code iss} and 192 * {@code sub} claims. 193 * 194 * @return The client identifier. 195 */ 196 public ClientID getClientID() { 197 198 return new ClientID(iss.getValue()); 199 } 200 201 202 203 /** 204 * Gets the issuer. Corresponds to the {@code iss} claim. 205 * 206 * @return The issuer. Contains the identifier of the OAuth client. 207 */ 208 public Issuer getIssuer() { 209 210 return iss; 211 } 212 213 214 /** 215 * Gets the subject. Corresponds to the {@code sub} claim. 216 * 217 * @return The subject. Contains the identifier of the OAuth client. 218 */ 219 public Subject getSubject() { 220 221 return sub; 222 } 223 224 225 /** 226 * Gets the audience. Corresponds to the {@code aud} claim 227 * (single-valued). 228 * 229 * @return The audience, typically the URL of the authorisation 230 * server's token endpoint. 231 */ 232 public Audience getAudience() { 233 234 return aud; 235 } 236 237 238 /** 239 * Gets the expiration time. Corresponds to the {@code exp} claim. 240 * 241 * @return The expiration time. 242 */ 243 public Date getExpirationTime() { 244 245 return exp; 246 } 247 248 249 /** 250 * Gets the not-before time. Corresponds to the {@code nbf} claim. 251 * 252 * @return The not-before time, {@code null} if not specified. 253 */ 254 public Date getNotBeforeTime() { 255 256 return nbf; 257 } 258 259 260 /** 261 * Gets the optional issue time. Corresponds to the {@code iat} claim. 262 * 263 * @return The issued-at time, {@code null} if not specified. 264 */ 265 public Date getIssueTime() { 266 267 return iat; 268 } 269 270 271 /** 272 * Gets the identifier for the JWT. Corresponds to the {@code jti} 273 * claim. 274 * 275 * @return The identifier for the JWT, {@code null} if not specified. 276 */ 277 public JWTID getJWTID() { 278 279 return jti; 280 } 281 282 283 /** 284 * Returns a JSON object representation of this JWT client 285 * authentication claims set. 286 * 287 * @return The JSON object. 288 */ 289 public JSONObject toJSONObject() { 290 291 JSONObject o = new JSONObject(); 292 293 o.put("iss", iss.getValue()); 294 o.put("sub", sub.getValue()); 295 296 List<Object> audList = new LinkedList<Object>(); 297 audList.add(aud); 298 o.put("aud", audList); 299 300 o.put("exp", DateUtils.toSecondsSinceEpoch(exp)); 301 302 if (nbf != null) 303 o.put("nbf", DateUtils.toSecondsSinceEpoch(nbf)); 304 305 if (iat != null) 306 o.put("iat", DateUtils.toSecondsSinceEpoch(iat)); 307 308 if (jti != null) 309 o.put("jti", jti.getValue()); 310 311 return o; 312 } 313 314 315 /** 316 * Returns a JSON Web Token (JWT) claims set representation of this 317 * client authentication claims set. 318 * 319 * @return The JWT claims set. 320 */ 321 public JWTClaimsSet toJWTClaimsSet() { 322 323 JWTClaimsSet jwtClaimsSet = new JWTClaimsSet(); 324 325 jwtClaimsSet.setIssuer(iss.getValue()); 326 jwtClaimsSet.setSubject(sub.getValue()); 327 328 List<String> audList = new LinkedList<String>(); 329 audList.add(aud.getValue()); 330 331 jwtClaimsSet.setAudience(audList); 332 jwtClaimsSet.setExpirationTime(exp); 333 334 if (nbf != null) 335 jwtClaimsSet.setNotBeforeTime(nbf); 336 337 if (iat != null) 338 jwtClaimsSet.setIssueTime(iat); 339 340 if (jti != null) 341 jwtClaimsSet.setJWTID(jti.getValue()); 342 343 return jwtClaimsSet; 344 } 345 346 347 /** 348 * Parses a JWT client authentication claims set from the specified 349 * JSON object. 350 * 351 * @param jsonObject The JSON object. Must not be {@code null}. 352 * 353 * @return The client authentication claims set. 354 * 355 * @throws ParseException If the JSON object couldn't be parsed to a 356 * client authentication claims set. 357 */ 358 public static JWTAuthenticationClaimsSet parse(final JSONObject jsonObject) 359 throws ParseException { 360 361 // Parse required claims 362 Issuer iss = new Issuer(JSONObjectUtils.getString(jsonObject, "iss")); 363 Subject sub = new Subject(JSONObjectUtils.getString(jsonObject, "sub")); 364 365 Audience aud; 366 367 if (jsonObject.get("aud") instanceof String) { 368 369 aud = new Audience(JSONObjectUtils.getString(jsonObject, "aud")); 370 371 } else { 372 String[] audList = JSONObjectUtils.getStringArray(jsonObject, "aud"); 373 374 if (audList.length > 1) 375 throw new ParseException("Multiple audiences (aud) not supported"); 376 377 aud = new Audience(audList[0]); 378 } 379 380 Date exp = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "exp")); 381 382 383 // Parse optional claims 384 385 Date nbf = null; 386 387 if (jsonObject.containsKey("nbf")) 388 nbf = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "nbf")); 389 390 Date iat = null; 391 392 if (jsonObject.containsKey("iat")) 393 iat = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "iat")); 394 395 JWTID jti = null; 396 397 if (jsonObject.containsKey("jti")) 398 jti = new JWTID(JSONObjectUtils.getString(jsonObject, "jti")); 399 400 401 // Check client ID 402 403 if (! iss.getValue().equals(sub.getValue())) 404 throw new ParseException("JWT issuer and subject must have the same client ID"); 405 406 ClientID clientID = new ClientID(iss.getValue()); 407 408 return new JWTAuthenticationClaimsSet(clientID, aud, exp, nbf, iat, jti); 409 } 410 411 412 /** 413 * Parses a JWT client authentication claims set from the specified JWT 414 * claims set. 415 * 416 * @param jwtClaimsSet The JWT claims set. Must not be {@code null}. 417 * 418 * @return The client authentication claims set. 419 * 420 * @throws ParseException If the JWT claims set couldn't be parsed to a 421 * client authentication claims set. 422 */ 423 public static JWTAuthenticationClaimsSet parse(final ReadOnlyJWTClaimsSet jwtClaimsSet) 424 throws ParseException { 425 426 return parse(jwtClaimsSet.toJSONObject()); 427 } 428}