
io.nats.client.support.JwtUtils Maven / Gradle / Ivy
// 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:
//
// 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;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.time.Duration;
import java.util.List;
import static io.nats.client.support.Encoding.*;
import static io.nats.client.support.JsonUtils.beginJson;
import static io.nats.client.support.JsonUtils.endJson;
/**
* Implements ADR-14
*/
public abstract class JwtUtils {
private JwtUtils() {} /* ensures cannot be constructed */
private static final String ENCODED_CLAIM_HEADER =
base64UrlEncodeToString("{\"typ\":\"JWT\", \"alg\":\"ed25519-nkey\"}");
private static final long NO_LIMIT = -1;
/**
* Format string with `%s` placeholder for the JWT token followed
* by the user NKey seed. This can be directly used as such:
*
*
* NKey userKey = NKey.createUser(new SecureRandom());
* NKey signingKey = loadFromSecretStore();
* String jwt = issueUserJWT(signingKey, accountId, new String(userKey.getPublicKey()));
* String.format(JwtUtils.NATS_USER_JWT_FORMAT, jwt, new String(userKey.getSeed()));
*
*/
public static final String NATS_USER_JWT_FORMAT = "-----BEGIN NATS USER JWT-----\n" +
"%s\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" +
"%s\n" +
"------END USER NKEY SEED------\n" +
"\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.
* @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 accountId, String publicUserKey) throws GeneralSecurityException, IOException {
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.
* @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.
* @throws IOException if signingKey sign method throws this exception.
* @return a JWT
*/
public static String issueUserJWT(NKey signingKey, String accountId, String publicUserKey, String name) throws GeneralSecurityException, IOException {
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.
* @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
* @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 accountId, String publicUserKey, String name, Duration expiration, String... tags) throws GeneralSecurityException, IOException {
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.
* @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.
* @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 accountId, String publicUserKey, String name, Duration expiration, String[] tags, long issuedAt) throws GeneralSecurityException, IOException {
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.
* @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 {
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());
}
// Validate the accountId:
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());
}
// 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 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, 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.
* @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 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 {
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.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.US_ASCII));
claim.jti = new String(base32Encode(encoded));
claimJson = claim.toJson();
// all three components (header/body/signature) are base64url encoded
String encBody = base64UrlEncodeToString(claimJson);
// compute the signature off of header + body (. included on purpose)
byte[] sig = (ENCODED_CLAIM_HEADER + "." + encBody).getBytes(StandardCharsets.UTF_8);
String encSig = base64UrlEncodeToString(signingKey.sign(sig));
// append signature to header and body and return it
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 base64UrlDecodeToString(jwt.split("\\.")[1]);
}
public static class UserClaim implements JsonSerializable {
public String issuerAccount; // User
public String[] tags; // User/GenericFields
public String type = "user"; // User/GenericFields
public int version = 2; // User/GenericFields
public Permission pub; // User/UserPermissionLimits/Permissions
public Permission sub; // User/UserPermissionLimits/Permissions
public ResponsePermission resp; // User/UserPermissionLimits/Permissions
public String[] src; // User/UserPermissionLimits/Limits/UserLimits
public List times; // User/UserPermissionLimits/Limits/UserLimits
public String locale; // User/UserPermissionLimits/Limits/UserLimits
public long subs = NO_LIMIT; // User/UserPermissionLimits/Limits/NatsLimits
public long data = NO_LIMIT; // User/UserPermissionLimits/Limits/NatsLimits
public long payload = NO_LIMIT; // User/UserPermissionLimits/Limits/NatsLimits
public boolean bearerToken; // User/UserPermissionLimits
public String[] allowedConnectionTypes; // User/UserPermissionLimits
public UserClaim(String issuerAccount) {
this.issuerAccount = issuerAccount;
}
@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);
JsonUtils.addField(sb, "pub", pub);
JsonUtils.addField(sb, "sub", sub);
JsonUtils.addField(sb, "resp", resp);
JsonUtils.addStrings(sb, "src", src);
JsonUtils.addJsons(sb, "times", times);
JsonUtils.addField(sb, "times_location", locale);
JsonUtils.addFieldWhenGteMinusOne(sb, "subs", subs);
JsonUtils.addFieldWhenGteMinusOne(sb, "data", data);
JsonUtils.addFieldWhenGteMinusOne(sb, "payload", payload);
JsonUtils.addFldWhenTrue(sb, "bearer_token", bearerToken);
JsonUtils.addStrings(sb, "allowed_connection_types", allowedConnectionTypes);
return endJson(sb).toString();
}
public UserClaim tags(String... tags) {
this.tags = tags;
return this;
}
public UserClaim pub(Permission pub) {
this.pub = pub;
return this;
}
public UserClaim sub(Permission sub) {
this.sub = sub;
return this;
}
public UserClaim resp(ResponsePermission resp) {
this.resp = resp;
return this;
}
public UserClaim src(String... src) {
this.src = src;
return this;
}
public UserClaim times(List times) {
this.times = times;
return this;
}
public UserClaim locale(String locale) {
this.locale = locale;
return this;
}
public UserClaim subs(long subs) {
this.subs = subs;
return this;
}
public UserClaim data(long data) {
this.data = data;
return this;
}
public UserClaim payload(long payload) {
this.payload = payload;
return this;
}
public UserClaim bearerToken(boolean bearerToken) {
this.bearerToken = bearerToken;
return this;
}
public UserClaim allowedConnectionTypes(String... allowedConnectionTypes) {
this.allowedConnectionTypes = allowedConnectionTypes;
return this;
}
}
public static class TimeRange implements JsonSerializable {
public String start;
public String end;
public TimeRange(String start, String end) {
this.start = start;
this.end = end;
}
@Override
public String toJson() {
StringBuilder sb = beginJson();
JsonUtils.addField(sb, "start", start);
JsonUtils.addField(sb, "end", end);
return endJson(sb).toString();
}
}
public static class ResponsePermission implements JsonSerializable {
public int maxMsgs;
public Duration expires;
public ResponsePermission maxMsgs(int maxMsgs) {
this.maxMsgs = maxMsgs;
return this;
}
public ResponsePermission expires(Duration expires) {
this.expires = expires;
return this;
}
public ResponsePermission expires(long expiresMillis) {
this.expires = Duration.ofMillis(expiresMillis);
return this;
}
@Override
public String toJson() {
StringBuilder sb = beginJson();
JsonUtils.addField(sb, "max", maxMsgs);
JsonUtils.addFieldAsNanos(sb, "ttl", expires);
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 {
String aud;
String jti;
long iat;
String iss;
String name;
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); // relative to the iat
}
JsonUtils.addField(sb, "nats", nats);
return endJson(sb).toString();
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy