001package com.nimbusds.oauth2.sdk.assertions.jwt; 002 003 004import java.util.*; 005 006import com.nimbusds.jose.util.DateUtils; 007import com.nimbusds.jwt.JWTClaimsSet; 008import com.nimbusds.oauth2.sdk.ParseException; 009import com.nimbusds.oauth2.sdk.assertions.AssertionDetails; 010import com.nimbusds.oauth2.sdk.auth.ClientSecretJWT; 011import com.nimbusds.oauth2.sdk.auth.PrivateKeyJWT; 012import com.nimbusds.oauth2.sdk.id.*; 013import com.nimbusds.oauth2.sdk.util.JSONObjectUtils; 014import net.jcip.annotations.Immutable; 015import net.minidev.json.JSONObject; 016 017 018/** 019 * JSON Web Token (JWT) bearer assertion details (claims set) for OAuth 2.0 020 * client authentication and authorisation grants. 021 * 022 * <p>Used for {@link ClientSecretJWT client secret JWT} and 023 * {@link PrivateKeyJWT private key JWT} authentication at the Token endpoint 024 * as well as {@link com.nimbusds.oauth2.sdk.JWTBearerGrant JWT bearer 025 * assertion grants}. 026 * 027 * <p>Example JWT bearer assertion claims set for client authentication: 028 * 029 * <pre> 030 * { 031 * "iss" : "http://client.example.com", 032 * "sub" : "http://client.example.com", 033 * "aud" : [ "http://idp.example.com/token" ], 034 * "jti" : "d396036d-c4d9-40d8-8e98-f7e8327002d9", 035 * "exp" : 1311281970, 036 * "iat" : 1311280970 037 * } 038 * </pre> 039 * 040 * <p>Related specifications: 041 * 042 * <ul> 043 * <li>JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and 044 * Authorization Grants (RFC 7523), section 3. 045 * </ul> 046 */ 047@Immutable 048public class JWTAssertionDetails extends AssertionDetails { 049 050 051 /** 052 * The names of the reserved JWT claims. 053 */ 054 private static final Set<String> reservedClaimsNames = new LinkedHashSet<>(); 055 056 057 static { 058 reservedClaimsNames.add("iss"); 059 reservedClaimsNames.add("sub"); 060 reservedClaimsNames.add("aud"); 061 reservedClaimsNames.add("exp"); 062 reservedClaimsNames.add("nbf"); 063 reservedClaimsNames.add("iat"); 064 reservedClaimsNames.add("jti"); 065 } 066 067 068 /** 069 * Gets the names of the reserved JWT bearer assertion claims. 070 * 071 * @return The names of the reserved JWT bearer assertion claims 072 * (read-only set). 073 */ 074 public static Set<String> getReservedClaimsNames() { 075 076 return Collections.unmodifiableSet(reservedClaimsNames); 077 } 078 079 080 /** 081 * The time before which this token must not be accepted for 082 * processing (optional). The serialised value is number of seconds 083 * from 1970-01-01T0:0:0Z as measured in UTC until the desired 084 * date/time. 085 */ 086 private final Date nbf; 087 088 089 /** 090 * Other optional custom claims. 091 */ 092 private final Map<String,Object> other; 093 094 095 /** 096 * Creates a new JWT bearer assertion details (claims set) instance. 097 * The expiration time (exp) is set to five minutes from the current 098 * system time. Generates a default identifier (jti) for the JWT. The 099 * issued-at (iat) and not-before (nbf) claims are not set. 100 * 101 * @param iss The issuer identifier. Must not be {@code null}. 102 * @param sub The subject. Must not be {@code null}. 103 * @param aud The audience identifier, typically the URI of the 104 * authorisation server's Token endpoint. Must not be 105 * {@code null}. 106 */ 107 public JWTAssertionDetails(final Issuer iss, 108 final Subject sub, 109 final Audience aud) { 110 111 this(iss, sub, aud.toSingleAudienceList(), new Date(new Date().getTime() + 5*60*1000L), null, null, new JWTID(), null); 112 } 113 114 115 /** 116 * Creates a new JWT bearer assertion details (claims set) instance. 117 * 118 * @param iss The issuer identifier. Must not be {@code null}. 119 * @param sub The subject. Must not be {@code null}. 120 * @param aud The audience, typically including the URI of the 121 * authorisation server's token endpoint. Must not be 122 * {@code null}. 123 * @param exp The expiration time. Must not be {@code null}. 124 * @param nbf The time before which the token must not be accepted 125 * for processing, {@code null} if not specified. 126 * @param iat The time at which the token was issued, {@code null} if 127 * not specified. 128 * @param jti Unique identifier for the JWT, {@code null} if not 129 * specified. 130 * @param other Other custom claims to include, {@code null} if none. 131 */ 132 public JWTAssertionDetails(final Issuer iss, 133 final Subject sub, 134 final List<Audience> aud, 135 final Date exp, 136 final Date nbf, 137 final Date iat, 138 final JWTID jti, 139 final Map<String,Object> other) { 140 141 super(iss, sub, aud, iat, exp, jti); 142 this.nbf = nbf; 143 this.other = other; 144 } 145 146 147 /** 148 * Returns the optional not-before time. Corresponds to the {@code nbf} 149 * claim. 150 * 151 * @return The not-before time, {@code null} if not specified. 152 */ 153 public Date getNotBeforeTime() { 154 155 return nbf; 156 } 157 158 159 /** 160 * Returns the optional assertion identifier, as a JWT ID. Corresponds 161 * to the {@code jti} claim. 162 * 163 * @see #getID() 164 * 165 * @return The optional JWT ID, {@code null} if not specified. 166 */ 167 public JWTID getJWTID() { 168 169 Identifier id = getID(); 170 return id != null ? new JWTID(id.getValue()) : null; 171 } 172 173 174 /** 175 * Returns the custom claims. 176 * 177 * @return The custom claims, {@code null} if not specified. 178 */ 179 public Map<String,Object> getCustomClaims() { 180 181 return other; 182 } 183 184 185 /** 186 * Returns a JSON object representation of this JWT bearer assertion 187 * details. 188 * 189 * @return The JSON object. 190 */ 191 public JSONObject toJSONObject() { 192 193 JSONObject o = new JSONObject(); 194 195 o.put("iss", getIssuer().getValue()); 196 o.put("sub", getSubject().getValue()); 197 o.put("aud", Audience.toStringList(getAudience())); 198 o.put("exp", DateUtils.toSecondsSinceEpoch(getExpirationTime())); 199 200 if (nbf != null) 201 o.put("nbf", DateUtils.toSecondsSinceEpoch(nbf)); 202 203 if (getIssueTime() != null) 204 o.put("iat", DateUtils.toSecondsSinceEpoch(getIssueTime())); 205 206 if (getID() != null) 207 o.put("jti", getID().getValue()); 208 209 if (other != null) { 210 o.putAll(other); 211 } 212 213 return o; 214 } 215 216 217 /** 218 * Returns a JSON Web Token (JWT) claims set representation of this 219 * JWT bearer assertion details. 220 * 221 * @return The JWT claims set. 222 */ 223 public JWTClaimsSet toJWTClaimsSet() { 224 225 JWTClaimsSet.Builder builder = new JWTClaimsSet.Builder() 226 .issuer(getIssuer().getValue()) 227 .subject(getSubject().getValue()) 228 .audience(Audience.toStringList(getAudience())) 229 .expirationTime(getExpirationTime()) 230 .notBeforeTime(nbf) // optional 231 .issueTime(getIssueTime()) // optional 232 .jwtID(getID() != null ? getJWTID().getValue() : null); // optional 233 234 // Append custom claims if any 235 if (other != null) { 236 for (Map.Entry<String,?> entry: other.entrySet()) { 237 builder = builder.claim(entry.getKey(), entry.getValue()); 238 } 239 } 240 241 return builder.build(); 242 } 243 244 245 /** 246 * Parses a JWT bearer assertion details (claims set) instance from the 247 * specified JSON object. 248 * 249 * @param jsonObject The JSON object. Must not be {@code null}. 250 * 251 * @return The JWT bearer assertion details. 252 * 253 * @throws ParseException If the JSON object couldn't be parsed to a 254 * JWT bearer assertion details instance. 255 */ 256 public static JWTAssertionDetails parse(final JSONObject jsonObject) 257 throws ParseException { 258 259 // Parse required claims 260 Issuer iss = new Issuer(JSONObjectUtils.getString(jsonObject, "iss")); 261 Subject sub = new Subject(JSONObjectUtils.getString(jsonObject, "sub")); 262 263 List<Audience> aud; 264 265 if (jsonObject.get("aud") instanceof String) { 266 aud = new Audience(JSONObjectUtils.getString(jsonObject, "aud")).toSingleAudienceList(); 267 } else { 268 aud = Audience.create(JSONObjectUtils.getStringList(jsonObject, "aud")); 269 } 270 271 Date exp = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "exp")); 272 273 274 // Parse optional claims 275 276 Date nbf = null; 277 278 if (jsonObject.containsKey("nbf")) 279 nbf = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "nbf")); 280 281 Date iat = null; 282 283 if (jsonObject.containsKey("iat")) 284 iat = DateUtils.fromSecondsSinceEpoch(JSONObjectUtils.getLong(jsonObject, "iat")); 285 286 JWTID jti = null; 287 288 if (jsonObject.containsKey("jti")) 289 jti = new JWTID(JSONObjectUtils.getString(jsonObject, "jti")); 290 291 // Parse custom claims 292 Map<String,Object> other = null; 293 294 Set<String> customClaimNames = jsonObject.keySet(); 295 if (customClaimNames.removeAll(reservedClaimsNames)) { 296 other = new LinkedHashMap<>(); 297 for (String claim: customClaimNames) { 298 other.put(claim, jsonObject.get(claim)); 299 } 300 } 301 302 return new JWTAssertionDetails(iss, sub, aud, exp, nbf, iat, jti, other); 303 } 304 305 306 /** 307 * Parses a JWT bearer assertion details instance from the specified 308 * JWT claims set. 309 * 310 * @param jwtClaimsSet The JWT claims set. Must not be {@code null}. 311 * 312 * @return The JWT bearer assertion details. 313 * 314 * @throws ParseException If the JWT claims set couldn't be parsed to a 315 * JWT bearer assertion details instance. 316 */ 317 public static JWTAssertionDetails parse(final JWTClaimsSet jwtClaimsSet) 318 throws ParseException { 319 320 return parse(jwtClaimsSet.toJSONObject()); 321 } 322}