Skip to content

Commit

Permalink
Enhance jwt utils (#906)
Browse files Browse the repository at this point in the history
* Enhance jwt utils

* fine tuning
  • Loading branch information
scottf committed May 10, 2023
1 parent 536c7dc commit 468404b
Show file tree
Hide file tree
Showing 3 changed files with 239 additions and 75 deletions.
22 changes: 13 additions & 9 deletions src/main/java/io/nats/client/support/JsonUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,11 @@ public static <T> void _addList(StringBuilder sb, String fname, List<T> list, Li
* Appends a json field to a string builder.
* @param sb string builder
* @param fname fieldname
* @param strArray field value
* @param strings field value
*/
public static void addStrings(StringBuilder sb, String fname, String[] strArray) {
if (strArray != null && strArray.length > 0) {
addStrings(sb, fname, Arrays.asList(strArray));
public static void addStrings(StringBuilder sb, String fname, String[] strings) {
if (strings != null && strings.length > 0) {
_addStrings(sb, fname, Arrays.asList(strings));
}
}

Expand All @@ -312,14 +312,18 @@ public static void addStrings(StringBuilder sb, String fname, String[] strArray)
*/
public static void addStrings(StringBuilder sb, String fname, List<String> strings) {
if (strings != null && strings.size() > 0) {
_addList(sb, fname, strings, (sbs, s) -> {
sb.append(Q);
jsonEncode(sb, s);
sb.append(Q);
});
_addStrings(sb, fname, strings);
}
}

private static void _addStrings(StringBuilder sb, String fname, List<String> strings) {
_addList(sb, fname, strings, (sbs, s) -> {
sb.append(Q);
jsonEncode(sb, s);
sb.append(Q);
});
}

/**
* Appends a json field to a string builder.
* @param sb string builder
Expand Down
229 changes: 166 additions & 63 deletions src/main/java/io/nats/client/support/JwtUtils.java
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021 The NATS Authors
// Copyright 2021-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:
Expand All @@ -20,68 +20,24 @@
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;

import static io.nats.client.support.Encoding.base32Encode;
import static io.nats.client.support.Encoding.toBase64Url;
import static io.nats.client.support.JsonUtils.beginJson;
import static io.nats.client.support.JsonUtils.endJson;

/**
* Implements https://github.com/nats-io/nats-architecture-and-design/blob/main/adr/ADR-14.md
* Implements <a href="https://github.com/nats-io/nats-architecture-and-design/blob/main/adr/ADR-14.md">ADR-14</a>
*/
public abstract class JwtUtils {

private JwtUtils() {} /* ensures cannot be constructed */

static class Nats implements JsonSerializable {
String issuerAccount;
String[] tags;

@Override
public String toJson() {
StringBuilder sb = beginJson();
JsonUtils.addField(sb, "issuer_account", issuerAccount);
JsonUtils.addStrings(sb, "tags", null == tags ? Collections.emptyList() : Arrays.asList(tags));
JsonUtils.addField(sb, "type", "user");
JsonUtils.addField(sb, "version", 2);
return endJson(sb).toString();
}
}

static class Claim implements JsonSerializable {
Duration exp;
long iat;
String iss;
String jti;
String name;
Nats nats;
String sub;

@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.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();
}
}

private static final String ENCODED_CLAIM_HEADER =
toBase64Url("{\"typ\":\"JWT\", \"alg\":\"ed25519-nkey\"}");

/**
* Format string with `%s` place holder for the JWT token followed
* Format string with `%s` placeholder for the JWT token followed
* by the user NKey seed. This can be directly used as such:
*
* <pre>
Expand All @@ -106,8 +62,8 @@ public String toJson() {
"*************************************************************\n";

/**
* Issue a user JWT from a scoped signing key. See https://docs.nats.io/nats-tools/nsc/signing_keys
*
* 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 @@ -122,12 +78,12 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ
}

/**
* Issue a user JWT from a scoped signing key. See https://docs.nats.io/nats-tools/nsc/signing_keys
*
* 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.
* @param name optional human readable name. When absent, default to publicUserKey.
* @param name optional human-readable name. When absent, default to publicUserKey.
* @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.
Expand All @@ -139,12 +95,12 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ
}

/**
* Issue a user JWT from a scoped signing key. See https://docs.nats.io/nats-tools/nsc/signing_keys
*
* 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.
* @param name optional human readable name. When absent, default to publicUserKey.
* @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 tags optional list of tags to be included in the JWT.
* @throws IllegalArgumentException if the accountId or publicUserKey is not a valid public key of the proper type
Expand All @@ -158,12 +114,12 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ
}

/**
* Method used for testing.
* 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.
* @param name optional human readable name. When absent, default to publicUserKey.
* @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 tags optional list of tags to be included in the JWT.
* @param issuedAt the current epoch seconds.
Expand All @@ -173,13 +129,32 @@ public static String issueUserJWT(NKey signingKey, String accountId, String publ
* @throws IOException if signingKey sign method throws this exception.
* @return a JWT
*/
protected static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name, Duration expiration, String[] tags, long issuedAt) throws GeneralSecurityException, IOException {
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));
}

/**
* 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 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, 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());
}
// Validate the accountId:
NKey accountKey = NKey.fromPublicKey(accountId.toCharArray());
NKey accountKey = NKey.fromPublicKey(nats.issuerAccount.toCharArray());
if (accountKey.getType() != NKey.Type.ACCOUNT) {
throw new IllegalArgumentException("issueUserJWT requires an account key for the accountId parameter, but got " + accountKey.getType());
}
Expand All @@ -190,15 +165,32 @@ protected static String issueUserJWT(NKey signingKey, String accountId, String p
}
String accSigningKeyPub = new String(signingKey.getPublicKey());

String claimName = Validator.nullOrEmpty(name) ? publicUserKey : name;

return issueJWT(signingKey, publicUserKey, claimName, expiration, issuedAt, 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 nats the generic nats claim
* @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 issueJWT(NKey signingKey, String publicUserKey, String name, Duration expiration, long issuedAt, String accSigningKeyPub, JsonSerializable nats) throws GeneralSecurityException, IOException {
Claim claim = new Claim();
claim.exp = expiration;
claim.iat = issuedAt;
claim.iss = accSigningKeyPub;
claim.name = Validator.nullOrEmpty(name) ? publicUserKey : name;
claim.name = name;
claim.sub = publicUserKey;
claim.nats = new Nats();
claim.nats.issuerAccount = accountId;
claim.nats.tags = tags;
claim.nats = nats;

// Issue At time is stored in unix seconds
String claimJson = claim.toJson();
Expand All @@ -220,4 +212,115 @@ protected static String issueUserJWT(NKey signingKey, String accountId, String p
// append signature to header and body and return it
return ENCODED_CLAIM_HEADER + "." + encBody + "." + encSig;
}

public static class UserClaim extends NatsClaim<UserClaim> {
public Permission pub;
public Permission sub;

public UserClaim(String issuerAccount) {
super("user", issuerAccount);
}

public UserClaim pub(Permission pub) {
this.pub = pub;
return this;
}

public UserClaim sub(Permission sub) {
this.sub = sub;
return this;
}

@Override
protected UserClaim getThis() {
return this;
}

@Override
protected void subAppendJson(StringBuilder sb) {
JsonUtils.addField(sb, "pub", pub);
JsonUtils.addField(sb, "sub", sub);
}
}

protected abstract static class NatsClaim<T> implements JsonSerializable {
public int version = 2;
public String type;
public String issuerAccount;
public String[] tags;

protected abstract T getThis();

protected NatsClaim(String type, String issuerAccount) {
this.type = type;
this.issuerAccount = issuerAccount;
}

public T tags(String[] tags) {
this.tags = tags;
return getThis();
}

protected abstract void subAppendJson(StringBuilder sb);

@Override
public String toJson() {
StringBuilder sb = beginJson();
JsonUtils.addField(sb, "issuer_account", issuerAccount);
JsonUtils.addStrings(sb, "tags", tags);
JsonUtils.addField(sb, "type", type);
JsonUtils.addField(sb, "version", version);
subAppendJson(sb);
return endJson(sb).toString();
}
}

public static class Permission implements JsonSerializable {
public String[] allow;
public String[] deny;

public Permission allow(String... allow) {
this.allow = allow;
return this;
}

public Permission deny(String... deny) {
this.deny = deny;
return this;
}

@Override
public String toJson() {
StringBuilder sb = beginJson();
JsonUtils.addStrings(sb, "allow", allow);
JsonUtils.addStrings(sb, "deny", deny);
return endJson(sb).toString();
}
}

static class Claim implements JsonSerializable {
Duration exp;
long iat;
String iss;
String jti;
String name;
JsonSerializable nats;
String sub;

@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.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();
}
}
}
Loading

0 comments on commit 468404b

Please sign in to comment.