001/* 002 * Copyright 2024 Vonage 003 * 004 * Permission is hereby granted, free of charge, to any person obtaining a copy 005 * of this software and associated documentation files (the "Software"), to deal 006 * in the Software without restriction, including without limitation the rights 007 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 008 * copies of the Software, and to permit persons to whom the Software is 009 * furnished to do so, subject to the following conditions: 010 * 011 * The above copyright notice and this permission notice shall be included in 012 * all copies or substantial portions of the Software. 013 * 014 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 015 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 016 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 017 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 018 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 019 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 020 * THE SOFTWARE. 021 */ 022package com.vonage.jwt; 023 024import com.auth0.jwt.JWT; 025import com.auth0.jwt.JWTCreator; 026import com.auth0.jwt.algorithms.Algorithm; 027import com.auth0.jwt.exceptions.JWTVerificationException; 028import com.auth0.jwt.interfaces.DecodedJWT; 029import java.io.IOException; 030import java.nio.file.Files; 031import java.nio.file.Path; 032import java.nio.file.Paths; 033import java.security.spec.InvalidKeySpecException; 034import java.time.Instant; 035import java.time.ZonedDateTime; 036import java.util.*; 037import java.util.function.Consumer; 038import java.util.stream.Collectors; 039 040/** 041 * Class which allows declaratively specifying claims for generating Json Web Tokens (JWTs). 042 * The {@link #builder()} static method provides the entry point, from which the mandatory 043 * and optional parameters can be specified. After calling {@linkplain Builder#build()}, the 044 * options can be re-used to create new tokens using the {@link #generate()} method. 045 * <p> 046 * Signed JWTs can be verified using the static {@link #verifySignature(String, String)} method. 047 */ 048public final class Jwt { 049 static final String APPLICATION_ID_CLAIM = "application_id"; 050 051 private final String privateKeyContents; 052 private final JWTCreator.Builder jwtBuilder; 053 private final Algorithm algorithm; 054 private final DecodedJWT jwt; 055 056 private Jwt(Builder builder) { 057 privateKeyContents = builder.privateKeyContents; 058 // Hack to avoid having to duplicate the builder's properties in this object. 059 jwt = JWT.decode((jwtBuilder = builder.auth0JwtBuilder).sign(Algorithm.none())); 060 try { 061 algorithm = privateKeyContents == null || privateKeyContents.trim().isEmpty() ? 062 Algorithm.none() : Algorithm.RSA256(new KeyConverter().privateKey(privateKeyContents)); 063 } 064 catch (InvalidKeySpecException ex) { 065 throw new IllegalStateException(ex); 066 } 067 } 068 069 /** 070 * Creates a new Base64-encoded JWT using the settings specified in the builder. 071 * 072 * @return A new Json Web Token as a string. 073 */ 074 public String generate() { 075 String jti = getId(); 076 if (jti == null || jti.isEmpty()) { 077 jwtBuilder.withJWTId(UUID.randomUUID().toString()); 078 } 079 080 Instant iat = getIssuedAt(); 081 if (iat == null) { 082 jwtBuilder.withIssuedAt(Instant.now()); 083 } 084 085 return jwtBuilder.sign(algorithm); 086 } 087 088 /** 089 * Returns the {@code application_id} claim. 090 * 091 * @return The Vonage application UUID. 092 */ 093 public UUID getApplicationId() { 094 return UUID.fromString(jwt.getClaim(APPLICATION_ID_CLAIM).asString()); 095 } 096 097 /** 098 * Returns the contents of the private key. 099 * 100 * @return The private key as a string, or {@code null} if the token is unsigned. 101 */ 102 String getPrivateKeyContents() { 103 return privateKeyContents; 104 } 105 106 /** 107 * Returns all claims, both standard and non-standard. 108 * 109 * @return The claims on this JWT as a Map. 110 */ 111 public Map<String, ?> getClaims() { 112 return jwt.getClaims().entrySet().stream().collect(Collectors.toMap( 113 Map.Entry::getKey, 114 e -> e.getValue().as(Object.class) 115 )); 116 } 117 118 /** 119 * Returns the {@code jti} claim. 120 * 121 * @return The JWT ID as a string, or {@code null} if unspecified. 122 */ 123 public String getId() { 124 return jwt.getId(); 125 } 126 127 /** 128 * Returns the {@code iat} claim. 129 * 130 * @return The issue time as an Instant, or {@code null} if unspecified. 131 */ 132 public Instant getIssuedAt() { 133 return jwt.getIssuedAtAsInstant(); 134 } 135 136 /** 137 * Returns the {@code nbf} claim. 138 * 139 * @return The start (not before) time as an Instant, or {@code null} if unspecified. 140 */ 141 public Instant getNotBefore() { 142 return jwt.getNotBeforeAsInstant(); 143 } 144 145 /** 146 * Returns the {@code exp} claim. 147 * 148 * @return The expiry time as an Instant, or {@code null} if unspecified. 149 */ 150 public Instant getExpiresAt() { 151 return jwt.getExpiresAtAsInstant(); 152 } 153 154 /** 155 * Returns the {@code sub} claim. 156 * 157 * @return The subject, or {@code null} if unspecified. 158 */ 159 public String getSubject() { 160 return jwt.getSubject(); 161 } 162 163 /** 164 * Builder for setting the properties of a JWT. 165 */ 166 public static class Builder { 167 private final JWTCreator.Builder auth0JwtBuilder = JWT.create(); 168 private UUID applicationId; 169 private String privateKeyContents = ""; 170 private boolean signed = true; 171 172 /** 173 * (REQUIRED) 174 * Sets the application ID. This must be your Vonage application ID. 175 * 176 * @param applicationId The application UUID. 177 * @return This builder. 178 */ 179 public Builder applicationId(UUID applicationId) { 180 this.applicationId = Objects.requireNonNull(applicationId); 181 return withProperties(b -> b.withClaim(APPLICATION_ID_CLAIM, applicationId.toString())); 182 } 183 184 /** 185 * (REQUIRED) 186 * Sets the application ID. This must be your Vonage application ID. 187 * 188 * @param applicationId The application ID as a string. Note that this must be a valid UUID. 189 * @return This builder. 190 */ 191 public Builder applicationId(String applicationId) { 192 return applicationId(UUID.fromString(applicationId)); 193 } 194 195 /** 196 * (CONDITIONAL) 197 * Create an unsigned token. Calling this means you won't need to provide a private key. 198 * 199 * @return This builder. 200 */ 201 public Builder unsigned() { 202 this.signed = false; 203 return this; 204 } 205 206 /** 207 * (CONDITIONAL) 208 * Sets the private key used for signing the JWT. 209 * 210 * @param privateKeyContents The actual private key as a string. 211 * @return This builder. 212 */ 213 public Builder privateKeyContents(String privateKeyContents) { 214 this.privateKeyContents = Objects.requireNonNull(privateKeyContents); 215 this.signed = !privateKeyContents.isEmpty(); 216 return this; 217 } 218 219 /** 220 * (CONDITIONAL) 221 * Sets the private key by reading it from a file. 222 * 223 * @param privateKeyPath Absolute path to the private key file. 224 * @return This builder. 225 * 226 * @throws IOException If the private key couldn't be read from the file. 227 */ 228 public Builder privateKeyPath(Path privateKeyPath) throws IOException { 229 return privateKeyContents(new String(Files.readAllBytes(privateKeyPath))); 230 } 231 232 /** 233 * (CONDITIONAL) 234 * Sets the private key by reading it from a file. This is a convenience 235 * method which simply delegates to {@linkplain #privateKeyPath(Path)}. 236 * 237 * @param privateKeyPath Absolute path to the private key file. 238 * @return This builder. 239 * 240 * @throws IOException If the private key couldn't be read from the file. 241 */ 242 public Builder privateKeyPath(String privateKeyPath) throws IOException { 243 return privateKeyPath(Paths.get(privateKeyPath)); 244 } 245 246 /** 247 * (OPTIONAL) 248 * This method enables specifying claims and other properties using the Auth0 JWT builder. 249 * 250 * @param jwtBuilder Lambda expression which sets desired properties on the builder. 251 * @return This builder. 252 */ 253 public Builder withProperties(Consumer<JWTCreator.Builder> jwtBuilder) { 254 jwtBuilder.accept(auth0JwtBuilder); 255 return this; 256 } 257 258 /** 259 * (OPTIONAL) 260 * Sets additional custom claims of the generated JWTs. 261 * 262 * @param claims The claims to add as a Map. 263 * @return This builder. 264 * 265 * @see #addClaim(String, Object) 266 * @see #withProperties(Consumer) 267 */ 268 public Builder claims(Map<String, ?> claims) { 269 withProperties(b -> b.withPayload(claims)); 270 return this; 271 } 272 273 /** 274 * (OPTIONAL) 275 * Adds a custom claim for generated JWTs. 276 * 277 * @param key Name of the claim. 278 * @param value Serializable value of the claim. 279 * 280 * @return This builder. 281 * 282 * @see #claims(Map) 283 * @see #withProperties(Consumer) 284 */ 285 public Builder addClaim(String key, Object value) { 286 return claims(Collections.singletonMap(key, value)); 287 } 288 289 /** 290 * (OPTIONAL) 291 * Sets the {@code iat} claim. 292 * If unspecified, the current time will be used every time a new JWT is generated. 293 * 294 * @param iat The issue time of generated JWTs. 295 * @return This builder. 296 */ 297 public Builder issuedAt(ZonedDateTime iat) { 298 return withProperties(b -> b.withIssuedAt(iat.toInstant())); 299 } 300 301 /** 302 * (OPTIONAL) 303 * Sets the {@code jti} claim. 304 * If unspecified, a random UUID will be used every time a new JWT is generated. 305 * 306 * @param jti The ID (nonce) of the generated JWTs. 307 * @return This builder. 308 */ 309 public Builder id(String jti) { 310 return withProperties(b -> b.withJWTId(jti)); 311 } 312 313 /** 314 * (OPTIONAL) 315 * Sets the {@code nbf} claim. 316 * 317 * @param nbf The start time at which the generated JWTs will be valid from. 318 * @return This builder. 319 */ 320 public Builder notBefore(ZonedDateTime nbf) { 321 return withProperties(b -> b.withNotBefore(nbf.toInstant())); 322 } 323 324 /** 325 * (OPTIONAL) 326 * Sets the {@code exp} claim. 327 * 328 * @param exp The expiry time of generated JWTs. 329 * @return This builder. 330 */ 331 public Builder expiresAt(ZonedDateTime exp) { 332 return withProperties(b -> b.withExpiresAt(exp.toInstant())); 333 } 334 335 /** 336 * (OPTIONAL) 337 * Sets the {@code sub} claim. 338 * 339 * @param sub The subject of generated JWTs. 340 * @return This builder. 341 */ 342 public Builder subject(String sub) { 343 return withProperties(b -> b.withSubject(sub)); 344 } 345 346 /** 347 * Builds the JWT generator using this builder's settings. 348 * 349 * @return A new JWT generator instance. 350 * @throws IllegalStateException If the required properties were not set. 351 */ 352 public Jwt build() { 353 validate(); 354 return new Jwt(this); 355 } 356 357 private void validate() { 358 if (applicationId == null && privateKeyContents.isEmpty()) { 359 throw new IllegalStateException("Both an Application ID and Private Key are required."); 360 } 361 if (applicationId == null) { 362 throw new IllegalStateException("Application ID is required."); 363 } 364 if (privateKeyContents.trim().isEmpty() && signed) { 365 throw new IllegalStateException("Private Key is required for signed token."); 366 } 367 } 368 } 369 370 /** 371 * Instantiate a new Builder for building Jwt objects. 372 * 373 * @return A new Builder. 374 */ 375 public static Builder builder() { 376 return new Builder(); 377 } 378 379 /** 380 * Determines whether the provided JSON Web Token was signed by a given SHA-256 HMAC secret. 381 * 382 * @param secret The 256-bit symmetric HMAC signature. 383 * @param token The encoded JWT to check. 384 * 385 * @return {@code true} iff the token was signed by the secret, {@code false} otherwise. 386 * 387 * @since 1.1.0 388 */ 389 public static boolean verifySignature(String token, String secret) { 390 try { 391 Objects.requireNonNull(token, "Token cannot be null."); 392 Objects.requireNonNull(secret, "Secret cannot be null."); 393 JWT.require(Algorithm.HMAC256(secret)).build().verify(token); 394 return true; 395 } 396 catch (JWTVerificationException ex) { 397 return false; 398 } 399 } 400}