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}