Skip to content

Commit

Permalink
Ability to set user credentials from strings(s) (#885)
Browse files Browse the repository at this point in the history
  • Loading branch information
scottf authored Apr 9, 2024
1 parent bad1132 commit 58c66f1
Show file tree
Hide file tree
Showing 11 changed files with 298 additions and 62 deletions.
38 changes: 38 additions & 0 deletions src/NATS.Client/ConnectionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,44 @@ public IConnection CreateConnection(string url, string jwt, string privateNkey,
opts.SetUserCredentials(jwt, privateNkey);
return CreateConnection(opts, reconnectOnConnect);
}

/// <summary>
/// Attempt to connect to the NATS server referenced by <paramref name="url"/>
/// with NATS 2.0 the user jwt and nkey seed credentials provided directly in the string.
/// </summary>
/// <param name="url"></param>
/// <param name="credentialsText">The text containing the "-----BEGIN NATS USER JWT-----" block
/// and the text containing the "-----BEGIN USER NKEY SEED-----" block</param>
/// <param name="reconnectOnConnect"></param>
/// <returns></returns>
public IConnection CreateConnectionWithCredentials(string url, string credentialsText, bool reconnectOnConnect = false)
{
Options opts = GetDefaultOptions(url);
opts.SetUserCredentialsFromString(credentialsText);
return CreateConnection(opts, reconnectOnConnect);
}

/// <summary>
/// Attempt to connect to the NATS server referenced by <paramref name="url"/>
/// with NATS 2.0 the user jwt and nkey seed credentials provided directly via strings.
/// </summary>
/// <remarks>
/// <para><paramref name="url"/>
/// Comma seperated arrays are also supported, e.g. <c>&quot;urlA, urlB&quot;</c>.</para>
/// </remarks>
/// <param name="url">A string containing the URL (or URLs) to the NATS Server. See the Remarks
/// section for more information.</param>
/// <param name="userJwtText">The text containing the "-----BEGIN NATS USER JWT-----" block</param>
/// <param name="nkeySeedText">The text containing the "-----BEGIN USER NKEY SEED-----" block or the seed begining with "SU".
/// May be the same as the jwt string if they are chained.</param>
/// <param name="reconnectOnConnect"></param>
/// <returns></returns>
public IConnection CreateConnectionWithCredentials(string url, string userJwtText, string nkeySeedText, bool reconnectOnConnect = false)
{
Options opts = GetDefaultOptions(url);
opts.SetUserCredentialsFromStrings(userJwtText, nkeySeedText);
return CreateConnection(opts, reconnectOnConnect);
}

/// <summary>
/// Retrieves the default set of client options.
Expand Down
69 changes: 16 additions & 53 deletions src/NATS.Client/DefaultUserJWTHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// limitations under the License.

using System.IO;
using System.Security;

namespace NATS.Client
{
Expand Down Expand Up @@ -56,33 +57,19 @@ public DefaultUserJWTHandler(string jwtFilePath, string credsFilePath)
/// <returns>The encoded JWT</returns>
public static string LoadUserFromFile(string path)
{
string text = null;
string line = null;
StringReader reader = null;
try
string text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text))
{
text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text)) throw new NATSException("Credentials file is empty");

reader = new StringReader(text);
for (line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN NATS USER JWT-----"))
{
return reader.ReadLine();
}
Nkeys.Wipe(line);
}
throw new NATSException("Credentials file does not contain a JWT");
throw new NATSException("Credentials file is empty");
}
finally
string user = JWTHandlerUtils.LoadUser(text);
if (user == null)
{
Nkeys.Wipe(text);
Nkeys.Wipe(line);
reader?.Dispose();
throw new NATSException("Credentials file does not contain a JWT");
}
return user;
}

/// <summary>
/// Generates a NATS Ed25519 keypair, used to sign server nonces, from a
/// private credentials file.
Expand All @@ -91,47 +78,23 @@ public static string LoadUserFromFile(string path)
/// <returns>A NATS Ed25519 KeyPair</returns>
public static NkeyPair LoadNkeyPairFromSeedFile(string path)
{
NkeyPair kp = null;
string text = null;
string line = null;
string seed = null;
StringReader reader = null;

try
{
text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text)) throw new NATSException("Credentials file is empty");

// if it's a nk file, it only has the nkey
if (text.StartsWith("SU"))
string text = File.ReadAllText(path).Trim();
if (string.IsNullOrEmpty(text))
{
kp = Nkeys.FromSeed(text);
return kp;
throw new NATSException("Credentials file is empty");
}

// otherwise assume it's a creds file.
reader = new StringReader(text);
for (line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN USER NKEY SEED-----"))
{
seed = reader.ReadLine();
kp = Nkeys.FromSeed(seed);
Nkeys.Wipe(seed);
}
Nkeys.Wipe(line);
}

NkeyPair kp = JWTHandlerUtils.LoadNkeyPair(text);
if (kp == null)
{
throw new NATSException("Seed not found in credentials file.");
else
return kp;
}
return kp;
}
finally
{
Nkeys.Wipe(line);
Nkeys.Wipe(text);
Nkeys.Wipe(seed);
reader?.Dispose();
}
}
Expand Down
72 changes: 72 additions & 0 deletions src/NATS.Client/JWTHandlerUtils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright 2019-2024 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.

using System.IO;
using System.Security;

