001/* 002 * Copyright 2025 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 JWTCreator.Builder jwtBuilder; 052 private final Algorithm algorithm; 053 private final DecodedJWT jwt; 054 055 private Jwt(Builder builder) { 056 // Hack to avoid having to duplicate the builder's properties in this object. 057 jwt = JWT.decode((jwtBuilder = builder.auth0JwtBuilder).sign(Algorithm.none())); 058 try { 059 algorithm = builder.signed ? 060 Algorithm.RSA256(new KeyConverter().privateKey(builder.privateKeyContents)) : 061 Algorithm.none(); 062 } 063 catch (InvalidKeySpecException ex) { 064 throw new IllegalStateException(ex); 065 } 066 } 067 068 /** 069 * Creates a new Base64-encoded JWT using the settings specified in the builder. 070 * 071 * @return A new Json Web Token as a string. 072 */ 073 public String generate() { 074 String jti = getId(); 075 if (jti == null || jti.trim().isEmpty()) { 076 jwtBuilder.withJWTId(UUID.randomUUID().toString()); 077 } 078 079 Instant iat = getIssuedAt(); 080 if (iat == null) { 081 jwtBuilder.withIssuedAt(Instant.now()); 082 } 083 084 return jwtBuilder.sign(algorithm); 085 } 086 087 /** 088 * Returns the {@code application_id} claim. 089 * 090 * @return The Vonage application UUID. 091 */ 092 public UUID getApplicationId() { 093 return UUID.fromString(jwt.getClaim(APPLICATION_ID_CLAIM).asString()); 094 } 095 096 /** 097 * Returns all claims, both standard and non-standard. 098 * 099 * @return The claims on this JWT as a Map. 100 */ 101 public Map<String, ?> getClaims() { 102 return jwt.getClaims().entrySet().stream().collect(Collectors.toMap( 103 Map.Entry::getKey, 104 e -> e.getValue().as(Object.class) 105 )); 106 } 107 108 /** 109 * Returns the {@code jti} claim. 110 * 111 * @return The JWT ID as a string, or {@code null} if unspecified. 112 */ 113 public String getId() { 114 return jwt.getId(); 115 } 116 117 /** 118 * Returns the {@code iat} claim. 119 * 120 * @return The issue time as an Instant, or {@code null} if unspecified. 121 */ 122 public Instant getIssuedAt() { 123 return jwt.getIssuedAtAsInstant(); 124 } 125 126 /** 127 * Returns the {@code nbf} claim. 128 * 129 * @return The start (not before) time as an Instant, or {@code null} if unspecified. 130 */ 131 public Instant getNotBefore() { 132 return jwt.getNotBeforeAsInstant(); 133 } 134 135 /** 136 * Returns the {@code exp} claim. 137 * 138 * @return The expiry time as an Instant, or {@code null} if unspecified. 139 */ 140 public Instant getExpiresAt() { 141 return jwt.getExpiresAtAsInstant(); 142 } 143 144 /** 145 * Returns the {@code sub} claim. 146 * 147 * @return The subject, or {@code null} if unspecified. 148 */ 149 public String getSubject() { 150 return jwt.getSubject(); 151 } 152 153 /** 154 * Builder for setting the properties of a JWT. 155 */ 156 public static class Builder { 157 private final JWTCreator.Builder auth0JwtBuilder = JWT.create(); 158 String privateKeyContents = ""; 159 UUID applicationId; 160 boolean signed = true; 161 162 /** 163 * (REQUIRED) 164 * Sets the application ID. This must be your Vonage application ID. 165 * 166 * @param applicationId The application UUID. 167 * @return This builder. 168 */ 169 public Builder applicationId(UUID applicationId) { 170 this.applicationId = Objects.requireNonNull(applicationId); 171 return withProperties(b -> b.withClaim(APPLICATION_ID_CLAIM, applicationId.toString())); 172 } 173 174 /** 175 * (REQUIRED) 176 * Sets the application ID. This must be your Vonage application ID. 177 * 178 * @param applicationId The application ID as a string. Note that this must be a valid UUID. 179 * @return This builder. 180 */ 181 public Builder applicationId(String applicationId) { 182 return applicationId(UUID.fromString(applicationId)); 183 } 184 185 /** 186 * (CONDITIONAL) 187 * Create an unsigned token. Calling this means you won't need to provide a private key. 188 * 189 * @return This builder. 190 */ 191 public Builder unsigned() { 192 this.signed = false; 193 return this; 194 } 195 196 /** 197 * (CONDITIONAL) 198 * Sets the private key used for signing the JWT. 199 * 200 * @param privateKeyContents The actual private key as a string. 201 * @return This builder. 202 */ 203 public Builder privateKeyContents(String privateKeyContents) { 204 this.privateKeyContents = Objects.requireNonNull(privateKeyContents); 205 this.signed = !privateKeyContents.trim().isEmpty(); 206 return this; 207 } 208 209 /** 210 * (CONDITIONAL) 211 * Sets the private key by reading it from a file. 212 * 213 * @param privateKeyPath Absolute path to the private key file. 214 * @return This builder. 215 * 216 * @throws IOException If the private key couldn't be read from the file. 217 */ 218 public Builder privateKeyPath(Path privateKeyPath) throws IOException { 219 return privateKeyContents(new String(Files.readAllBytes(privateKeyPath))); 220 } 221 222 /** 223 * (CONDITIONAL) 224 * Sets the private key by reading it from a file. This is a convenience 225 * method which simply delegates to {@linkplain #privateKeyPath(Path)}. 226 * 227 * @param privateKeyPath Absolute path to the private key file. 228 * @return This builder. 229 * 230 * @throws IOException If the private key couldn't be read from the file. 231 */ 232 public Builder privateKeyPath(String privateKeyPath) throws IOException { 233 return privateKeyPath(Paths.get(privateKeyPath)); 234 } 235 236 /** 237 * (OPTIONAL) 238 * This method enables specifying claims and other properties using the Auth0 JWT builder. 239 * 240 * @param jwtBuilder Lambda expression which sets desired properties on the builder. 241 * @return This builder. 242 */ 243 public Builder withProperties(Consumer<JWTCreator.Builder> jwtBuilder) { 244 jwtBuilder.accept(auth0JwtBuilder); 245 return this; 246 } 247 248 /** 249 * (OPTIONAL) 250 * Sets additional custom claims of the generated JWTs. 251 * 252 * @param claims The claims to add as a Map. 253 * @return This builder. 254 * 255 * @see #addClaim(String, Object) 256 * @see #withProperties(Consumer) 257 */ 258 public Builder claims(Map<String, ?> claims) { 259 withProperties(b -> b.withPayload(claims)); 260 return this; 261 } 262 263 /** 264 * (OPTIONAL) 265 * Adds a custom claim for generated JWTs. 266 * 267 * @param key Name of the claim. 268 * @param value Serializable value of the claim. 269 * 270 * @return This builder. 271 * 272 * @see #claims(Map) 273 * @see #withProperties(Consumer) 274 */ 275 public Builder addClaim(String key, Object value) { 276 return claims(Collections.singletonMap(key, value)); 277 } 278 279 /** 280 * (OPTIONAL) 281 * Sets the {@code iat} claim. 282 * If unspecified, the current time will be used every time a new JWT is generated. 283 * 284 * @param iat The issue time of generated JWTs. 285 * @return This builder. 286 */ 287 public Builder issuedAt(ZonedDateTime iat) { 288 return withProperties(b -> b.withIssuedAt(iat.toInstant())); 289 } 290 291 /** 292 * (OPTIONAL) 293 * Sets the {@code jti} claim. 294 * If unspecified, a random UUID will be used every time a new JWT is generated. 295 * 296 * @param jti The ID (nonce) of the generated JWTs. 297 * @return This builder. 298 */ 299 public Builder id(String jti) { 300 return withProperties(b -> b.withJWTId(jti)); 301 } 302 303 /** 304 * (OPTIONAL) 305 * Sets the {@code nbf} claim. 306 * 307 * @param nbf The start time at which the generated JWTs will be valid from. 308 * @return This builder. 309 */ 310 public Builder notBefore(ZonedDateTime nbf) { 311 return withProperties(b -> b.withNotBefore(nbf.toInstant())); 312 } 313 314 /** 315 * (OPTIONAL) 316 * Sets the {@code exp} claim. 317 * 318 * @param exp The expiry time of generated JWTs. 319 * @return This builder. 320 */ 321 public Builder expiresAt(ZonedDateTime exp) { 322 return withProperties(b -> b.withExpiresAt(exp.toInstant())); 323 } 324 325 /** 326 * (OPTIONAL) 327 * Sets the {@code sub} claim. 328 * 329 * @param sub The subject of generated JWTs. 330 * @return This builder. 331 */ 332 public Builder subject(String sub) { 333 return withProperties(b -> b.withSubject(sub)); 334 } 335 336 /** 337 * Builds the JWT generator using this builder's settings. 338 * 339 * @return A new JWT generator instance. 340 * @throws IllegalStateException If the required properties were not set. 341 */ 342 public Jwt build() { 343 validate(); 344 return new Jwt(this); 345 } 346 347 private void validate() { 348 if (applicationId == null && privateKeyContents.isEmpty()) { 349 throw new IllegalStateException("Both an Application ID and Private Key are required."); 350 } 351 if (applicationId == null) { 352 throw new IllegalStateException("Application ID is required."); 353 } 354 if (privateKeyContents.trim().isEmpty() && signed) { 355 throw new IllegalStateException("Private Key is required for signed token."); 356 } 357 } 358 } 359 360 /** 361 * Instantiate a new Builder for building Jwt objects. 362 * 363 * @return A new Builder. 364 */ 365 public static Builder builder() { 366 return new Builder(); 367 } 368 369 /** 370 * Determines whether the provided JSON Web Token was signed by a given SHA-256 HMAC secret. 371 * 372 * @param secret The 256-bit symmetric HMAC signature. 373 * @param token The encoded JWT to check. 374 * 375 * @return {@code true} iff the token was signed by the secret, {@code false} otherwise. 376 * 377 * @since 1.1.0 378 */ 379 public static boolean verifySignature(String token, String secret) { 380 try { 381 Objects.requireNonNull(token, "Token cannot be null."); 382 Objects.requireNonNull(secret, "Secret cannot be null."); 383 JWT.require(Algorithm.HMAC256(secret)).build().verify(token); 384 return true; 385 } 386 catch (JWTVerificationException ex) { 387 return false; 388 } 389 } 390}