From bb8737396fcf596ad5350e65175664f723183eec Mon Sep 17 00:00:00 2001 From: scottf Date: Mon, 31 Jul 2023 11:20:21 -0400 Subject: [PATCH 1/4] Updated JwtUtils to support audience re-ordered json claim fields to match go, fixed tests --- src/main/java/io/nats/client/NKey.java | 4 +- .../java/io/nats/client/support/Encoding.java | 20 ++- .../java/io/nats/client/support/JwtUtils.java | 94 ++++++++++--- .../io/nats/client/support/JwtUtilsTests.java | 130 ++++++++++-------- 4 files changed, 168 insertions(+), 80 deletions(-) diff --git a/src/main/java/io/nats/client/NKey.java b/src/main/java/io/nats/client/NKey.java index b2c93c254..72b3b157b 100644 --- a/src/main/java/io/nats/client/NKey.java +++ b/src/main/java/io/nats/client/NKey.java @@ -1,4 +1,4 @@ -// Copyright 2018 The NATS Authors +// Copyright 2023 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at: @@ -469,7 +469,7 @@ public static NKey fromPublicKey(char[] publicKey) { } Type type = NKey.Type.fromPrefix(prefix); - return new NKey(type, publicKey, null); + return new NKey(type, publicKey, new String(raw).toCharArray()); } /** diff --git a/src/main/java/io/nats/client/support/Encoding.java b/src/main/java/io/nats/client/support/Encoding.java index 193dc04a3..0e3687532 100644 --- a/src/main/java/io/nats/client/support/Encoding.java +++ b/src/main/java/io/nats/client/support/Encoding.java @@ -22,16 +22,32 @@ public abstract class Encoding { private Encoding() {} /* ensures cannot be constructed */ + /** + * @deprecated prefere base64UrlEncode + */ + @Deprecated public static byte[] base64Encode(byte[] input) { return Base64.getUrlEncoder().withoutPadding().encode(input); } + public static byte[] base64UrlEncode(byte[] input) { + return Base64.getUrlEncoder().withoutPadding().encode(input); + } + public static String toBase64Url(byte[] input) { - return new String(base64Encode(input)); + return new String(base64UrlEncode(input)); } public static String toBase64Url(String input) { - return new String(base64Encode(input.getBytes(StandardCharsets.UTF_8))); + return new String(base64UrlEncode(input.getBytes(StandardCharsets.US_ASCII))); + } + + public static byte[] base64UrlDecode(byte[] input) { + return Base64.getUrlDecoder().decode(input); + } + + public static String fromBase64Url(String input) { + return new String(base64UrlDecode(input.getBytes(StandardCharsets.US_ASCII))); } // http://en.wikipedia.org/wiki/Base_32 diff --git a/src/main/java/io/nats/client/support/JwtUtils.java b/src/main/java/io/nats/client/support/JwtUtils.java index 34065544f..92a3bacbf 100644 --- a/src/main/java/io/nats/client/support/JwtUtils.java +++ b/src/main/java/io/nats/client/support/JwtUtils.java @@ -22,8 +22,7 @@ import java.time.Duration; import java.util.List; -import static io.nats.client.support.Encoding.base32Encode; -import static io.nats.client.support.Encoding.toBase64Url; +import static io.nats.client.support.Encoding.*; import static io.nats.client.support.JsonUtils.beginJson; import static io.nats.client.support.JsonUtils.endJson; @@ -64,9 +63,16 @@ private JwtUtils() {} /* ensures cannot be constructed */ "\n" + "*************************************************************\n"; + /** + * Get the current time in seconds since epoch. Used for issue time. + * @return the time + */ + public static long currentTimeSeconds() { + return System.currentTimeMillis() / 1000; + } + /** * Issue a user JWT from a scoped signing key. See Signing Keys - * * @param signingKey a mandatory account nkey pair to sign the generated jwt. * @param accountId a mandatory public account nkey. Will throw error when not set or not account nkey. * @param publicUserKey a mandatory public user nkey. Will throw error when not set or not user nkey. @@ -77,12 +83,11 @@ private JwtUtils() {} /* ensures cannot be constructed */ * @return a JWT */ public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey) throws GeneralSecurityException, IOException { - return issueUserJWT(signingKey, accountId, publicUserKey, null, null); + return issueUserJWT(signingKey, publicUserKey, null, null, currentTimeSeconds(), null, new UserClaim(accountId)); } /** * Issue a user JWT from a scoped signing key. See Signing Keys - * * @param signingKey a mandatory account nkey pair to sign the generated jwt. * @param accountId a mandatory public account nkey. Will throw error when not set or not account nkey. * @param publicUserKey a mandatory public user nkey. Will throw error when not set or not user nkey. @@ -94,12 +99,11 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @return a JWT */ public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name) throws GeneralSecurityException, IOException { - return issueUserJWT(signingKey, accountId, publicUserKey, name, null); + return issueUserJWT(signingKey, publicUserKey, name, null, currentTimeSeconds(), null, new UserClaim(accountId)); } /** * Issue a user JWT from a scoped signing key. See Signing Keys - * * @param signingKey a mandatory account nkey pair to sign the generated jwt. * @param accountId a mandatory public account nkey. Will throw error when not set or not account nkey. * @param publicUserKey a mandatory public user nkey. Will throw error when not set or not user nkey. @@ -113,12 +117,11 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @return a JWT */ public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name, Duration expiration, String... tags) throws GeneralSecurityException, IOException { - return issueUserJWT(signingKey, accountId, publicUserKey, name, expiration, tags, System.currentTimeMillis() / 1000); + return issueUserJWT(signingKey, publicUserKey, name, expiration, currentTimeSeconds(), null, new UserClaim(accountId).tags(tags)); } /** * Issue a user JWT from a scoped signing key. See Signing Keys - * * @param signingKey a mandatory account nkey pair to sign the generated jwt. * @param accountId a mandatory public account nkey. Will throw error when not set or not account nkey. * @param publicUserKey a mandatory public user nkey. Will throw error when not set or not user nkey. @@ -133,12 +136,15 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @return a JWT */ public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name, Duration expiration, String[] tags, long issuedAt) throws GeneralSecurityException, IOException { - return issueUserJWT(signingKey, publicUserKey, name, expiration, issuedAt, new UserClaim(accountId).tags(tags)); + return issueUserJWT(signingKey, publicUserKey, name, expiration, issuedAt, null, new UserClaim(accountId).tags(tags)); + } + + public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name, Duration expiration, String[] tags, long issuedAt, String audience) throws GeneralSecurityException, IOException { + return issueUserJWT(signingKey, publicUserKey, name, expiration, issuedAt, audience, new UserClaim(accountId).tags(tags)); } /** * Issue a user JWT from a scoped signing key. See Signing Keys - * * @param signingKey a mandatory account nkey pair to sign the generated jwt. * @param publicUserKey a mandatory public user nkey. Will throw error when not set or not user nkey. * @param name optional human-readable name. When absent, default to publicUserKey. @@ -152,6 +158,25 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ * @return a JWT */ public static String issueUserJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, UserClaim nats) throws GeneralSecurityException, IOException { + return issueUserJWT(signingKey, publicUserKey, name, expiration, issuedAt, null, nats); + } + + /** + * Issue a user JWT from a scoped signing key. See Signing Keys + * @param signingKey a mandatory account nkey pair to sign the generated jwt. + * @param publicUserKey a mandatory public user nkey. Will throw error when not set or not user nkey. + * @param name optional human-readable name. When absent, default to publicUserKey. + * @param expiration optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. + * @param issuedAt the current epoch seconds. + * @param audience the optional audience + * @param nats the user claim + * @throws IllegalArgumentException if the accountId or publicUserKey is not a valid public key of the proper type + * @throws NullPointerException if signingKey, accountId, or publicUserKey are null. + * @throws GeneralSecurityException if SHA-256 MessageDigest is missing, or if the signingKey can not be used for signing. + * @throws IOException if signingKey sign method throws this exception. + * @return a JWT + */ + public static String issueUserJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String audience, UserClaim nats) throws GeneralSecurityException, IOException { // Validate the signingKey: if (signingKey.getType() != NKey.Type.ACCOUNT) { throw new IllegalArgumentException("issueUserJWT requires an account key for the signingKey parameter, but got " + signingKey.getType()); @@ -170,12 +195,11 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String String claimName = Validator.nullOrEmpty(name) ? publicUserKey : name; - return issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, accSigningKeyPub, nats); + return issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, audience, accSigningKeyPub, nats); } /** * Issue a JWT - * * @param signingKey account nkey pair to sign the generated jwt. * @param publicUserKey a mandatory public user nkey. * @param name optional human-readable name. @@ -188,12 +212,31 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String * @return a JWT */ public static String issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String accSigningKeyPub, JsonSerializable nats) throws GeneralSecurityException, IOException { + return issueJWT(signingKey, publicUserKey, name, expiration, issuedAt, null, accSigningKeyPub, nats); + } + + /** + * Issue a JWT + * @param signingKey account nkey pair to sign the generated jwt. + * @param publicUserKey a mandatory public user nkey. + * @param name optional human-readable name. + * @param expiration optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. + * @param issuedAt the current epoch seconds. + * @param audience the optional audience + * @param accSigningKeyPub the account signing key + * @param nats the generic nats claim + * @return a JWT + * @throws GeneralSecurityException if SHA-256 MessageDigest is missing, or if the signingKey can not be used for signing. + * @throws IOException if signingKey sign method throws this exception. + */ + public static String issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String audience, String accSigningKeyPub, JsonSerializable nats) throws GeneralSecurityException, IOException { Claim claim = new Claim(); claim.exp = expiration; claim.iat = issuedAt; claim.iss = accSigningKeyPub; claim.name = name; claim.sub = publicUserKey; + claim.aud = audience; claim.nats = nats; // Issue At time is stored in unix seconds @@ -201,7 +244,7 @@ public static String issueJWT(NKey signingKey, String publicUserKey, String name // Compute jti, a base32 encoded sha256 hash MessageDigest sha256 = MessageDigest.getInstance("SHA-256"); - byte[] encoded = sha256.digest(claimJson.getBytes(StandardCharsets.UTF_8)); + byte[] encoded = sha256.digest(claimJson.getBytes(StandardCharsets.US_ASCII)); claim.jti = new String(base32Encode(encoded)); claimJson = claim.toJson(); @@ -217,6 +260,11 @@ public static String issueJWT(NKey signingKey, String publicUserKey, String name return ENCODED_CLAIM_HEADER + "." + encBody + "." + encSig; } + public static String getClaimBody(String jwt) { + String[] split = jwt.split("\\."); + return fromBase64Url(split[1]); + } + public static class UserClaim implements JsonSerializable { public String issuerAccount; // User public String[] tags; // User/GenericFields @@ -390,27 +438,29 @@ public String toJson() { } static class Claim implements JsonSerializable { - Duration exp; + String aud; + String jti; long iat; String iss; - String jti; String name; - JsonSerializable nats; String sub; + Duration exp; + JsonSerializable nats; @Override public String toJson() { StringBuilder sb = beginJson(); + JsonUtils.addField(sb, "aud", aud); + JsonUtils.addFieldEvenEmpty(sb, "jti", jti); + JsonUtils.addField(sb, "iat", iat); + JsonUtils.addField(sb, "iss", iss); + JsonUtils.addField(sb, "name", name); + JsonUtils.addField(sb, "sub", sub); if (exp != null && !exp.isZero() && !exp.isNegative()) { long seconds = exp.toMillis() / 1000; JsonUtils.addField(sb, "exp", iat + seconds); } - JsonUtils.addField(sb, "iat", iat); - JsonUtils.addFieldEvenEmpty(sb, "jti", jti); - JsonUtils.addField(sb, "iss", iss); - JsonUtils.addField(sb, "name", name); JsonUtils.addField(sb, "nats", nats); - JsonUtils.addField(sb, "sub", sub); return endJson(sb).toString(); } } diff --git a/src/test/java/io/nats/client/support/JwtUtilsTests.java b/src/test/java/io/nats/client/support/JwtUtilsTests.java index 5e280cc82..1a80e78c5 100644 --- a/src/test/java/io/nats/client/support/JwtUtilsTests.java +++ b/src/test/java/io/nats/client/support/JwtUtilsTests.java @@ -1,3 +1,16 @@ +// Copyright 2023 The NATS Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at: +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package io.nats.client.support; import io.nats.client.NKey; @@ -18,15 +31,19 @@ public void issueUserJWTSuccessMinimal() throws Exception { NKey userKey = NKey.fromSeed("SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY".toCharArray()); NKey signingKey = NKey.fromSeed("SAANJIBNEKGCRUWJCPIWUXFBFJLR36FJTFKGBGKAT7AQXH2LVFNQWZJMQU".toCharArray()); String accountId = "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6"; - String jwt = issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378); + String jwt = issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378, "audience"); + String claimBody = getClaimBody(jwt); String cred = String.format(NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed())); + /* - Generated JWT: + Formatted Claim Body: { + "aud": "audience", + "jti": "PASRIRDVH2NWAPOKCO7TFIJVWI2OESTOH4CJ2PSGYH77YPQRXPVA", "iat": 1633043378, - "jti": "X4TWD4I3YGCAZ4PRKULOOUACR5W5AIKSM6MXVPJZBLXHZZMNFZAQ", "iss": "ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY", "name": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ", + "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ", "nats": { "issuer_account": "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6", "type": "user", @@ -34,12 +51,12 @@ public void issueUserJWTSuccessMinimal() throws Exception { "subs": -1, "data": -1, "payload": -1 - }, - "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ" + } } */ - assertEquals("-----BEGIN NATS USER JWT-----\n" + - "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJpYXQiOjE2MzMwNDMzNzgsImp0aSI6IlROUUhFRzVMQllNVzVJM0lMVFBYSDRON0dHSEVGM0lLTURNSDRRSjROUUFVSUQ1MkZFM1EiLCJpc3MiOiJBRFE0QllNNUtJQ1I1T1hEU1AzUzNXVko1Q1lFT1JHUUtUNzJTVlJGMlpEVkE3TFRGS01DSVBHWSIsIm5hbWUiOiJVQTZLT01RNjdYT0UzRkhFMzdXNE9YQURWWFZZSVNCTkxUQlVUMkxTWTVWRktBSUo3Q1JEUjJSWiIsIm5hdHMiOnsiaXNzdWVyX2FjY291bnQiOiJBQ1haUkFMSUwyMldSRVREUlhZS09ZREI3WEMzRTdNQlNWVVNVTUZBQ082T001VlBSTkZNT09PNiIsInR5cGUiOiJ1c2VyIiwidmVyc2lvbiI6Miwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMX0sInN1YiI6IlVBNktPTVE2N1hPRTNGSEUzN1c0T1hBRFZYVllJU0JOTFRCVVQyTFNZNVZGS0FJSjdDUkRSMlJaIn0.9uzP8We3PCdR6G_50kvaKxLIOIwTlnc2eZe5249XFQj1JGgsHC0VJ3MSMerxIdNpWbSPQrgy1oKL2yU-xjr8Bw\n" + + String expectedClaimBody = "{\"aud\":\"audience\",\"jti\":\"PASRIRDVH2NWAPOKCO7TFIJVWI2OESTOH4CJ2PSGYH77YPQRXPVA\",\"iat\":1633043378,\"iss\":\"ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY\",\"name\":\"UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ\",\"sub\":\"UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ\",\"nats\":{\"issuer_account\":\"ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6\",\"type\":\"user\",\"version\":2,\"subs\":-1,\"data\":-1,\"payload\":-1}}"; + String expectedCred = "-----BEGIN NATS USER JWT-----\n" + + "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJhdWQiOiJhdWRpZW5jZSIsImp0aSI6IlBBU1JJUkRWSDJOV0FQT0tDTzdURklKVldJMk9FU1RPSDRDSjJQU0dZSDc3WVBRUlhQVkEiLCJpYXQiOjE2MzMwNDMzNzgsImlzcyI6IkFEUTRCWU01S0lDUjVPWERTUDNTM1dWSjVDWUVPUkdRS1Q3MlNWUkYyWkRWQTdMVEZLTUNJUEdZIiwibmFtZSI6IlVBNktPTVE2N1hPRTNGSEUzN1c0T1hBRFZYVllJU0JOTFRCVVQyTFNZNVZGS0FJSjdDUkRSMlJaIiwic3ViIjoiVUE2S09NUTY3WE9FM0ZIRTM3VzRPWEFEVlhWWUlTQk5MVEJVVDJMU1k1VkZLQUlKN0NSRFIyUloiLCJuYXRzIjp7Imlzc3Vlcl9hY2NvdW50IjoiQUNYWlJBTElMMjJXUkVURFJYWUtPWURCN1hDM0U3TUJTVlVTVU1GQUNPNk9NNVZQUk5GTU9PTzYiLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjIsInN1YnMiOi0xLCJkYXRhIjotMSwicGF5bG9hZCI6LTF9fQ.nt_bErX7UuqDSxC8NUORaB0r4IS_33Wds1vV_o0HRI-BwE9UxM-zAFtq43o3-d98s6u1jASgVXp0h81om8mVDw\n" + "------END NATS USER JWT------\n" + "\n" + "************************* IMPORTANT *************************\n" + @@ -50,8 +67,9 @@ public void issueUserJWTSuccessMinimal() throws Exception { "SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY\n" + "------END USER NKEY SEED------\n" + "\n" + - "*************************************************************\n", - cred); + "*************************************************************\n"; + assertEquals(expectedClaimBody, claimBody); + assertEquals(expectedCred, cred); } @Test @@ -59,33 +77,33 @@ public void issueUserJWTSuccessAllArgs() throws Exception { NKey userKey = NKey.fromSeed("SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY".toCharArray()); NKey signingKey = NKey.fromSeed("SAANJIBNEKGCRUWJCPIWUXFBFJLR36FJTFKGBGKAT7AQXH2LVFNQWZJMQU".toCharArray()); String accountId = "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6"; - String jwt = issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), "name", Duration.ofSeconds(100), new String[]{"tag1", "tag\\two"}, 1633043378); + String jwt = issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), "name", Duration.ofSeconds(100), new String[]{"tag1", "tag\\two"}, 1633043378, "audience"); + String claimBody = getClaimBody(jwt); String cred = String.format(NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed())); /* - Generated JWT: + Formatted Claim Body: { - "exp": 1633043478, + "aud": "audience", + "jti": "YK4IE2OAVVGD7CZ2OHGVDSQCRIHL4HNRPAQ2DE5B6VQ6HM5ZFWYA", "iat": 1633043378, - "jti": "43FXG2FKK7OY2QH7OJGYOD7BF2UFU447EDJZEPO3VSBNJJVAKJCA", "iss": "ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY", "name": "name", + "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ", + "exp": 1633043478, "nats": { "issuer_account": "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6", - "tags": [ - "tag1", - "tag\\two" - ], + "tags": ["tag1", "tag\\two"], "type": "user", "version": 2, "subs": -1, "data": -1, "payload": -1 - }, - "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ" + } } */ - assertEquals("-----BEGIN NATS USER JWT-----\n" + - "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJleHAiOjE2MzMwNDM0NzgsImlhdCI6MTYzMzA0MzM3OCwianRpIjoiWk1ENEJCRkxTVklZVzNBRVNHVk5UNDdEQ1dSN0lPV1paUVk3NFBLQTNDWEhQV0NYMkFOUSIsImlzcyI6IkFEUTRCWU01S0lDUjVPWERTUDNTM1dWSjVDWUVPUkdRS1Q3MlNWUkYyWkRWQTdMVEZLTUNJUEdZIiwibmFtZSI6Im5hbWUiLCJuYXRzIjp7Imlzc3Vlcl9hY2NvdW50IjoiQUNYWlJBTElMMjJXUkVURFJYWUtPWURCN1hDM0U3TUJTVlVTVU1GQUNPNk9NNVZQUk5GTU9PTzYiLCJ0YWdzIjpbInRhZzEiLCJ0YWdcXHR3byJdLCJ0eXBlIjoidXNlciIsInZlcnNpb24iOjIsInN1YnMiOi0xLCJkYXRhIjotMSwicGF5bG9hZCI6LTF9LCJzdWIiOiJVQTZLT01RNjdYT0UzRkhFMzdXNE9YQURWWFZZSVNCTkxUQlVUMkxTWTVWRktBSUo3Q1JEUjJSWiJ9.5cUsiR0BTT1g6mbLeNRROvEfHFIG3m22Zt9q_xjozjEj_JREZsZHm84noE0Ec8xWN92bIrETmaX-qywOGBdcCg\n" + + String expectedClaimBody = "{\"aud\":\"audience\",\"jti\":\"YK4IE2OAVVGD7CZ2OHGVDSQCRIHL4HNRPAQ2DE5B6VQ6HM5ZFWYA\",\"iat\":1633043378,\"iss\":\"ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY\",\"name\":\"name\",\"sub\":\"UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ\",\"exp\":1633043478,\"nats\":{\"issuer_account\":\"ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6\",\"tags\":[\"tag1\",\"tag\\\\two\"],\"type\":\"user\",\"version\":2,\"subs\":-1,\"data\":-1,\"payload\":-1}}"; + String expectedCred = "-----BEGIN NATS USER JWT-----\n" + + "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJhdWQiOiJhdWRpZW5jZSIsImp0aSI6IllLNElFMk9BVlZHRDdDWjJPSEdWRFNRQ1JJSEw0SE5SUEFRMkRFNUI2VlE2SE01WkZXWUEiLCJpYXQiOjE2MzMwNDMzNzgsImlzcyI6IkFEUTRCWU01S0lDUjVPWERTUDNTM1dWSjVDWUVPUkdRS1Q3MlNWUkYyWkRWQTdMVEZLTUNJUEdZIiwibmFtZSI6Im5hbWUiLCJzdWIiOiJVQTZLT01RNjdYT0UzRkhFMzdXNE9YQURWWFZZSVNCTkxUQlVUMkxTWTVWRktBSUo3Q1JEUjJSWiIsImV4cCI6MTYzMzA0MzQ3OCwibmF0cyI6eyJpc3N1ZXJfYWNjb3VudCI6IkFDWFpSQUxJTDIyV1JFVERSWFlLT1lEQjdYQzNFN01CU1ZVU1VNRkFDTzZPTTVWUFJORk1PT082IiwidGFncyI6WyJ0YWcxIiwidGFnXFx0d28iXSwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyLCJzdWJzIjotMSwiZGF0YSI6LTEsInBheWxvYWQiOi0xfX0.cX9OENTYC54mV5g3q1rL9QJiazhBnI88poJ-ssYwDE4ywpi4WhAinXXYd7wXaaTqks97_9cY25XP01sFTqUeDQ\n" + "------END NATS USER JWT------\n" + "\n" + "************************* IMPORTANT *************************\n" + @@ -96,8 +114,9 @@ public void issueUserJWTSuccessAllArgs() throws Exception { "SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY\n" + "------END USER NKEY SEED------\n" + "\n" + - "*************************************************************\n", - cred); + "*************************************************************\n"; + assertEquals(expectedClaimBody, claimBody); + assertEquals(expectedCred, cred); } @Test @@ -114,16 +133,17 @@ public void issueUserJWTSuccessCustom() throws Exception { .deny(new String[] {"sub-deny-subject"})) .tags(new String[]{"tag1", "tag\\two"}); - String jwt = issueUserJWT(signingKey, new String(userKey.getPublicKey()), null, null, 1633043378, userClaim); + String jwt = issueUserJWT(signingKey, new String(userKey.getPublicKey()), "custom", null, 1633043378, userClaim); + String claimBody = getClaimBody(jwt); String cred = String.format(NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed())); - /* - Generated JWT: + Formatted Claim Body: { + "jti": "XAI5HUYQESXRZIYCDCWRSCXRDLVUG2G75QD4NJAOSTD72B6OIX6Q", "iat": 1633043378, - "jti": "JUB65ANDEIROWEGEBOVRGEJRW6FX74PIJX3LJDYHBKKQITDGATTA", "iss": "ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY", - "name": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ", + "name": "custom", + "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ", "nats": { "issuer_account": "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6", "tags": ["tag1", "tag\\two"], @@ -140,13 +160,13 @@ public void issueUserJWTSuccessCustom() throws Exception { "subs": -1, "data": -1, "payload": -1 - }, - "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ" + } } */ - assertEquals("-----BEGIN NATS USER JWT-----\n" + - "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJpYXQiOjE2MzMwNDMzNzgsImp0aSI6IldGMkJDR0VMRVVOTzJDWEYzUjZaN0w0RTNKS09PM0tCREVDRU5IUkVSSTczRzYzRUtZVlEiLCJpc3MiOiJBRFE0QllNNUtJQ1I1T1hEU1AzUzNXVko1Q1lFT1JHUUtUNzJTVlJGMlpEVkE3TFRGS01DSVBHWSIsIm5hbWUiOiJVQTZLT01RNjdYT0UzRkhFMzdXNE9YQURWWFZZSVNCTkxUQlVUMkxTWTVWRktBSUo3Q1JEUjJSWiIsIm5hdHMiOnsiaXNzdWVyX2FjY291bnQiOiJBQ1haUkFMSUwyMldSRVREUlhZS09ZREI3WEMzRTdNQlNWVVNVTUZBQ082T001VlBSTkZNT09PNiIsInRhZ3MiOlsidGFnMSIsInRhZ1xcdHdvIl0sInR5cGUiOiJ1c2VyIiwidmVyc2lvbiI6MiwicHViIjp7ImFsbG93IjpbInB1Yi1hbGxvdy1zdWJqZWN0Il0sImRlbnkiOlsicHViLWRlbnktc3ViamVjdCJdfSwic3ViIjp7ImFsbG93IjpbInN1Yi1hbGxvdy1zdWJqZWN0Il0sImRlbnkiOlsic3ViLWRlbnktc3ViamVjdCJdfSwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMX0sInN1YiI6IlVBNktPTVE2N1hPRTNGSEUzN1c0T1hBRFZYVllJU0JOTFRCVVQyTFNZNVZGS0FJSjdDUkRSMlJaIn0.kKExDG8HH5zWytrknE8XXKSnkUdWQYi3FgCfNks0IM9LlJyDbiQIadpJa4eyFh_ajCIXKGhMTKIULEwNbrPbAQ\n" + + String expectedClaimBody = "{\"jti\":\"XAI5HUYQESXRZIYCDCWRSCXRDLVUG2G75QD4NJAOSTD72B6OIX6Q\",\"iat\":1633043378,\"iss\":\"ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY\",\"name\":\"custom\",\"sub\":\"UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ\",\"nats\":{\"issuer_account\":\"ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6\",\"tags\":[\"tag1\",\"tag\\\\two\"],\"type\":\"user\",\"version\":2,\"pub\":{\"allow\":[\"pub-allow-subject\"],\"deny\":[\"pub-deny-subject\"]},\"sub\":{\"allow\":[\"sub-allow-subject\"],\"deny\":[\"sub-deny-subject\"]},\"subs\":-1,\"data\":-1,\"payload\":-1}}"; + String expectedCred = "-----BEGIN NATS USER JWT-----\n" + + "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJqdGkiOiJYQUk1SFVZUUVTWFJaSVlDRENXUlNDWFJETFZVRzJHNzVRRDROSkFPU1RENzJCNk9JWDZRIiwiaWF0IjoxNjMzMDQzMzc4LCJpc3MiOiJBRFE0QllNNUtJQ1I1T1hEU1AzUzNXVko1Q1lFT1JHUUtUNzJTVlJGMlpEVkE3TFRGS01DSVBHWSIsIm5hbWUiOiJjdXN0b20iLCJzdWIiOiJVQTZLT01RNjdYT0UzRkhFMzdXNE9YQURWWFZZSVNCTkxUQlVUMkxTWTVWRktBSUo3Q1JEUjJSWiIsIm5hdHMiOnsiaXNzdWVyX2FjY291bnQiOiJBQ1haUkFMSUwyMldSRVREUlhZS09ZREI3WEMzRTdNQlNWVVNVTUZBQ082T001VlBSTkZNT09PNiIsInRhZ3MiOlsidGFnMSIsInRhZ1xcdHdvIl0sInR5cGUiOiJ1c2VyIiwidmVyc2lvbiI6MiwicHViIjp7ImFsbG93IjpbInB1Yi1hbGxvdy1zdWJqZWN0Il0sImRlbnkiOlsicHViLWRlbnktc3ViamVjdCJdfSwic3ViIjp7ImFsbG93IjpbInN1Yi1hbGxvdy1zdWJqZWN0Il0sImRlbnkiOlsic3ViLWRlbnktc3ViamVjdCJdfSwic3VicyI6LTEsImRhdGEiOi0xLCJwYXlsb2FkIjotMX19.hcNv6KKQh6cy6nTu6jOtXgf5vvhkuo3P2cB9s-AeOnYW9BZKWxljKmHHlrPiYJ-YdWvPA1y7XsMeKckRDP1rAQ\n" + "------END NATS USER JWT------\n" + "\n" + "************************* IMPORTANT *************************\n" + @@ -157,8 +177,9 @@ public void issueUserJWTSuccessCustom() throws Exception { "SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY\n" + "------END USER NKEY SEED------\n" + "\n" + - "*************************************************************\n", - cred); + "*************************************************************\n"; + assertEquals(expectedClaimBody, claimBody); + assertEquals(expectedCred, cred); } @Test @@ -172,15 +193,17 @@ public void issueUserJWTSuccessCustomLimits() throws Exception { .payload(3); String jwt = issueUserJWT(signingKey, new String(userKey.getPublicKey()), null, null, 1633043378, userClaim); + String claimBody = getClaimBody(jwt); String cred = String.format(NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed())); /* - Generated JWT: + Formatted Claim Body: { + "jti": "2GIBM5DP442KSBGE7DAS6ZDAB6Z5OPSHD4WBSHBZI7JVFVYBUMBQ", "iat": 1633043378, - "jti": "JUB65ANDEIROWEGEBOVRGEJRW6FX74PIJX3LJDYHBKKQITDGATTA", "iss": "ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY", "name": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ", + "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ", "nats": { "issuer_account": "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6", "type": "user", @@ -188,25 +211,25 @@ public void issueUserJWTSuccessCustomLimits() throws Exception { "subs": 1, "data": 2, "payload": 3 - }, - "sub": "UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ" + } } */ - - assertEquals("-----BEGIN NATS USER JWT-----\n" + - "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJpYXQiOjE2MzMwNDMzNzgsImp0aSI6IkVLNUpHR0VYVEhKWUZZM0VYSEZXSVVQTE5HWU82QjRaSUZETk43V1k2NVFYVEhMT0JPVUEiLCJpc3MiOiJBRFE0QllNNUtJQ1I1T1hEU1AzUzNXVko1Q1lFT1JHUUtUNzJTVlJGMlpEVkE3TFRGS01DSVBHWSIsIm5hbWUiOiJVQTZLT01RNjdYT0UzRkhFMzdXNE9YQURWWFZZSVNCTkxUQlVUMkxTWTVWRktBSUo3Q1JEUjJSWiIsIm5hdHMiOnsiaXNzdWVyX2FjY291bnQiOiJBQ1haUkFMSUwyMldSRVREUlhZS09ZREI3WEMzRTdNQlNWVVNVTUZBQ082T001VlBSTkZNT09PNiIsInR5cGUiOiJ1c2VyIiwidmVyc2lvbiI6Miwic3VicyI6MSwiZGF0YSI6MiwicGF5bG9hZCI6M30sInN1YiI6IlVBNktPTVE2N1hPRTNGSEUzN1c0T1hBRFZYVllJU0JOTFRCVVQyTFNZNVZGS0FJSjdDUkRSMlJaIn0.7nMNv_ugIM28U9va9cQR64LwbQZYEz7DZqJnD7Z-h49ZXp0PB96bHXfvzwgBgxNuTvlnWpnBmt5XtF33PlRMDw\n" + - "------END NATS USER JWT------\n" + - "\n" + - "************************* IMPORTANT *************************\n" + - " NKEY Seed printed below can be used to sign and prove identity.\n" + - " NKEYs are sensitive and should be treated as secrets.\n" + - "\n" + - "-----BEGIN USER NKEY SEED-----\n" + - "SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY\n" + - "------END USER NKEY SEED------\n" + - "\n" + - "*************************************************************\n", - cred); + String expectedClaimBody = "{\"jti\":\"2GIBM5DP442KSBGE7DAS6ZDAB6Z5OPSHD4WBSHBZI7JVFVYBUMBQ\",\"iat\":1633043378,\"iss\":\"ADQ4BYM5KICR5OXDSP3S3WVJ5CYEORGQKT72SVRF2ZDVA7LTFKMCIPGY\",\"name\":\"UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ\",\"sub\":\"UA6KOMQ67XOE3FHE37W4OXADVXVYISBNLTBUT2LSY5VFKAIJ7CRDR2RZ\",\"nats\":{\"issuer_account\":\"ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6\",\"type\":\"user\",\"version\":2,\"subs\":1,\"data\":2,\"payload\":3}}"; + String expectedCred = "-----BEGIN NATS USER JWT-----\n" + + "eyJ0eXAiOiJKV1QiLCAiYWxnIjoiZWQyNTUxOS1ua2V5In0.eyJqdGkiOiIyR0lCTTVEUDQ0MktTQkdFN0RBUzZaREFCNlo1T1BTSEQ0V0JTSEJaSTdKVkZWWUJVTUJRIiwiaWF0IjoxNjMzMDQzMzc4LCJpc3MiOiJBRFE0QllNNUtJQ1I1T1hEU1AzUzNXVko1Q1lFT1JHUUtUNzJTVlJGMlpEVkE3TFRGS01DSVBHWSIsIm5hbWUiOiJVQTZLT01RNjdYT0UzRkhFMzdXNE9YQURWWFZZSVNCTkxUQlVUMkxTWTVWRktBSUo3Q1JEUjJSWiIsInN1YiI6IlVBNktPTVE2N1hPRTNGSEUzN1c0T1hBRFZYVllJU0JOTFRCVVQyTFNZNVZGS0FJSjdDUkRSMlJaIiwibmF0cyI6eyJpc3N1ZXJfYWNjb3VudCI6IkFDWFpSQUxJTDIyV1JFVERSWFlLT1lEQjdYQzNFN01CU1ZVU1VNRkFDTzZPTTVWUFJORk1PT082IiwidHlwZSI6InVzZXIiLCJ2ZXJzaW9uIjoyLCJzdWJzIjoxLCJkYXRhIjoyLCJwYXlsb2FkIjozfX0.1DbLJayUdxCNaBapTRV8NkPmTGxa5R5TW5QsOB9iZoTuhgcVVI2tCOx1VIn7ccNJvmERw3dOpIf7uPVtxgv2AQ\n" + + "------END NATS USER JWT------\n" + + "\n" + + "************************* IMPORTANT *************************\n" + + " NKEY Seed printed below can be used to sign and prove identity.\n" + + " NKEYs are sensitive and should be treated as secrets.\n" + + "\n" + + "-----BEGIN USER NKEY SEED-----\n" + + "SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY\n" + + "------END USER NKEY SEED------\n" + + "\n" + + "*************************************************************\n"; + assertEquals(expectedClaimBody, claimBody); + assertEquals(expectedCred, cred); } @Test @@ -239,7 +262,6 @@ public void issueUserJWTBadPublicUserKey() throws Exception { public void userJwt() throws Exception { UserClaim uc = new UserClaim("test-issuer-account"); assertEquals(DEFAULT_JSON, uc.toJson()); - System.out.println(DEFAULT_JSON); List times = new ArrayList<>(); times.add(new TimeRange("01:15:00", "03:15:00")); From 42c4a7edf307e442cf5faeeed3fd245cc0a1acc9 Mon Sep 17 00:00:00 2001 From: scottf Date: Mon, 31 Jul 2023 14:49:01 -0400 Subject: [PATCH 2/4] Updated JwtUtils to support audience re-ordered json claim fields to match go, fixed tests --- .../io/nats/client/support/JsonUtils.java | 2 +- .../java/io/nats/client/support/JwtUtils.java | 29 ++++++++++++------- .../io/nats/client/support/JwtUtilsTests.java | 24 ++++++++------- 3 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/main/java/io/nats/client/support/JsonUtils.java b/src/main/java/io/nats/client/support/JsonUtils.java index ce8c3885b..ff37ce44d 100644 --- a/src/main/java/io/nats/client/support/JsonUtils.java +++ b/src/main/java/io/nats/client/support/JsonUtils.java @@ -254,7 +254,7 @@ public static void addFieldWhenGteMinusOne(StringBuilder sb, String fname, Long * @param value duration value */ public static void addFieldAsNanos(StringBuilder sb, String fname, Duration value) { - if (value != null && value != Duration.ZERO) { + if (value != null && !value.isZero() && !value.isNegative()) { sb.append(Q); jsonEncode(sb, fname); sb.append(QCOLON).append(value.toNanos()).append(COMMA); diff --git a/src/main/java/io/nats/client/support/JwtUtils.java b/src/main/java/io/nats/client/support/JwtUtils.java index 92a3bacbf..a7e1a85b7 100644 --- a/src/main/java/io/nats/client/support/JwtUtils.java +++ b/src/main/java/io/nats/client/support/JwtUtils.java @@ -168,7 +168,7 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String * @param name optional human-readable name. When absent, default to publicUserKey. * @param expiration optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. * @param issuedAt the current epoch seconds. - * @param audience the optional audience + * @param audience the optional audience * @param nats the user claim * @throws IllegalArgumentException if the accountId or publicUserKey is not a valid public key of the proper type * @throws NullPointerException if signingKey, accountId, or publicUserKey are null. @@ -189,13 +189,13 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String // Validate the publicUserKey: NKey userKey = NKey.fromPublicKey(publicUserKey.toCharArray()); if (userKey.getType() != NKey.Type.USER) { - throw new IllegalArgumentException("issueUserJWT requires a user key for the publicUserKey, but got " + userKey.getType()); + throw new IllegalArgumentException("issueUserJWT requires a user key for the publicUserKey parameter, but got " + userKey.getType()); } String accSigningKeyPub = new String(signingKey.getPublicKey()); String claimName = Validator.nullOrEmpty(name) ? publicUserKey : name; - return issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, audience, accSigningKeyPub, nats); + return issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, accSigningKeyPub, audience, nats); } /** @@ -212,31 +212,32 @@ public static String issueUserJWT(NKey signingKey, String publicUserKey, String * @return a JWT */ public static String issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String accSigningKeyPub, JsonSerializable nats) throws GeneralSecurityException, IOException { - return issueJWT(signingKey, publicUserKey, name, expiration, issuedAt, null, accSigningKeyPub, nats); + return issueJWT(signingKey, publicUserKey, name, expiration, issuedAt, accSigningKeyPub, null, nats); } /** * Issue a JWT + * * @param signingKey account nkey pair to sign the generated jwt. * @param publicUserKey a mandatory public user nkey. * @param name optional human-readable name. * @param expiration optional but recommended duration, when the generated jwt needs to expire. If not set, JWT will not expire. * @param issuedAt the current epoch seconds. - * @param audience the optional audience * @param accSigningKeyPub the account signing key + * @param audience the optional audience * @param nats the generic nats claim * @return a JWT * @throws GeneralSecurityException if SHA-256 MessageDigest is missing, or if the signingKey can not be used for signing. * @throws IOException if signingKey sign method throws this exception. */ - public static String issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String audience, String accSigningKeyPub, JsonSerializable nats) throws GeneralSecurityException, IOException { + public static String issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String accSigningKeyPub, String audience, JsonSerializable nats) throws GeneralSecurityException, IOException { Claim claim = new Claim(); - claim.exp = expiration; + claim.aud = audience; claim.iat = issuedAt; claim.iss = accSigningKeyPub; claim.name = name; claim.sub = publicUserKey; - claim.aud = audience; + claim.exp = expiration; claim.nats = nats; // Issue At time is stored in unix seconds @@ -260,9 +261,13 @@ public static String issueJWT(NKey signingKey, String publicUserKey, String name return ENCODED_CLAIM_HEADER + "." + encBody + "." + encSig; } + /** + * Get the claim body from a JWT + * @param jwt the encoded jwt + * @return the claim body json + */ public static String getClaimBody(String jwt) { - String[] split = jwt.split("\\."); - return fromBase64Url(split[1]); + return fromBase64Url(jwt.split("\\.")[1]); } public static class UserClaim implements JsonSerializable { @@ -456,10 +461,12 @@ public String toJson() { JsonUtils.addField(sb, "iss", iss); JsonUtils.addField(sb, "name", name); JsonUtils.addField(sb, "sub", sub); + if (exp != null && !exp.isZero() && !exp.isNegative()) { long seconds = exp.toMillis() / 1000; - JsonUtils.addField(sb, "exp", iat + seconds); + JsonUtils.addField(sb, "exp", iat + seconds); // relative to the iat } + JsonUtils.addField(sb, "nats", nats); return endJson(sb).toString(); } diff --git a/src/test/java/io/nats/client/support/JwtUtilsTests.java b/src/test/java/io/nats/client/support/JwtUtilsTests.java index 1a80e78c5..82087aeb5 100644 --- a/src/test/java/io/nats/client/support/JwtUtilsTests.java +++ b/src/test/java/io/nats/client/support/JwtUtilsTests.java @@ -34,7 +34,6 @@ public void issueUserJWTSuccessMinimal() throws Exception { String jwt = issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378, "audience"); String claimBody = getClaimBody(jwt); String cred = String.format(NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed())); - /* Formatted Claim Body: { @@ -233,35 +232,38 @@ public void issueUserJWTSuccessCustomLimits() throws Exception { } @Test - public void issueUserJWTBadSigningKey() throws Exception { + public void issueUserJWTBadSigningKey() { NKey userKey = NKey.fromSeed("SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY".toCharArray()); // should be account, but this is a user key: NKey signingKey = NKey.fromSeed("SUAIW7IZ2YDQYLTE4FJ64ZBX7UMLCN57V6GHALKMUSMJCU5PJDNUO6BVUI".toCharArray()); String accountId = "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6"; - assertThrows(IllegalArgumentException.class, () -> issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378)); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378)); + assertEquals("issueUserJWT requires an account key for the signingKey parameter, but got USER", e.getMessage()); } @Test - public void issueUserJWTBadAccountId() throws Exception { + public void issueUserJWTBadAccountId() { NKey userKey = NKey.fromSeed("SUAGL3KX4ZBBD53BNNLSHGAAGCMXSEYZ6NTYUBUCPZQGHYNK3ZRQBUDPRY".toCharArray()); NKey signingKey = NKey.fromSeed("SAANJIBNEKGCRUWJCPIWUXFBFJLR36FJTFKGBGKAT7AQXH2LVFNQWZJMQU".toCharArray()); // should be account, but this is a user key: String accountId = "UDN6WZFPYTS4YSUHUD4YFFU5NVKT6BVCY5QXQFYF3I23AER622SBOVUZ"; - assertThrows(IllegalArgumentException.class, () -> issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378)); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378)); + assertEquals("issueUserJWT requires an account key for the accountId parameter, but got USER", e.getMessage()); } @Test - public void issueUserJWTBadPublicUserKey() throws Exception { + public void issueUserJWTBadPublicUserKey() { NKey userKey = NKey.fromSeed("SAADFHQTEKYBOCG4CPEPNAJ5FLRX4G4WTCNTAIOKN3LARLHGVKB4BRUHYY".toCharArray()); NKey signingKey = NKey.fromSeed("SAANJIBNEKGCRUWJCPIWUXFBFJLR36FJTFKGBGKAT7AQXH2LVFNQWZJMQU".toCharArray()); String accountId = "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6"; - assertThrows(IllegalArgumentException.class, () -> issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378)); + IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378)); + assertEquals("issueUserJWT requires a user key for the publicUserKey, but got ACCOUNT", e.getMessage()); } @Test - public void userJwt() throws Exception { + public void testUserClaimJson() { UserClaim uc = new UserClaim("test-issuer-account"); - assertEquals(DEFAULT_JSON, uc.toJson()); + assertEquals(BASIC_JSON, uc.toJson()); List times = new ArrayList<>(); times.add(new TimeRange("01:15:00", "03:15:00")); @@ -281,11 +283,11 @@ public void userJwt() throws Exception { assertEquals(FULL_JSON, uc.toJson()); } - private static final String DEFAULT_JSON = "{\"issuer_account\":\"test-issuer-account\",\"type\":\"user\",\"version\":2,\"subs\":-1,\"data\":-1,\"payload\":-1}"; + private static final String BASIC_JSON = "{\"issuer_account\":\"test-issuer-account\",\"type\":\"user\",\"version\":2,\"subs\":-1,\"data\":-1,\"payload\":-1}"; private static final String FULL_JSON = "{\"issuer_account\":\"test-issuer-account\",\"tags\":[\"tag1\",\"tag2\"],\"type\":\"user\",\"version\":2,\"pub\":{\"allow\":[\"pa1\",\"pa2\"],\"deny\":[\"pd1\",\"pd2\"]},\"sub\":{\"allow\":[\"sa1\",\"sa2\"],\"deny\":[\"sd1\",\"sd2\"]},\"resp\":{\"max\":99,\"ttl\":999000000},\"src\":[\"src1\",\"src2\"],\"times\":[{\"start\":\"01:15:00\",\"end\":\"03:15:00\"}],\"times_location\":\"US\\/Eastern\",\"subs\":42,\"data\":43,\"payload\":44,\"bearer_token\":true,\"allowed_connection_types\":[\"nats\",\"tls\"]}"; /* - DEFAULT_JSON + BASIC_JSON { "issuer_account": "test-issuer-account", "type": "user", From 1bd40847445bcdb1cb419395b35e2482f132354a Mon Sep 17 00:00:00 2001 From: scottf Date: Mon, 31 Jul 2023 14:49:39 -0400 Subject: [PATCH 3/4] Updated JwtUtils to support audience re-ordered json claim fields to match go, fixed tests --- src/test/java/io/nats/client/support/JwtUtilsTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/io/nats/client/support/JwtUtilsTests.java b/src/test/java/io/nats/client/support/JwtUtilsTests.java index 82087aeb5..c706ec217 100644 --- a/src/test/java/io/nats/client/support/JwtUtilsTests.java +++ b/src/test/java/io/nats/client/support/JwtUtilsTests.java @@ -257,7 +257,7 @@ public void issueUserJWTBadPublicUserKey() { NKey signingKey = NKey.fromSeed("SAANJIBNEKGCRUWJCPIWUXFBFJLR36FJTFKGBGKAT7AQXH2LVFNQWZJMQU".toCharArray()); String accountId = "ACXZRALIL22WRETDRXYKOYDB7XC3E7MBSVUSUMFACO6OM5VPRNFMOOO6"; IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()), null, null, null, 1633043378)); - assertEquals("issueUserJWT requires a user key for the publicUserKey, but got ACCOUNT", e.getMessage()); + assertEquals("issueUserJWT requires a user key for the publicUserKey parameter, but got ACCOUNT", e.getMessage()); } @Test From 614b947ae8b58c9b3e249ad190d6a250c238e030 Mon Sep 17 00:00:00 2001 From: scottf Date: Mon, 31 Jul 2023 15:08:08 -0400 Subject: [PATCH 4/4] reverted uncessary change --- src/main/java/io/nats/client/NKey.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/nats/client/NKey.java b/src/main/java/io/nats/client/NKey.java index 72b3b157b..b2c93c254 100644 --- a/src/main/java/io/nats/client/NKey.java +++ b/src/main/java/io/nats/client/NKey.java @@ -1,4 +1,4 @@ -// Copyright 2023 The NATS Authors +// Copyright 2018 The NATS Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at: @@ -469,7 +469,7 @@ public static NKey fromPublicKey(char[] publicKey) { } Type type = NKey.Type.fromPrefix(prefix); - return new NKey(type, publicKey, new String(raw).toCharArray()); + return new NKey(type, publicKey, null); } /**