Skip to content

Commit e275779

Browse files
2 parents e99e38e + 488cbb9 commit e275779

File tree

9 files changed

+434
-7
lines changed

9 files changed

+434
-7
lines changed

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,11 @@ endif
205205
test-http-service:
206206
@echo "No tests for http service."
207207

208+
# Run unit tests
209+
test-unit:
210+
@echo "Running unit tests..."
211+
@dotnet test ${TEST_LOGGER_OPTIONS} -f ${DOTNET_VERSION} --filter "FullyQualifiedName~Momento.Sdk.Tests.Unit"
212+
208213

209214
## Run example applications and snippets
210215
run-examples:
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
namespace Momento.Sdk.Auth;
2+
3+
using Momento.Sdk.Exceptions;
4+
using System;
5+
6+
/// <summary>
7+
/// Reads a v2 api key and Momento service endpoint stored as strings.
8+
/// </summary>
9+
public class ApiKeyV2TokenProvider : ICredentialProvider
10+
{
11+
// For v2 api keys, the original endpoint is necessary to reconstruct
12+
// the provider in WithCacheEndpoint.
13+
private readonly string origEndpoint;
14+
/// <inheritdoc />
15+
public string AuthToken { get; private set; }
16+
/// <inheritdoc />
17+
public string ControlEndpoint { get; private set; }
18+
/// <inheritdoc />
19+
public string CacheEndpoint { get; private set; }
20+
/// <inheritdoc />
21+
public string TokenEndpoint { get; private set; }
22+
/// <inheritdoc />
23+
public bool SecureEndpoints { get; private set; } = true;
24+
25+
/// <summary>
26+
/// Constructs a ApiKeyV2TokenProvider from a v2 api key and endpoint.
27+
/// </summary>
28+
/// <param name="token">The v2 api key.</param>
29+
/// <param name="endpoint">The Momento service endpoint.</param>
30+
public ApiKeyV2TokenProvider(string token, string endpoint)
31+
{
32+
if (String.IsNullOrEmpty(endpoint))
33+
{
34+
throw new InvalidArgumentException($"Endpoint is empty or null.");
35+
}
36+
if (String.IsNullOrEmpty(token))
37+
{
38+
throw new InvalidArgumentException($"Auth token is empty or null.");
39+
}
40+
if (!AuthUtils.IsV2ApiKey(token))
41+
{
42+
throw new InvalidArgumentException("Received an invalid v2 API key. Are you using the correct key? Or did you mean to use `StringMomentoTokenProvider()` with a legacy key instead?");
43+
}
44+
45+
AuthToken = token;
46+
ControlEndpoint = "control." + endpoint;
47+
CacheEndpoint = "cache." + endpoint;
48+
TokenEndpoint = "token." + endpoint;
49+
origEndpoint = endpoint;
50+
}
51+
52+
/// <inheritdoc />
53+
public ICredentialProvider WithCacheEndpoint(string cacheEndpoint)
54+
{
55+
var updated = new ApiKeyV2TokenProvider(this.AuthToken, this.origEndpoint);
56+
updated.CacheEndpoint = cacheEndpoint;
57+
return updated;
58+
}
59+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace Momento.Sdk.Auth;
2+
3+
using Momento.Sdk.Exceptions;
4+
using System;
5+
6+
/// <summary>
7+
/// Reads and parses a JWT token stored as a string.
8+
/// </summary>
9+
public class DisposableTokenProvider : ICredentialProvider
10+
{
11+
// For V1 tokens, the original token is necessary to reconstruct
12+
// the provider in WithCacheEndpoint.
13+
private readonly string origAuthToken;
14+
/// <inheritdoc />
15+
public string AuthToken { get; private set; }
16+
/// <inheritdoc />
17+
public string ControlEndpoint { get; private set; }
18+
/// <inheritdoc />
19+
public string CacheEndpoint { get; private set; }
20+
/// <inheritdoc />
21+
public string TokenEndpoint { get; private set; }
22+
/// <inheritdoc />
23+
public bool SecureEndpoints { get; private set; } = true;
24+
25+
/// <summary>
26+
/// Reads and parses a JWT token from a string.
27+
/// </summary>
28+
/// <param name="token">The JWT token.</param>
29+
public DisposableTokenProvider(string token)
30+
{
31+
origAuthToken = token;
32+
AuthToken = token;
33+
if (String.IsNullOrEmpty(AuthToken))
34+
{
35+
throw new InvalidArgumentException($"String '{token}' is empty or null.");
36+
}
37+
38+
var tokenData = AuthUtils.TryDecodeAuthToken(AuthToken);
39+
ControlEndpoint = tokenData.ControlEndpoint;
40+
CacheEndpoint = tokenData.CacheEndpoint;
41+
TokenEndpoint = tokenData.TokenEndpoint;
42+
AuthToken = tokenData.AuthToken;
43+
}
44+
45+
/// <inheritdoc />
46+
public ICredentialProvider WithCacheEndpoint(string cacheEndpoint)
47+
{
48+
var updated = new DisposableTokenProvider(this.origAuthToken);
49+
updated.CacheEndpoint = cacheEndpoint;
50+
return updated;
51+
}
52+
}

src/Momento.Sdk/Auth/EnvMomentoTokenProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ namespace Momento.Sdk.Auth;
77
/// <summary>
88
/// Reads and parses a JWT token stored as an environment variable.
99
/// </summary>
10+
[Obsolete("EnvMomentoTokenProvider is deprecated, please use EnvMomentoV2TokenProvider instead.")]
1011
public class EnvMomentoTokenProvider : ICredentialProvider
1112
{
1213
private readonly string envVarName;
@@ -26,6 +27,7 @@ public class EnvMomentoTokenProvider : ICredentialProvider
2627
/// Reads and parses a JWT token stored as an environment variable.
2728
/// </summary>
2829
/// <param name="name">Name of the environment variable that contains the JWT token.</param>
30+
[Obsolete("EnvMomentoTokenProvider is deprecated, please use EnvMomentoV2TokenProvider instead.")]
2931
public EnvMomentoTokenProvider(string name)
3032
{
3133
this.envVarName = name;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
namespace Momento.Sdk.Auth;
2+
3+
using Momento.Sdk.Exceptions;
4+
using Momento.Sdk.Internal;
5+
using System;
6+
7+
/// <summary>
8+
/// Reads v2 api key and Momento service endpoint stored as environment variables.
9+
/// </summary>
10+
public class EnvMomentoV2TokenProvider : ICredentialProvider
11+
{
12+
// The original environment variable names are required to reconstruct
13+
// the provider when using WithCacheEndpoint.
14+
private readonly string endpointEnvVarName;
15+
private readonly string apiKeyEnvVarName;
16+
17+
/// <inheritdoc />
18+
public string AuthToken { get; private set; }
19+
/// <inheritdoc />
20+
public string ControlEndpoint { get; private set; }
21+
/// <inheritdoc />
22+
public string CacheEndpoint { get; private set; }
23+
/// <inheritdoc />
24+
public string TokenEndpoint { get; private set; }
25+
/// <inheritdoc />
26+
public bool SecureEndpoints { get; private set; } = true;
27+
28+
/// <summary>
29+
/// Constructs a EnvMomentoV2TokenProvider from a Momento service endpoint and a v2 api key stored as environment variables.
30+
/// </summary>
31+
/// <param name="apiKeyEnvVar">Name of the environment variable that contains the v2 api key.</param>
32+
/// <param name="endpointEnvVar">Name of the environment variable that contains the Momento service endpoint.</param>
33+
public EnvMomentoV2TokenProvider(string apiKeyEnvVar = "MOMENTO_API_KEY", string endpointEnvVar = "MOMENTO_ENDPOINT")
34+
{
35+
if (String.IsNullOrEmpty(endpointEnvVar))
36+
{
37+
throw new InvalidArgumentException($"Endpoint environment variable name is empty or null.");
38+
}
39+
var endpoint = Environment.GetEnvironmentVariable(endpointEnvVar);
40+
if (String.IsNullOrEmpty(endpoint))
41+
{
42+
throw new InvalidArgumentException($"Endpoint is empty or null.");
43+
}
44+
this.endpointEnvVarName = endpointEnvVar;
45+
46+
if (String.IsNullOrEmpty(apiKeyEnvVar))
47+
{
48+
throw new InvalidArgumentException($"API key environment variable name is empty or null.");
49+
}
50+
var apiKey = Environment.GetEnvironmentVariable(apiKeyEnvVar);
51+
if (String.IsNullOrEmpty(apiKey))
52+
{
53+
throw new InvalidArgumentException($"Environment variable '{apiKeyEnvVar}' is empty or null.");
54+
}
55+
this.apiKeyEnvVarName = apiKeyEnvVar;
56+
57+
AuthToken = apiKey;
58+
if (!AuthUtils.IsV2ApiKey(AuthToken))
59+
{
60+
throw new InvalidArgumentException("Received an invalid v2 API key. Are you using the correct key? Or did you mean to use `StringMomentoTokenProvider()` with a legacy key instead?");
61+
}
62+
63+
ControlEndpoint = "control." + endpoint;
64+
CacheEndpoint = "cache." + endpoint;
65+
TokenEndpoint = "token." + endpoint;
66+
}
67+
68+
/// <inheritdoc />
69+
public ICredentialProvider WithCacheEndpoint(string cacheEndpoint)
70+
{
71+
var updated = new EnvMomentoV2TokenProvider(this.apiKeyEnvVarName, this.endpointEnvVarName);
72+
updated.CacheEndpoint = cacheEndpoint;
73+
return updated;
74+
}
75+
}

src/Momento.Sdk/Auth/StringMomentoTokenProvider.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ namespace Momento.Sdk.Auth;
66
/// <summary>
77
/// Reads and parses a JWT token stored as a string.
88
/// </summary>
9+
[Obsolete("StringMomentoTokenProvider is deprecated, please use ApiKeyV2TokenProvider or DisposableTokenProvider instead.")]
910
public class StringMomentoTokenProvider : ICredentialProvider
1011
{
1112
// For V1 tokens, the original token is necessary to reconstruct
@@ -26,6 +27,7 @@ public class StringMomentoTokenProvider : ICredentialProvider
2627
/// Reads and parses a JWT token from a string.
2728
/// </summary>
2829
/// <param name="token">The JWT token.</param>
30+
[Obsolete("StringMomentoTokenProvider is deprecated, please use ApiKeyV2TokenProvider or DisposableTokenProvider instead.")]
2931
public StringMomentoTokenProvider(string token)
3032
{
3133
origAuthToken = token;

src/Momento.Sdk/Auth/Utils.cs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,11 @@ public static TokenAndEndpoints TryDecodeAuthToken(string authToken)
6464
}
6565
else
6666
{
67-
var claims = JwtUtils.DecodeJwt(authToken);
67+
if (IsV2ApiKey(authToken))
68+
{
69+
throw new InvalidArgumentException("Received a v2 API key. Are you using the correct key? Or did you mean to use `ApiKeyV2TokenProvider()` or `EnvMomentoV2TokenProvider()` instead?");
70+
}
71+
var claims = JwtUtils.DecodeLegacyJwt(authToken);
6872
return new TokenAndEndpoints(
6973
authToken,
7074
claims.ControlEndpoint,
@@ -78,4 +82,22 @@ public static TokenAndEndpoints TryDecodeAuthToken(string authToken)
7882
throw new InvalidArgumentException("The supplied Momento authToken is not valid.");
7983
}
8084
}
85+
86+
public static bool IsV2ApiKey(string authToken)
87+
{
88+
if (AuthUtils.IsBase64String(authToken))
89+
{
90+
return false;
91+
}
92+
try
93+
{
94+
var claims = JwtUtils.DecodeV2Jwt(authToken);
95+
return claims.Type != null && claims.Type == "g";
96+
}
97+
catch
98+
{
99+
return false;
100+
}
101+
102+
}
81103
}

src/Momento.Sdk/Internal/JwtUtils.cs

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ public class JwtUtils
1313
{
1414
/// <summary>
1515
/// extracts the controlEndpoint and cacheEndpoint
16-
/// from the jwt
16+
/// from the legacy jwt
1717
/// </summary>
1818
/// <param name="jwt"></param>
1919
/// <returns></returns>
20-
public static Claims DecodeJwt(string jwt)
20+
public static LegacyClaims DecodeLegacyJwt(string jwt)
2121
{
2222
IJsonSerializer serializer = new JsonNetSerializer();
2323

@@ -26,7 +26,30 @@ public static Claims DecodeJwt(string jwt)
2626
try
2727
{
2828
var decodedJwt = decoder.Decode(jwt);
29-
return JsonConvert.DeserializeObject<Claims>(decodedJwt);
29+
return JsonConvert.DeserializeObject<LegacyClaims>(decodedJwt);
30+
}
31+
catch (Exception)
32+
{
33+
throw new InvalidArgumentException("invalid jwt passed");
34+
}
35+
}
36+
37+
/// <summary>
38+
/// extracts the type, id, and optional expiration
39+
/// from the v2 jwt
40+
/// </summary>
41+
/// <param name="jwt"></param>
42+
/// <returns></returns>
43+
public static V2Claims DecodeV2Jwt(string jwt)
44+
{
45+
IJsonSerializer serializer = new JsonNetSerializer();
46+
47+
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
48+
JwtDecoder decoder = new JwtDecoder(serializer, urlEncoder);
49+
try
50+
{
51+
var decodedJwt = decoder.Decode(jwt);
52+
return JsonConvert.DeserializeObject<V2Claims>(decodedJwt);
3053
}
3154
catch (Exception)
3255
{
@@ -36,10 +59,10 @@ public static Claims DecodeJwt(string jwt)
3659
}
3760

3861
/// <summary>
39-
/// Encapsulates claims embedded in a JWT token that specify host endpoints
62+
/// Encapsulates claims embedded in a legacy JWT token that specify host endpoints
4063
/// for the control plane and the data plane.
4164
/// </summary>
42-
public class Claims
65+
public class LegacyClaims
4366
{
4467

4568
/// <summary>
@@ -60,9 +83,32 @@ public class Claims
6083
/// </summary>
6184
/// <param name="cacheEndpoint">Data plane endpoint</param>
6285
/// <param name="controlEndpoint">Control plane endpoint</param>
63-
public Claims(string cacheEndpoint, string controlEndpoint)
86+
public LegacyClaims(string cacheEndpoint, string controlEndpoint)
6487
{
6588
this.CacheEndpoint = cacheEndpoint;
6689
this.ControlEndpoint = controlEndpoint;
6790
}
6891
}
92+
93+
/// <summary>
94+
/// Encapsulates claims embedded in a v2 JWT token.
95+
/// </summary>
96+
public class V2Claims
97+
{
98+
99+
/// <summary>
100+
/// Type of api key. "g" for global (v2) api keys.
101+
/// </summary>
102+
[JsonProperty(PropertyName = "t", Required = Required.Always)]
103+
public string Type { get; private set; }
104+
105+
/// <summary>
106+
/// Encapsulates claims embedded in a v2 JWT token that specify type of key,
107+
/// key ID, and optional expiration.
108+
/// </summary>
109+
/// <param name="type">Type of api key</param>
110+
public V2Claims(string type)
111+
{
112+
this.Type = type;
113+
}
114+
}

0 commit comments

Comments
 (0)