Skip to content

Commit

Permalink
feat: Public OAuth uptake for C# libraries (#762)
Browse files Browse the repository at this point in the history
* Public OAuth uptake for C# libraries
  • Loading branch information
AsabuHere authored Oct 23, 2024
1 parent 48d7fdf commit 451cae4
Show file tree
Hide file tree
Showing 22 changed files with 653 additions and 23 deletions.
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
47 changes: 47 additions & 0 deletions src/Twilio/AuthStrategies/BasicAuthStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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()
{
int hash = 17;
hash = hash * 31 + (username != null ? username.GetHashCode() : 0);
hash = hash * 31 + (password != null ? password.GetHashCode() : 0);
return hash;
}

}
}
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;
}
}
}
155 changes: 155 additions & 0 deletions src/Twilio/AuthStrategies/TokenAuthStrategy.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
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()
{
int hash = 17;
hash = hash * 31 + (token != null ? token.GetHashCode() : 0);
hash = hash * 31 + (tokenManager != null ? tokenManager.GetHashCode() : 0);
return hash;
}


public bool tokenExpired(String accessToken){
#if NET35
return IsTokenExpired(accessToken);
#else
return isTokenExpired(accessToken);
#endif
}

#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 111 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 118 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 @@ -28,7 +28,7 @@ namespace Twilio.Clients.BearerToken
/// <summary>
/// Implementation of a TwilioRestClient.
/// </summary>
[Beta]
[Deprecated]
public class TwilioOrgsTokenRestClient
{
/// <summary>
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

0 comments on commit 451cae4

Please sign in to comment.