Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Public OAuth uptake for C# libraries #762

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/Twilio/Annotations/Deprecated.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System;

namespace Twilio.Annotations
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class Deprecated : Attribute
{
public string Message { get; }

public Deprecated(string message = "This feature is deprecated")
{
Message = message;
}
}
}
11 changes: 11 additions & 0 deletions src/Twilio/AuthStrategies/AuthStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Twilio.AuthStrategies
{
public abstract class AuthStrategy
{
protected AuthStrategy(){}

public abstract string GetAuthString();

public abstract bool RequiresAuthentication();
}
}
32 changes: 32 additions & 0 deletions src/Twilio/AuthStrategies/Base64UrlEncode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#if NET35
using System;
using System.Collections.Generic;
using System.Text;
using System.Web.Script.Serialization;
using Twilio.Annotations;

namespace Twilio.AuthStrategies{

[Beta]
public abstract class Base64UrlEncode
{
public static string Decode(string base64Url)
{
// Replace URL-safe characters with Base64 characters
string base64 = base64Url
.Replace('-', '+')
.Replace('_', '/');

// Add padding if necessary
switch (base64.Length % 4)
{
case 2: base64 += "=="; break;
case 3: base64 += "="; break;
}

byte[] bytes = Convert.FromBase64String(base64);
return Encoding.UTF8.GetString(bytes);
}
}
}
#endif
44 changes: 44 additions & 0 deletions src/Twilio/AuthStrategies/BasicAuthStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using System;
using System.Text;

namespace Twilio.AuthStrategies
{
public class BasicAuthStrategy : AuthStrategy
{
private string username;
private string password;

public BasicAuthStrategy(string username, string password)
{
this.username = username;
this.password = password;
}

public override string GetAuthString()
{
var credentials = username + ":" + password;
var encoded = System.Text.Encoding.UTF8.GetBytes(credentials);
var finalEncoded = Convert.ToBase64String(encoded);
return $"Basic {finalEncoded}";
}

public override bool RequiresAuthentication()
{
return true;
}

public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj)) return true;
if (obj == null || GetType() != obj.GetType()) return false;
var that = (BasicAuthStrategy)obj;
return username == that.username && password == that.password;
}

public override int GetHashCode()
{
return HashCode.Combine(username, password);

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 40 in src/Twilio/AuthStrategies/BasicAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context
}

}
}
17 changes: 17 additions & 0 deletions src/Twilio/AuthStrategies/NoAuthStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Twilio.AuthStrategies
{
public class NoAuthStrategy : AuthStrategy
{
public NoAuthStrategy(){}

public override string GetAuthString()
{
return string.Empty;
}

public override bool RequiresAuthentication()
{
return false;
}
}
}
152 changes: 152 additions & 0 deletions src/Twilio/AuthStrategies/TokenAuthStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
using System;
using System.Threading;
using Twilio.Http.BearerToken;
using Twilio.Exceptions;

#if !NET35
using System.IdentityModel.Tokens.Jwt;
using System.Threading.Tasks;
#endif

#if NET35
using Twilio.Http.Net35;
using System.Collections.Generic;
using System.Text;
using System.Web.Script.Serialization;
#endif

namespace Twilio.AuthStrategies
{
public class TokenAuthStrategy : AuthStrategy
{
private string token;
private TokenManager tokenManager;


public TokenAuthStrategy(TokenManager tokenManager)
{
this.tokenManager = tokenManager ?? throw new ArgumentNullException(nameof(tokenManager));
}

public override string GetAuthString()
{
FetchToken();
return $"Bearer {token}";
}

public override bool RequiresAuthentication()
{
return true;
}

// Token-specific refresh logic
private void FetchToken()
{
if (string.IsNullOrEmpty(token) || tokenExpired(token))
{
lock (typeof(TokenAuthStrategy))
{
if (string.IsNullOrEmpty(token) || tokenExpired(token))
{
token = tokenManager.fetchAccessToken();
}
}
}
}

public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj)) return true;
if (obj == null || GetType() != obj.GetType()) return false;
var that = (TokenAuthStrategy)obj;
return token == that.token && tokenManager.Equals(that.tokenManager);
}

public override int GetHashCode()
{
return HashCode.Combine(token, tokenManager);

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context

Check failure on line 67 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

The name 'HashCode' does not exist in the current context
}


public bool tokenExpired(String accessToken){
#if NET35
return IsTokenExpired(accessToken);
#else
return isTokenExpired(accessToken);
#endif
Comment on lines +72 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have any function that works in both old and new framework? instead of making two different functions, if we can use single one to do it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we cannot use single function to do it, whatever works in 3.5 is deprecated in later versions and vice versa

}

