io.nats.client.support.JwtUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jnats Show documentation
Show all versions of jnats Show documentation
Client library for working with the NATS messaging system.
// 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