Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Jwt Utils update to support "audience" #949

Merged
merged 4 commits into from
Jul 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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