Skip to content

Commit

Permalink
Jwt Utils update to support "audience" (#949)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottf committed Jul 31, 2023
1 parent e35f029 commit f362bc7
Show file tree
Hide file tree
Showing 4 changed files with 189 additions and 92 deletions.
20 changes: 18 additions & 2 deletions src/main/java/io/nats/client/support/Encoding.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/io/nats/client/support/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
107 changes: 82 additions & 25 deletions src/main/java/io/nats/client/support/JwtUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 <a href="https://docs.nats.io/nats-tools/nsc/signing_keys">Signing Keys</a>
*
* @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.
Expand All @@ -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 <a href="https://docs.nats.io/nats-tools/nsc/signing_keys">Signing Keys</a>
*
* @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.
Expand All @@ -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 <a href="https://docs.nats.io/nats-tools/nsc/signing_keys">Signing Keys</a>
*
* @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.
Expand All @@ -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 <a href="https://docs.nats.io/nats-tools/nsc/signing_keys">Signing Keys</a>
*
* @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.
Expand All @@ -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 <a href="https://docs.nats.io/nats-tools/nsc/signing_keys">Signing Keys</a>
*
* @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.
Expand All @@ -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 <a href="https://docs.nats.io/nats-tools/nsc/signing_keys">Signing Keys</a>
* @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());
Expand All @@ -164,18 +189,17 @@ 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, accSigningKeyPub, nats);
return issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, accSigningKeyPub, audience, 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.
Expand All @@ -188,20 +212,40 @@ 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, 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 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 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.exp = expiration;
claim.nats = nats;

// Issue At time is stored in unix seconds
String claimJson = claim.toJson();

// 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();
Expand All @@ -217,6 +261,15 @@ 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) {
return fromBase64Url(jwt.split("\\.")[1]);
}

public static class UserClaim implements JsonSerializable {
public String issuerAccount; // User
public String[] tags; // User/GenericFields
Expand Down Expand Up @@ -390,27 +443,31 @@ 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();
if (exp != null && !exp.isZero() && !exp.isNegative()) {
long seconds = exp.toMillis() / 1000;
JsonUtils.addField(sb, "exp", iat + seconds);
}
JsonUtils.addField(sb, "iat", iat);
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, "nats", nats);
JsonUtils.addField(sb, "sub", sub);

if (exp != null && !exp.isZero() && !exp.isNegative()) {
long seconds = exp.toMillis() / 1000;
JsonUtils.addField(sb, "exp", iat + seconds); // relative to the iat
}

JsonUtils.addField(sb, "nats", nats);
return endJson(sb).toString();
}
}
Expand Down
Loading

0 comments on commit f362bc7

Please sign in to comment.