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