Skip to content

Commit

Permalink
Add admin calls for MFA (#105)
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelschattgen authored Jul 26, 2024
1 parent c5658ac commit 7d763dd
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 0 deletions.
25 changes: 25 additions & 0 deletions Gotrue/AdminClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using Newtonsoft.Json;
using Supabase.Gotrue.Interfaces;
using Supabase.Gotrue.Mfa;
using Supabase.Gotrue.Responses;

namespace Supabase.Gotrue
Expand Down Expand Up @@ -99,6 +100,7 @@ public async Task<bool> DeleteUser(string uid)
{
return _api.UpdateUserById(_serviceKey, userId, userData);
}

/// <inheritdoc />
public async Task<GenerateLinkResponse?> GenerateLink(GenerateLinkOptions options)
{
Expand All @@ -112,6 +114,29 @@ public async Task<bool> DeleteUser(string uid)
return result;
}

/// <inheritdoc />
public async Task<MfaAdminListFactorsResponse?> ListFactors(MfaAdminListFactorsParams listFactorsParams)
{
var response = await _api.ListFactors(_serviceKey, listFactorsParams);
response.ResponseMessage?.EnsureSuccessStatusCode();

if (response.Content is null)
return null;

var result = JsonConvert.DeserializeObject<List<Factor>>(response.Content);
var listFactorsResponse = new MfaAdminListFactorsResponse
{
Factors = result

Check warning on line 129 in Gotrue/AdminClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Possible null reference assignment.

Check warning on line 129 in Gotrue/AdminClient.cs

View workflow job for this annotation

GitHub Actions / build-and-test

Possible null reference assignment.
};

return listFactorsResponse;
}

public async Task<MfaAdminDeleteFactorResponse?> DeleteFactor(MfaAdminDeleteFactorParams deleteFactorParams)
{
return await _api.DeleteFactor(_serviceKey, deleteFactorParams);
}

/// <inheritdoc />
public async Task<User?> Update(UserAttributes attributes)
{
Expand Down
12 changes: 12 additions & 0 deletions Gotrue/Api.cs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,18 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt
return Helpers.MakeRequest<MfaUnenrollResponse>(HttpMethod.Delete, $"{Url}/factors/{mfaUnenrollParams.FactorId}", null, CreateAuthedRequestHeaders(jwt));
}

/// <inheritdoc />
public Task<BaseResponse> ListFactors(string jwt, MfaAdminListFactorsParams listFactorsParams)
{
return Helpers.MakeRequest(HttpMethod.Get, $"{Url}/admin/users/{listFactorsParams.UserId}/factors", null, CreateAuthedRequestHeaders(jwt));
}

/// <inheritdoc />
public Task<MfaAdminDeleteFactorResponse?> DeleteFactor(string jwt, MfaAdminDeleteFactorParams deleteFactorParams)
{
return Helpers.MakeRequest<MfaAdminDeleteFactorResponse>(HttpMethod.Delete, $"{Url}/admin/users/{deleteFactorParams.UserId}/factors/{deleteFactorParams.Id}", null, CreateAuthedRequestHeaders(jwt));
}

/// <inheritdoc />
public async Task<ProviderAuthState> LinkIdentity(string token, Provider provider, SignInOptions options)
{
Expand Down
15 changes: 15 additions & 0 deletions Gotrue/Interfaces/IGotrueAdminClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Supabase.Core.Attributes;
using Supabase.Core.Interfaces;
using Supabase.Gotrue.Mfa;
using Supabase.Gotrue.Responses;

namespace Supabase.Gotrue.Interfaces
Expand Down Expand Up @@ -91,5 +92,19 @@ public interface IGotrueAdminClient<TUser> : IGettableHeaders
/// <param name="options">Options for this call. `Password` is required for <see cref="GenerateLinkOptions.LinkType.SignUp"/>, `Data` is an optional parameter for <see cref="GenerateLinkOptions.LinkType.SignUp"/>.</param>
/// <returns></returns>
public Task<GenerateLinkResponse?> GenerateLink(GenerateLinkOptions options);

/// <summary>
/// Lists all factors associated to a specific user.
/// </summary>
/// <param name="listFactorParams">A <see cref="MfaAdminListFactorsParams"/> object that contains the user id.</param>
/// <returns>A list of <see cref="Factor"/> that this user has enabled.</returns>
public Task<MfaAdminListFactorsResponse?> ListFactors(MfaAdminListFactorsParams listFactorsParams);

/// <summary>
/// Deletes a factor on a user. This will log the user out of all active sessions if the deleted factor was verified.
/// </summary>
/// <param name="listFactorParams">A <see cref="MfaAdminListFactorsParams"/> object that contains the user id.</param>
/// <returns>A <see cref="MfaAdminDeleteFactorResponse"/> containing the deleted factor id.</returns>
public Task<MfaAdminDeleteFactorResponse?> DeleteFactor(MfaAdminDeleteFactorParams deleteFactorParams);
}
}
2 changes: 2 additions & 0 deletions Gotrue/Interfaces/IGotrueApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ public interface IGotrueApi<TUser, TSession> : IGettableHeaders
Task<MfaChallengeResponse?> Challenge(string jwt, MfaChallengeParams mfaChallengeParams);
Task<MfaVerifyResponse?> Verify(string jwt, MfaVerifyParams mfaVerifyParams);
Task<MfaUnenrollResponse?> Unenroll(string jwt, MfaUnenrollParams mfaVerifyParams);
Task<BaseResponse> ListFactors(string jwt, MfaAdminListFactorsParams listFactorsParams);
Task<MfaAdminDeleteFactorResponse?> DeleteFactor(string jwt, MfaAdminDeleteFactorParams deleteFactorParams);

