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}