From 7262b2936c66bc46c8f5a59b557c321d71b5686e Mon Sep 17 00:00:00 2001 From: ibebbs Date: Thu, 4 Feb 2021 10:13:02 +0000 Subject: [PATCH] Allow StreamClient to connect using UserSessionToken. --- src/stream-net/StreamClient.cs | 61 ++++-------------- src/stream-net/StreamClientToken.cs | 97 +++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 49 deletions(-) create mode 100644 src/stream-net/StreamClientToken.cs diff --git a/src/stream-net/StreamClient.cs b/src/stream-net/StreamClient.cs index af1f17d..e30c754 100644 --- a/src/stream-net/StreamClient.cs +++ b/src/stream-net/StreamClient.cs @@ -29,31 +29,31 @@ public class StreamClient : IStreamClient readonly RestClient _client; readonly StreamClientOptions _options; - readonly string _apiSecret; + readonly IStreamClientToken _streamClientToken; readonly string _apiKey; - public StreamClient(string apiKey, string apiSecret, StreamClientOptions options = null) + public StreamClient(string apiKey, string apiSecretOrToken, StreamClientOptions options = null) { if (string.IsNullOrWhiteSpace(apiKey)) throw new ArgumentNullException("apiKey", "Must have an apiKey"); - if (string.IsNullOrWhiteSpace(apiSecret)) - throw new ArgumentNullException("apiSecret", "Must have an apiSecret"); + if (string.IsNullOrWhiteSpace(apiSecretOrToken)) + throw new ArgumentNullException("apiSecret", "Must have an apiSecret or user session token"); _apiKey = apiKey; - _apiSecret = apiSecret; + _streamClientToken = StreamClientToken.For(apiSecretOrToken); _options = options ?? StreamClientOptions.Default; _client = new RestClient(GetBaseUrl(_options.Location), TimeSpan.FromMilliseconds(_options.Timeout)); } - private StreamClient(string apiKey, string apiSecret, RestClient client, StreamClientOptions options = null) + private StreamClient(string apiKey, IStreamClientToken streamClientToken, RestClient client, StreamClientOptions options = null) { if (string.IsNullOrWhiteSpace(apiKey)) throw new ArgumentNullException("apiKey", "Must have an apiKey"); - if (string.IsNullOrWhiteSpace(apiSecret)) - throw new ArgumentNullException("apiSecret", "Must have an apiSecret"); + if (streamClientToken is null) + throw new ArgumentNullException("streamClientToken", "Must have a streamClientToken"); _apiKey = apiKey; - _apiSecret = apiSecret; + _streamClientToken = streamClientToken; _options = options ?? StreamClientOptions.Default; _client = client; } @@ -95,15 +95,7 @@ public async Task ActivityPartialUpdate(string id = null, ForeignIDTime foreignI public string CreateUserSessionToken(string userId, IDictionary extraData = null) { - var payload = new Dictionary - { - {"user_id", userId} - }; - if (extraData != null) - { - extraData.ForEach(x => payload[x.Key] = x.Value); - } - return this.JWToken(payload); + return _streamClientToken.CreateUserSessionToken(userId, extraData); } /// @@ -146,7 +138,7 @@ public Personalization Personalization get { var _personalization = new RestClient(GetBasePersonalizationUrl(_options.PersonalizationLocation), TimeSpan.FromMilliseconds(_options.PersonalizationTimeout)); - return new Personalization(new StreamClient(_apiKey, _apiSecret, _personalization, _options)); + return new Personalization(new StreamClient(_apiKey, _streamClientToken, _personalization, _options)); } } @@ -217,14 +209,6 @@ internal Task MakeRequest(RestRequest request) return _client.Execute(request); } - private static string Base64UrlEncode(byte[] input) - { - return Convert.ToBase64String(input) - .Replace('+', '-') - .Replace('/', '_') - .Trim('='); - } - internal string JWToken(string feedId, string userID = null) { var payload = new Dictionary() @@ -237,28 +221,7 @@ internal string JWToken(string feedId, string userID = null) { payload["user_id"] = userID; } - return this.JWToken(payload); - } - - internal string JWToken(object payload) - { - var segments = new List(); - - byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(StreamClient.JWTHeader)); - byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload)); - - segments.Add(Base64UrlEncode(headerBytes)); - segments.Add(Base64UrlEncode(payloadBytes)); - - var stringToSign = string.Join(".", segments.ToArray()); - var bytesToSign = Encoding.UTF8.GetBytes(stringToSign); - - using (var sha = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret))) - { - byte[] signature = sha.ComputeHash(bytesToSign); - segments.Add(Base64UrlEncode(signature)); - } - return string.Join(".", segments.ToArray()); + return _streamClientToken.For(payload); } } } diff --git a/src/stream-net/StreamClientToken.cs b/src/stream-net/StreamClientToken.cs new file mode 100644 index 0000000..8da4d26 --- /dev/null +++ b/src/stream-net/StreamClientToken.cs @@ -0,0 +1,97 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; + +namespace Stream +{ + public interface IStreamClientToken + { + string CreateUserSessionToken(string userId, IDictionary extraData = null); + + string For(object payload); + } + + public static class StreamClientToken + { + public static IStreamClientToken For(string apiSecretOrToken) + { + return apiSecretOrToken.Contains(".") + ? (IStreamClientToken) new StreamApiSessionToken(apiSecretOrToken) + : (IStreamClientToken) new StreamApiSecret(apiSecretOrToken); + } + } + + public class StreamApiSessionToken : IStreamClientToken + { + private readonly string _sessionToken; + + public StreamApiSessionToken(string sessionToken) + { + _sessionToken = sessionToken; + } + + public string CreateUserSessionToken(string userId, IDictionary extraData = null) + { + throw new InvalidOperationException("Clients connecting using a user session token cannot create additional user session tokens"); + } + + public string For(object payload) + { + return _sessionToken; + } + } + + public class StreamApiSecret : IStreamClientToken + { + private readonly string _apiSecret; + + public StreamApiSecret(string apiSecret) + { + _apiSecret = apiSecret; + } + + private static string Base64UrlEncode(byte[] input) + { + return Convert.ToBase64String(input) + .Replace('+', '-') + .Replace('/', '_') + .Trim('='); + } + + public string CreateUserSessionToken(string userId, IDictionary extraData = null) + { + var payload = new Dictionary + { + {"user_id", userId} + }; + if (extraData != null) + { + extraData.ForEach(x => payload[x.Key] = x.Value); + } + return For(payload); + } + + public string For(object payload) + { + var segments = new List(); + + byte[] headerBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(StreamClient.JWTHeader)); + byte[] payloadBytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(payload)); + + segments.Add(Base64UrlEncode(headerBytes)); + segments.Add(Base64UrlEncode(payloadBytes)); + + var stringToSign = string.Join(".", segments.ToArray()); + var bytesToSign = Encoding.UTF8.GetBytes(stringToSign); + + using (var sha = new HMACSHA256(Encoding.UTF8.GetBytes(_apiSecret))) + { + byte[] signature = sha.ComputeHash(bytesToSign); + segments.Add(Base64UrlEncode(signature)); + } + return string.Join(".", segments.ToArray()); + } + } +}