/// <summary>
/// Links an oauth identity to an existing user.
Expand Down
11 changes: 11 additions & 0 deletions Gotrue/Mfa/MfaAdminDeleteFactorParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Supabase.Gotrue.Mfa
{
public class MfaAdminDeleteFactorParams
{
// Id of the MFA factor to delete
public string Id { get; set; }

// Id of the user whose factor is being deleted
public string UserId { get; set; }
}
}
11 changes: 11 additions & 0 deletions Gotrue/Mfa/MfaAdminDeleteFactorResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;

namespace Supabase.Gotrue.Mfa
{
public class MfaAdminDeleteFactorResponse
{
// Id of the factor that was successfully deleted
[JsonPropertyName("id")]
public string Id { get; set; }
}
}
8 changes: 8 additions & 0 deletions Gotrue/Mfa/MfaAdminListFactorsParams.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Supabase.Gotrue.Mfa
{
public class MfaAdminListFactorsParams
{
// Id of the user
public string UserId { get; set; }
}
}
10 changes: 10 additions & 0 deletions Gotrue/Mfa/MfaAdminListFactorsResponse.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Newtonsoft.Json;

namespace Supabase.Gotrue.Mfa
{
public class MfaAdminListFactorsResponse
{
public List<Factor> Factors { get; set; } = new List<Factor>();
}
}
84 changes: 84 additions & 0 deletions GotrueTests/MfaClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,15 @@ namespace GotrueTests;
public class MfaClientTests
{
private IGotrueClient<User, Session> _client;
private IGotrueAdminClient<User> _adminClient;

private readonly string _serviceKey = GenerateServiceRoleToken();

[TestInitialize]
public void TestInitializer()
{
_client = new Client(new ClientOptions { AllowUnconfirmedUserSessions = true });
_adminClient = new AdminClient(_serviceKey, new ClientOptions { AllowUnconfirmedUserSessions = true });
}

[TestMethod("MFA: Complete flow")]
Expand Down Expand Up @@ -103,6 +107,86 @@ await _client.ChallengeAndVerify(new MfaChallengeAndVerifyParams
IsTrue(factors.Totp.Count == 0);
}

[TestMethod("MFA Admin: List factors for user")]
public async Task MfaAdminListFactorsForUser()
{
var email = $"{RandomString(12)}@supabase.io";
var session = await _client.SignUp(email, PASSWORD);
VerifyGoodSession(session);

var mfaEnrollParams = new MfaEnrollParams
{
Issuer = "Supabase",
FactorType = "totp",
FriendlyName = "Enroll test"
};

var enrollResponse = await _client.Enroll(mfaEnrollParams);
IsNotNull(enrollResponse.Id);

var factors = await _adminClient.ListFactors(new MfaAdminListFactorsParams
{
UserId = session.User.Id
});
IsNotNull(factors);
AreEqual(1, factors.Factors.Count);
AreEqual(enrollResponse.Id, factors.Factors.FirstOrDefault().Id);
AreEqual("unverified", factors.Factors.FirstOrDefault().Status);

var totpCode = TotpGenerator.GeneratePin(enrollResponse.Totp.Secret, 30, 6);
await _client.ChallengeAndVerify(new MfaChallengeAndVerifyParams
{
FactorId = enrollResponse.Id,
Code = totpCode
});

factors = await _adminClient.ListFactors(new MfaAdminListFactorsParams
{
UserId = session.User.Id
});
IsNotNull(factors);
AreEqual(1, factors.Factors.Count);
AreEqual(enrollResponse.Id, factors.Factors.FirstOrDefault().Id);
AreEqual("verified", factors.Factors.FirstOrDefault().Status);
}

[TestMethod("MFA Admin: Delete factor for user")]
public async Task MfaAdminDeleteFactorForUser()
{
var email = $"{RandomString(12)}@supabase.io";
var session = await _client.SignUp(email, PASSWORD);
VerifyGoodSession(session);

var mfaEnrollParams = new MfaEnrollParams
{
Issuer = "Supabase",
FactorType = "totp",
FriendlyName = "Enroll test"
};

var enrollResponse = await _client.Enroll(mfaEnrollParams);
IsNotNull(enrollResponse.Id);

var listFactors = await _adminClient.ListFactors(new MfaAdminListFactorsParams
{
UserId = session.User.Id
});
AreEqual(1, listFactors.Factors.Count);

var deleteFactorResponse = await _adminClient.DeleteFactor(new MfaAdminDeleteFactorParams
{
Id = enrollResponse.Id,
UserId = session.User.Id
});
AreEqual(enrollResponse.Id, deleteFactorResponse.Id);

listFactors = await _adminClient.ListFactors(new MfaAdminListFactorsParams
{
UserId = session.User.Id
});
AreEqual(0, listFactors.Factors.Count);
}

[TestMethod("MFA: Invalid TOTP")]
public async Task MfaInvalidTotp()
{
Expand Down

0 comments on commit 7d763dd

Please sign in to comment.