diff --git a/src/examples/java/io/nats/examples/service/AuthCalloutServiceExample.java b/src/examples/java/io/nats/examples/authcallout/AuthCalloutServiceExample.java similarity index 52% rename from src/examples/java/io/nats/examples/service/AuthCalloutServiceExample.java rename to src/examples/java/io/nats/examples/authcallout/AuthCalloutServiceExample.java index 4745b274d..adecf8766 100644 --- a/src/examples/java/io/nats/examples/service/AuthCalloutServiceExample.java +++ b/src/examples/java/io/nats/examples/authcallout/AuthCalloutServiceExample.java @@ -11,7 +11,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package io.nats.examples.service; +package io.nats.examples.authcallout; import io.nats.client.*; import io.nats.client.impl.Headers; @@ -24,48 +24,38 @@ import java.util.Map; import java.util.concurrent.CompletableFuture; -import static io.nats.jwt.Utils.currentTimeSeconds; import static io.nats.jwt.Utils.getClaimBody; -/** - * This example demonstrates basic setup and use of the Service Framework - */ public class AuthCalloutServiceExample { - static String ISSUER_NKEY = "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"; static String ISSUER_NSEED = "SAANDLKMXL6CUS3CP52WIXBEDN6YJ545GDKC65U5JZPPV6WH6ESWUA6YAI"; - static final Map NATS_USERS; + static final Map NATS_USERS; -// static final NKey USER_KEY; static final NKey USER_SIGNING_KEY; -// static final String PUB_USER_KEY; static final String PUB_USER_SIGNING_KEY; static { try { -// USER_KEY = NKey.fromSeed(ISSUER_NKEY.toCharArray()); -// PUB_USER_KEY = new String(USER_KEY.getPublicKey()); USER_SIGNING_KEY = NKey.fromSeed(ISSUER_NSEED.toCharArray()); PUB_USER_SIGNING_KEY = new String(USER_SIGNING_KEY.getPublicKey()); } catch (Exception e) { throw new RuntimeException(e); } - // curveNKey = NKey.fromSeed(ISSUER_XSEED.toCharArray()); - // sys/sys, SYS - // alice/alice, APP - // bob/bob, APP, pub allow "bob.>", sub allow "bob.>", response max 1 + // This sets up a map of users to simulate a back end auth system + // sys/sys, SYS + // alice/alice, APP + // bob/bob, APP, pub allow "bob.>", sub allow "bob.>", response max 1 NATS_USERS = new HashMap<>(); - NATS_USERS.put("sys", new NatsUser().userPass("sys").account("SYS")); - NATS_USERS.put("alice", new NatsUser().userPass("alice").account("APP")); + NATS_USERS.put("sys", new AuthCalloutUser().userPass("sys").account("SYS")); + NATS_USERS.put("alice", new AuthCalloutUser().userPass("alice").account("APP")); Permission p = new Permission().allow("bob.>"); ResponsePermission r = new ResponsePermission().max(1); - NATS_USERS.put("bob", new NatsUser().userPass("bob").account("APP").pub(p).sub(p).resp(r)); + NATS_USERS.put("bob", new AuthCalloutUser().userPass("bob").account("APP").pub(p).sub(p).resp(r)); } public static void main(String[] args) throws Exception { - Options options = new Options.Builder() .server("nats://localhost:4222") .errorListener(new ErrorListener() {}) @@ -75,76 +65,39 @@ public static void main(String[] args) throws Exception { try (Connection nc = Nats.connect(options)) { // endpoints can be created ahead of time // or created directly by the ServiceEndpoint builder. - Endpoint epAuthCallout = Endpoint.builder() - .name("AuthEndpoint") + Endpoint endpoint = Endpoint.builder() + .name("AuthCallbackEndpoint") .subject("$SYS.REQ.USER.AUTH") .build(); AuthCalloutHandler handler = new AuthCalloutHandler(nc); - ServiceEndpoint seAuthCallout = ServiceEndpoint.builder() - .endpoint(epAuthCallout) + + ServiceEndpoint serviceEndpoint = ServiceEndpoint.builder() + .endpoint(endpoint) .handler(handler) .build(); - // Create the service from service endpoints. - Service service1 = new ServiceBuilder() + // Create the service from service endpoint. + Service acService = new ServiceBuilder() .connection(nc) - .name("AuthService") + .name("AuthCallbackService") .version("0.0.1") - .addServiceEndpoint(seAuthCallout) + .addServiceEndpoint(serviceEndpoint) .build(); - System.out.println("\n" + service1); + System.out.println("\n" + acService); // ---------------------------------------------------------------------------------------------------- // Start the services // ---------------------------------------------------------------------------------------------------- - CompletableFuture serviceStoppedFuture = service1.startService(); - - AuthCalloutUserExample.main("alicex", "alice"); - AuthCalloutUserExample.main("alice", "alice"); + CompletableFuture serviceStoppedFuture = acService.startService(); + serviceStoppedFuture.get(); // waits indefinitely since there is nothing calling stop } catch (Exception e) { - e.printStackTrace(); + onException(e); } } - /* - { - "aud": "nats-authorization-request", - "jti": "43CCBS2OARGHVNIMBI2IEGVXSCT2L25WNQ4D2BF2T24DWORJLE2A", - "iat": 1704653960, - "iss": "NC5WUN42IKPYZQI5EWREZOHGYHZA6XN7UE766VSNBV3AOPTYKAISY6XV", - "sub": "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA", - "exp": 1704653962, - "nats": { - "client_info": { - "kind": "Client", - "host": "127.0.0.1", - "id": 28, - "type": "nats", - "user": "alicex" - }, - "connect_opts": { - "protocol": 1, - "pass": "alice", - "lang": "java", - "user": "alicex", - "version": "2.17.3.dev" - }, - "server_id": { - "name": "NC5WUN42IKPYZQI5EWREZOHGYHZA6XN7UE766VSNBV3AOPTYKAISY6XV", - "host": "0.0.0.0", - "id": "NC5WUN42IKPYZQI5EWREZOHGYHZA6XN7UE766VSNBV3AOPTYKAISY6XV", - "version": "2.10.4" - }, - "type": "authorization_request", - "version": 2, - "user_nkey": "UCPGU74LE4GOMJZFFD6Z4QJLSUGUJIQ23OH6IZUMRI3BXITCIPPFGW6D" - } - } - */ - static class AuthCalloutHandler implements ServiceMessageHandler { Connection nc; @@ -154,13 +107,14 @@ public AuthCalloutHandler(Connection nc) { @Override public void onMessage(ServiceMessage smsg) { - System.out.println("\nSubject : " + smsg.getSubject()); - System.out.println("Headers : " + hToString(smsg.getHeaders())); + System.out.println("\nReceived Message"); + System.out.println("Subject : " + smsg.getSubject()); + System.out.println("Headers : " + headersToString(smsg.getHeaders())); try { // Convert the message data into a Claim Claim claim = new Claim(getClaimBody(smsg.getData())); - System.out.println("Claim : " + claim.toJson()); + System.out.println("Claim-Request : " + claim.toJson()); // The Claim should contain an Authorization Request AuthorizationRequest ar = claim.authorizationRequest; @@ -168,24 +122,27 @@ public void onMessage(ServiceMessage smsg) { System.err.println("Invalid Authorization Request Claim"); return; } - System.out.println("Auth Req : " + ar.toJson()); + printJson("Auth Request : ", ar.toJson(), "server_id", "user_nkey", "client_info", "connect_opts", "client_tls", "request_nonce"); // Check if the user exists. - NatsUser u = NATS_USERS.get(ar.connectOpts.user); - if (u == null) { + AuthCalloutUser acUser = NATS_USERS.get(ar.connectOpts.user); + if (acUser == null) { respond(smsg, ar, null, "User Not Found: " + ar.connectOpts.user); return; } + if (!acUser.pass.equals(ar.connectOpts.pass)) { + respond(smsg, ar, null, "Password does not match: " + acUser.pass + " != " + ar.connectOpts.pass); + return; + } UserClaim uc = new UserClaim() - .pub(u.pub) - .sub(u.sub) - .resp(u.resp); + .pub(acUser.pub) + .sub(acUser.sub) + .resp(acUser.resp); String userJwt = new ClaimIssuer() - .aud(u.account) + .aud(acUser.account) .name(ar.connectOpts.user) - .iat(currentTimeSeconds()) .iss(PUB_USER_SIGNING_KEY) .sub(ar.userNkey) .nats(uc) @@ -198,24 +155,55 @@ public void onMessage(ServiceMessage smsg) { } } + static final String SPACER = " "; + private void printJson(String label, String json, String... splits) { + if (splits != null && splits.length > 0) { + String indent = SPACER.substring(0, label.length()); + boolean first = true; + for (String split : splits) { + int at = json.indexOf("\"" + split + "\""); + if (at > 0) { + if (first) { + first = false; + System.out.println(label + json.substring(0, at)); + } + else { + System.out.println(indent + json.substring(0, at)); + } + json = json.substring(at); + } + } + System.out.println(indent + json); + } + else { + System.out.println(label + json); + } + } + private void respond(ServiceMessage smsg, AuthorizationRequest ar, String userJwt, String error) throws GeneralSecurityException, IOException { + AuthorizationResponse response = new AuthorizationResponse() .jwt(userJwt) .error(error); - System.out.println("Auth Resp : " + response.toJson()); + + if (userJwt != null) { + printJson("Auth Resp JWT : ", getClaimBody(userJwt), "name", "nats"); + } + else { + System.out.println("Auth Resp ERR : " + response.toJson()); + } String jwt = new ClaimIssuer() .aud(ar.serverId.id) - .iat(currentTimeSeconds()) .iss(PUB_USER_SIGNING_KEY) .sub(ar.userNkey) .nats(response) .issueJwt(USER_SIGNING_KEY); - System.out.println("Claim Resp : " + getClaimBody(jwt)); + System.out.println("Claim-Response: " + getClaimBody(jwt)); smsg.respond(nc, jwt); } } @@ -225,52 +213,7 @@ private static void onException(Exception e) { e.printStackTrace(); } - static class NatsUser { - public String user; - public String pass; - public String account; - public Permission pub; - public Permission sub; - public ResponsePermission resp; - - public NatsUser userPass(String userPass) { - this.user = userPass; - this.pass = userPass; - return this; - } - - public NatsUser user(String user) { - this.user = user; - return this; - } - - public NatsUser pass(String pass) { - this.pass = pass; - return this; - } - - public NatsUser account(String account) { - this.account = account; - return this; - } - - public NatsUser pub(Permission pub) { - this.pub = pub; - return this; - } - - public NatsUser sub(Permission sub) { - this.sub = sub; - return this; - } - - public NatsUser resp(ResponsePermission resp) { - this.resp = resp; - return this; - } - } - - public static String hToString(Headers h) { + public static String headersToString(Headers h) { if (h == null || h.isEmpty()) { return "None"; } diff --git a/src/examples/java/io/nats/examples/authcallout/AuthCalloutUser.java b/src/examples/java/io/nats/examples/authcallout/AuthCalloutUser.java new file mode 100644 index 000000000..65af691c1 --- /dev/null +++ b/src/examples/java/io/nats/examples/authcallout/AuthCalloutUser.java @@ -0,0 +1,49 @@ +package io.nats.examples.authcallout; + +import io.nats.jwt.Permission; +import io.nats.jwt.ResponsePermission; + +public class AuthCalloutUser { + public String user; + public String pass; + public String account; + public Permission pub; + public Permission sub; + public ResponsePermission resp; + + public AuthCalloutUser userPass(String userPass) { + this.user = userPass; + this.pass = userPass; + return this; + } + + public AuthCalloutUser user(String user) { + this.user = user; + return this; + } + + public AuthCalloutUser pass(String pass) { + this.pass = pass; + return this; + } + + public AuthCalloutUser account(String account) { + this.account = account; + return this; + } + + public AuthCalloutUser pub(Permission pub) { + this.pub = pub; + return this; + } + + public AuthCalloutUser sub(Permission sub) { + this.sub = sub; + return this; + } + + public AuthCalloutUser resp(ResponsePermission resp) { + this.resp = resp; + return this; + } +} diff --git a/src/examples/java/io/nats/examples/service/AuthCalloutUserExample.java b/src/examples/java/io/nats/examples/service/AuthCalloutUserExample.java deleted file mode 100644 index 6a273e755..000000000 --- a/src/examples/java/io/nats/examples/service/AuthCalloutUserExample.java +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2022 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.examples.service; - -import io.nats.client.Connection; -import io.nats.client.ErrorListener; -import io.nats.client.Nats; -import io.nats.client.Options; -import io.nats.service.ServiceMessage; - -import java.io.IOException; - -/** - * This example demonstrates basic setup and use of the Service Framework - */ -public class AuthCalloutUserExample { - public static void main(String[] args) throws IOException { - main("alice", "alice"); - } - - public static void main(String u, String p) { - Options options = new Options.Builder() - .server("nats://localhost:4222") - .errorListener(new ErrorListener() {}) - .userInfo(u, p) - .build(); - - try (Connection nc = Nats.connect(options)) { - System.out.println("\nAuthCalloutUserExample --> Connected"); - System.out.println(nc.getServerInfo()); - System.out.println("----------------------------------------------------------------------------------------------------"); - } - catch (Exception e) { - System.err.println("\nAuthCalloutUserExample --> Did Not Connect"); - e.printStackTrace(); - System.err.println("----------------------------------------------------------------------------------------------------"); - } - } - - private static void handleAuthMessage(Connection nc, ServiceMessage smsg) { - smsg.respond(nc, smsg.getData()); - } -} diff --git a/src/main/java/io/nats/jwt/Claim.java b/src/main/java/io/nats/jwt/Claim.java index 830effdbf..192f58c01 100644 --- a/src/main/java/io/nats/jwt/Claim.java +++ b/src/main/java/io/nats/jwt/Claim.java @@ -48,10 +48,10 @@ public Claim(char[] json) throws JsonParseException { Claim(ClaimIssuer issuer) { aud = issuer.aud; - exp = issuer.exp == null ? 0 : issuer.exp; jti = issuer.jti; - iat = issuer.iat; + iat = issuer.iatResolved; iss = issuer.iss; + exp = issuer.expResolved; name = issuer.name; nbf = issuer.nbf; sub = issuer.sub; diff --git a/src/main/java/io/nats/jwt/ClaimIssuer.java b/src/main/java/io/nats/jwt/ClaimIssuer.java index 04b181cff..16498ae5b 100644 --- a/src/main/java/io/nats/jwt/ClaimIssuer.java +++ b/src/main/java/io/nats/jwt/ClaimIssuer.java @@ -25,25 +25,34 @@ import static io.nats.client.support.Encoding.base32Encode; import static io.nats.client.support.Encoding.toBase64Url; import static io.nats.jwt.Utils.ENCODED_CLAIM_HEADER; +import static io.nats.jwt.Utils.currentTimeSeconds; public class ClaimIssuer { String aud; String jti; - long iat; - Long exp; + Long iatInput; + Long expInput; + long iatResolved; + long expResolved; String iss; String name; String nbf; String sub; - Duration expiresIn; JsonSerializable nats; + Duration expiresInInput; public String issueJwt(NKey signingKey) throws GeneralSecurityException, IOException { - if (exp == null) { - if (expiresIn != null && !expiresIn.isZero() && !expiresIn.isNegative()) { - exp = iat + (expiresIn.toMillis() / 1000); + iatResolved = iatInput == null ? currentTimeSeconds() : iatInput; + if (expInput == null) { + if (expiresInInput != null) { + long millis = expiresInInput.toMillis(); + if (millis > 0) { + expInput = iatResolved + (millis / 1000); + } } } + expResolved = expInput == null ? 0 : expInput; + Claim claim = new Claim(this); // Issue At time is stored in unix seconds @@ -72,21 +81,11 @@ public ClaimIssuer nats(JsonSerializable nats) { return this; } - public ClaimIssuer expiresIn(Duration expiresIn) { - this.expiresIn = expiresIn; - return this; - } - public ClaimIssuer aud(String aud) { this.aud = aud; return this; } - public ClaimIssuer iat(long iat) { - this.iat = iat; - return this; - } - public ClaimIssuer iss(String iss) { this.iss = iss; return this; @@ -107,8 +106,18 @@ public ClaimIssuer sub(String sub) { return this; } + public ClaimIssuer iat(long iat) { + this.iatInput = iat; + return this; + } + public ClaimIssuer exp(Long exp) { - this.exp = exp; + this.expInput = exp; + return this; + } + + public ClaimIssuer expiresIn(Duration expiresIn) { + this.expiresInInput = expiresIn; return this; } }