From 69d82ef01a4228616c2538ad4e77c6afddd966f1 Mon Sep 17 00:00:00 2001 From: Ryan Rowland Date: Tue, 10 Nov 2020 14:07:29 -0600 Subject: [PATCH] feat: Add region to access token (#546) --- src/Twilio/JWT/AccessToken/Token.cs | 26 +++++++- .../Jwt/AccessToken/AccessTokenTest.cs | 49 ++++++++++++--- test/Twilio.Test/Jwt/DecodedJwt.cs | 62 +++++++++++++------ 3 files changed, 105 insertions(+), 32 deletions(-) diff --git a/src/Twilio/JWT/AccessToken/Token.cs b/src/Twilio/JWT/AccessToken/Token.cs index bd3ca81dd..0786ed39b 100644 --- a/src/Twilio/JWT/AccessToken/Token.cs +++ b/src/Twilio/JWT/AccessToken/Token.cs @@ -13,6 +13,7 @@ public class Token : BaseJwt private readonly string _identity; private readonly DateTime? _nbf; private readonly HashSet _grants; + private readonly string _region; /// /// Create a new Access Token @@ -24,6 +25,7 @@ public class Token : BaseJwt /// Token expiration /// Token nbf /// Token grants + /// Token region public Token( string accountSid, string signingKeySid, @@ -31,7 +33,8 @@ public Token( string identity = null, DateTime? expiration = null, DateTime? nbf = null, - HashSet grants = null + HashSet grants = null, + string region = null ) : base(secret, signingKeySid, expiration.HasValue ? expiration.Value : DateTime.UtcNow.AddSeconds(3600)) { var now = BaseJwt.ConvertToUnixTimestamp(DateTime.UtcNow); @@ -40,6 +43,7 @@ public Token( this._identity = identity; this._nbf = nbf; this._grants = grants; + this._region = region; } /// @@ -75,6 +79,17 @@ public override DateTime? Nbf } } + /// + /// The region associated with this account + /// + public string Region + { + get + { + return _region; + } + } + /// /// Headers for an Access Token /// @@ -82,7 +97,14 @@ public override Dictionary Headers { get { - return new Dictionary { { "cty", "twilio-fpa;v=1" } }; + var headers = new Dictionary { { "cty", "twilio-fpa;v=1" } }; + + if (_region != null) + { + headers.Add("twr", _region); + } + + return headers; } } diff --git a/test/Twilio.Test/Jwt/AccessToken/AccessTokenTest.cs b/test/Twilio.Test/Jwt/AccessToken/AccessTokenTest.cs index 503dd1c05..33af9de61 100644 --- a/test/Twilio.Test/Jwt/AccessToken/AccessTokenTest.cs +++ b/test/Twilio.Test/Jwt/AccessToken/AccessTokenTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using NUnit.Framework; using Twilio.Jwt; @@ -16,16 +17,9 @@ public TestToken( string identity = null, DateTime? expiration = null, DateTime? nbf = null, - HashSet grants = null - ) : base(accountSid, signingKeySid, secret, identity, expiration, nbf, grants) {} - - public override Dictionary Headers - { - get - { - return new Dictionary(); - } - } + HashSet grants = null, + string region = null + ) : base(accountSid, signingKeySid, secret, identity, expiration, nbf, grants, region) {} } [TestFixture] @@ -54,6 +48,41 @@ public void TestBuildToken() Assert.AreEqual("{}", payload["grants"].ToString()); } + [Test] + public void TestHaveRegion() + { + var now = DateTime.UtcNow; + var token = new TestToken("AC456", "SK123", Secret, region: "foo").ToJwt(); + Assert.IsNotNull(token); + Assert.IsNotEmpty(token); + + var decoded = new DecodedJwt(token, Secret); + var header = decoded.Header; + Assert.IsNotNull(header); + Assert.AreEqual("twilio-fpa;v=1", header["cty"]); + Assert.AreEqual("foo", header["twr"]); + } + + [Test] + public void TestEmptyRegion() + { + var now = DateTime.UtcNow; + var token = new TestToken("AC456", "SK123", Secret).ToJwt(); + Assert.IsNotNull(token); + Assert.IsNotEmpty(token); + + var decoded = new DecodedJwt(token, Secret); + var header = decoded.Header; + Assert.IsNotNull(header); + Assert.AreEqual("twilio-fpa;v=1", header["cty"]); + + try { + var twr = header["twr"]; + Assert.Fail(); + } catch (KeyNotFoundException) { + // Pass + } + } [Test] public void TestHaveNbf() diff --git a/test/Twilio.Test/Jwt/DecodedJwt.cs b/test/Twilio.Test/Jwt/DecodedJwt.cs index 7521e48d3..73e0f4fd9 100644 --- a/test/Twilio.Test/Jwt/DecodedJwt.cs +++ b/test/Twilio.Test/Jwt/DecodedJwt.cs @@ -1,50 +1,72 @@ -#if !NET35 -using System.IdentityModel.Tokens.Jwt; -#else +using System; using System.Collections.Generic; -#endif - -using System; +using System.Security.Cryptography; +using System.Text; using Newtonsoft.Json; namespace Twilio.Tests.Jwt { class DecodedJwt { -#if !NET35 + private static byte[] Base64UrlDecode(string input) => Convert.FromBase64String(UrlDecode(input)); - public JwtPayload Payload + private static string UrlDecode(string input) { - get + var output = input; + output = output.Replace('-', '+'); // 62nd char of encoding + output = output.Replace('_', '/'); // 63rd char of encoding + + // Pad with trailing '='s + switch (output.Length % 4) { - return token.Payload; + case 0: + break; // No pad chars in this case + case 2: + output += "=="; + break; // Two pad chars + case 3: + output += "="; + break; // One pad char + default: + throw new Exception($"Illegal base-64 string: '{input}'."); } + + return output; } - private readonly JwtSecurityToken token; + private static IDictionary DecodeJwtPart(string part) + { + var bytes = Base64UrlDecode(part); + var json = Encoding.UTF8.GetString(bytes); + var data = JsonConvert.DeserializeObject>(json); - public DecodedJwt(string jwt, string secret) + return data; + } + + public IDictionary Header { - token = new JwtSecurityToken(jwt); + get + { + return _header; + } } -#else public IDictionary Payload { get { - return JsonConvert.DeserializeObject>(token); + return _payload; } } - private readonly string token; + private readonly IDictionary _header; + private readonly IDictionary _payload; public DecodedJwt(string jwt, string secret) { - token = JWT.JsonWebToken.Decode(jwt, secret); + var parts = jwt.Split('.'); + _header = DecodeJwtPart(parts[0]); + _payload = DecodeJwtPart(parts[1]); } - -#endif - } }