namespace NATS.Client
{
public class JWTHandlerUtils
{
public static string LoadUser(string text)
{
StringReader reader = null;
try
{
reader = new StringReader(text);
for (string line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN NATS USER JWT-----"))
{
return reader.ReadLine();
}
}

return null;
}
finally
{
reader?.Dispose();
}
}

public static NkeyPair LoadNkeyPair(string nkeySeed)
{
StringReader reader = null;
try
{
// if it's a nk file, it only has the nkey
if (nkeySeed.StartsWith("SU"))
{
return Nkeys.FromSeed(nkeySeed);
}

// otherwise assume it's a creds file.
reader = new StringReader(nkeySeed);
for (string line = reader.ReadLine(); line != null; line = reader.ReadLine())
{
if (line.Contains("-----BEGIN USER NKEY SEED-----"))
{
return Nkeys.FromSeed(reader.ReadLine());
}
}

return null;
}
finally
{
reader?.Dispose();
}
}
}
}
7 changes: 4 additions & 3 deletions src/NATS.Client/NKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,10 @@ public static void Wipe(ref byte[] src)
/// <param name="src">string to wipe</param>
public static void Wipe(string src)
{
// best effort to wipe.
if (src != null && src.Length > 0)
src.Remove(0);
// This code commented out b/c string.remove does not touch the original string.
// There is not way to really wipe the contents of a string.
// if (src != null && src.Length > 0)
// src.Remove(0);
}

internal static byte[] DecodeSeed(byte[] raw)
Expand Down
25 changes: 25 additions & 0 deletions src/NATS.Client/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,31 @@ public void SetUserCredentials(string credentialsPath, string privateKeyPath)
UserSignatureEventHandler = handler.DefaultUserSignatureHandler;
}

/// <summary>
/// Sets user credentials from text instead of a file using the NATS 2.0 security scheme.
/// </summary>
/// <param name="credentialsText">The text containing the "-----BEGIN NATS USER JWT-----" block
/// and the text containing the "-----BEGIN USER NKEY SEED-----" block</param>
public void SetUserCredentialsFromString(string credentialsText)
{
var handler = new StringUserJWTHandler(credentialsText, credentialsText);
UserJWTEventHandler = handler.DefaultUserJWTEventHandler;
UserSignatureEventHandler = handler.DefaultUserSignatureHandler;
}

/// <summary>
/// Sets user credentials from text instead of a file using the NATS 2.0 security scheme.
/// </summary>
/// <param name="userJwtText">The text containing the "-----BEGIN NATS USER JWT-----" block</param>
/// <param name="nkeySeedText">The text containing the "-----BEGIN USER NKEY SEED-----" block or the seed begining with "SU".
/// May be the same as the jwt string if they are chained.</param>
public void SetUserCredentialsFromStrings(string userJwtText, string nkeySeedText)
{
var handler = new StringUserJWTHandler(userJwtText, nkeySeedText);
UserJWTEventHandler = handler.DefaultUserJWTEventHandler;
UserSignatureEventHandler = handler.DefaultUserSignatureHandler;
}

/// <summary>
/// Sets user credentials using the NATS 2.0 security scheme.
/// </summary>
Expand Down
93 changes: 93 additions & 0 deletions src/NATS.Client/StringUserJWTHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2024 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.

using System.IO;
using System.Security;

namespace NATS.Client
{
/// <summary>
/// TODO
/// </summary>
public class StringUserJWTHandler
{
/// <summary>
/// Gets the JWT file.
/// </summary>
public string UserJwt { get; }

/// <summary>
/// Gets the credentials files.
/// </summary>
public string NkeySeed { get; }

/// <summary>
/// Creates a static user jwt handler.
/// </summary>
/// <param name="credentialsText">The text containing the "-----BEGIN NATS USER JWT-----" block
/// and the text containing the "-----BEGIN USER NKEY SEED-----" block</param>
public StringUserJWTHandler(string credentialsText) : this(credentialsText, credentialsText) {}

/// <summary>
/// Creates a static user jwt handler.
/// </summary>
/// <param name="userJwt">The text containing the "-----BEGIN NATS USER JWT-----" block</param>
/// <param name="nkeySeed">The text containing the "-----BEGIN USER NKEY SEED-----" block or the seed begining with "SU".
/// May be the same as the jwt string if they are chained.</param>
public StringUserJWTHandler(string userJwt, string nkeySeed)
{
UserJwt = JWTHandlerUtils.LoadUser(userJwt);
if (UserJwt == null)
{
throw new NATSException("Credentials do not contain a JWT");
}

if (JWTHandlerUtils.LoadNkeyPair(nkeySeed) == null)
{
throw new NATSException("Seed not found.");
}
NkeySeed = nkeySeed;
}

/// <summary>
/// The default User JWT Event Handler.
/// </summary>
/// <param name="sender">Usually the connection.</param>
/// <param name="args">Arguments</param>
public void DefaultUserJWTEventHandler(object sender, UserJWTEventArgs args)
{
args.JWT = UserJwt;
}

/// <summary>
/// Utility method to signs the UserSignatureEventArgs server nonce from
/// a private credentials file.
/// </summary>
/// <param name="args">Arguments</param>
public void SignNonce(UserSignatureEventArgs args)
{
// you have to load this every time b/c signing actually wipes data
args.SignedNonce = JWTHandlerUtils.LoadNkeyPair(NkeySeed).Sign(args.ServerNonce);
}

/// <summary>
/// The default User Signature event handler.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
public void DefaultUserSignatureHandler(object sender, UserSignatureEventArgs args)
{
SignNonce(args);
}
}
}
9 changes: 9 additions & 0 deletions src/Tests/IntegrationTests/IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@
<None Update="config\tls_first.conf">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\certs\test.creds.nkey-seed-block.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\certs\test.creds.nkey-seed-only.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
<None Update="config\certs\test.creds.user-block.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Loading

0 comments on commit 58c66f1

Please sign in to comment.