#if NET35
public static bool IsTokenExpired(string token)
{
try
{
// Split the token into its components
var parts = token.Split('.');
if (parts.Length != 3)
throw new ArgumentException("Malformed token received");

// Decode the payload (the second part of the JWT)
string payload = Base64UrlEncode.Decode(parts[1]);

// Parse the payload JSON
var serializer = new JavaScriptSerializer();
var payloadData = serializer.Deserialize<Dictionary<string, object>>(payload);

// Check the 'exp' claim
if (payloadData.TryGetValue("exp", out object expObj))
{
if (long.TryParse(expObj.ToString(), out long exp))
{
DateTime expirationDate = UnixTimeStampToDateTime(exp);
return DateTime.UtcNow > expirationDate;
}
}

// If 'exp' claim is missing or not a valid timestamp, consider the token expired
throw new ApiConnectionException("token expired");
return true;

Check warning on line 108 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

Unreachable code detected
}
catch (Exception ex)
{
// Handle exceptions (e.g., malformed token or invalid JSON)
Console.WriteLine($"Error checking token expiration: {ex.Message}");
throw new ApiConnectionException("token expired");
return true; // Consider as expired if there's an error

Check warning on line 115 in src/Twilio/AuthStrategies/TokenAuthStrategy.cs

View workflow job for this annotation

GitHub Actions / Test

Unreachable code detected
}
}

private static DateTime UnixTimeStampToDateTime(long unixTimeStamp)
{
// Unix timestamp is seconds past epoch
var epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
return epoch.AddSeconds(unixTimeStamp);
}
#endif

#if !NET35
public bool isTokenExpired(string token){
var handler = new JwtSecurityTokenHandler();
try{
var jwtToken = handler.ReadJwtToken(token);
var exp = jwtToken.Payload.Exp;
if (exp.HasValue)
{
var expirationDate = DateTimeOffset.FromUnixTimeSeconds(exp.Value).UtcDateTime;
return DateTime.UtcNow > expirationDate;
}
else
{
return true; // Assuming token is expired if exp claim is missing
}
}
catch (Exception ex)
{
Console.WriteLine($"Error reading token: {ex.Message}");

return true; // Treat as expired if there is an error
}
}
#endif
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#endif

using Twilio.Http;
using Twilio.Http.BearerToken;

Check warning on line 17 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

The using directive for 'Twilio.Http.BearerToken' appeared previously in this namespace

Check warning on line 17 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

The using directive for 'Twilio.Http.BearerToken' appeared previously in this namespace

Check warning on line 17 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

The using directive for 'Twilio.Http.BearerToken' appeared previously in this namespace
#if NET35
using Twilio.Http.Net35;
using System.Collections.Generic;
Expand All @@ -28,7 +28,7 @@
/// <summary>
/// Implementation of a TwilioRestClient.
/// </summary>
[Beta]
[Deprecated]
public class TwilioOrgsTokenRestClient
{
/// <summary>
Expand Down Expand Up @@ -185,14 +185,14 @@

// If 'exp' claim is missing or not a valid timestamp, consider the token expired
throw new ApiConnectionException("token expired 1");
return true;

Check warning on line 188 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

Unreachable code detected
}
catch (Exception ex)
{
// Handle exceptions (e.g., malformed token or invalid JSON)
Console.WriteLine($"Error checking token expiration: {ex.Message}");
throw new ApiConnectionException("token expired 2");
return true; // Consider as expired if there's an error

Check warning on line 195 in src/Twilio/Clients/BearerToken/TwilioOrgsTokenRestClient.cs

View workflow job for this annotation

GitHub Actions / Test

Unreachable code detected
}
}

Expand Down
22 changes: 18 additions & 4 deletions src/Twilio/Clients/TwilioRestClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Net;
using Newtonsoft.Json;
using Twilio.Exceptions;
using Twilio.AuthStrategies;

#if !NET35
using System.Threading.Tasks;
Expand Down Expand Up @@ -51,6 +52,7 @@ public class TwilioRestClient : ITwilioRestClient
public string LogLevel { get; set; } = Environment.GetEnvironmentVariable("TWILIO_LOG_LEVEL");
private readonly string _username;
private readonly string _password;
private readonly AuthStrategy _authstrategy;

/// <summary>
/// Constructor for a TwilioRestClient
Expand All @@ -68,11 +70,13 @@ public TwilioRestClient(
string accountSid = null,
string region = null,
HttpClient httpClient = null,
string edge = null
string edge = null,
AuthStrategy authstrategy = null
)
{
_username = username;
_password = password;
_authstrategy = authstrategy;

AccountSid = accountSid ?? username;
HttpClient = httpClient ?? DefaultClient();
Expand All @@ -89,7 +93,13 @@ public TwilioRestClient(
/// <returns>response of the request</returns>
public Response Request(Request request)
{
request.SetAuth(_username, _password);

if(_username != null && _password != null){
request.SetAuth(_username, _password);
}
else if(_authstrategy != null){
request.SetAuth(_authstrategy);
}

if (LogLevel == "debug")
LogRequest(request);
Expand All @@ -102,7 +112,6 @@ public Response Request(Request request)

if (UserAgentExtensions != null)
request.UserAgentExtensions = UserAgentExtensions;

Response response;
try
{
Expand Down Expand Up @@ -132,7 +141,12 @@ public Response Request(Request request)
/// <returns>Task that resolves to the response of the request</returns>
public async Task<Response> RequestAsync(Request request)
{
request.SetAuth(_username, _password);
if(_username != null && _password != null){
request.SetAuth(_username, _password);
}
else if(_authstrategy != null){
request.SetAuth(_authstrategy);
}

if (Region != null)
request.Region = Region;
Expand Down
Loading
Loading