diff --git a/packages/Gotrue/Gotrue.Tests/Gotrue.Tests.csproj b/packages/Gotrue/Gotrue.Tests/Gotrue.Tests.csproj
index 5a2f578e..2d702c33 100644
--- a/packages/Gotrue/Gotrue.Tests/Gotrue.Tests.csproj
+++ b/packages/Gotrue/Gotrue.Tests/Gotrue.Tests.csproj
@@ -36,6 +36,7 @@
+
diff --git a/packages/Gotrue/Gotrue.Tests/Observability/ObservabilityContractTests.cs b/packages/Gotrue/Gotrue.Tests/Observability/ObservabilityContractTests.cs
index bcd0d6a5..8c0e51a3 100644
--- a/packages/Gotrue/Gotrue.Tests/Observability/ObservabilityContractTests.cs
+++ b/packages/Gotrue/Gotrue.Tests/Observability/ObservabilityContractTests.cs
@@ -141,6 +141,17 @@ public async Task DomainSpan_ShouldParentTheHttpSpan_GivenRecoveryCodesStatusRea
this.activities.Should().ContainSingle(a => a.OperationName == "GET /factors/recovery-codes").Which.ParentSpanId.Should().Be(domainSpan.SpanId);
}
+ [TestMethod]
+ public async Task DomainSpan_ShouldParentTheHttpSpan_GivenRecoveryCodesUnenrollment()
+ {
+ this.MockTokenSuccess();
+ await this.Refresh();
+ this.MockRecoveryCodesUnenrollmentSuccess();
+ await this.client.UnenrollRecoveryCodesAsync();
+ var domainSpan = this.activities.Should().ContainSingle(a => a.OperationName == "gotrue.unenroll_recovery_codes").Which;
+ this.activities.Should().ContainSingle(a => a.OperationName == "DELETE /factors/recovery-codes").Which.ParentSpanId.Should().Be(domainSpan.SpanId);
+ }
+
[TestMethod]
public async Task HttpSpan_ShouldBeMarkedError_GivenFailedRequest()
{
@@ -255,6 +266,14 @@ private void MockRecoveryCodesStatusSuccess() =>
.WithHeader("Content-Type", "application/json")
.WithBody(File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "GetRecoveryCodesStatus", "Data", "recovery_codes_status.json"))));
+ private void MockRecoveryCodesUnenrollmentSuccess() =>
+ this.server
+ .Given(Request.Create().WithPath("/factors/recovery-codes").UsingDelete())
+ .RespondWith(Response.Create()
+ .WithStatusCode(200)
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UnenrollRecoveryCodes", "Data", "unenrolled.json"))));
+
private void MockTokenFailure() =>
this.server
.Given(Request.Create().WithPath("/token").UsingPost())
diff --git a/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/insufficient_aal_error.json b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/insufficient_aal_error.json
new file mode 100644
index 00000000..c528ead2
--- /dev/null
+++ b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/insufficient_aal_error.json
@@ -0,0 +1,5 @@
+{
+ "code": 403,
+ "error_code": "insufficient_aal",
+ "msg": "AAL2 required to delete recovery codes"
+}
diff --git a/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/not_enrolled_error.json b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/not_enrolled_error.json
new file mode 100644
index 00000000..ae20295e
--- /dev/null
+++ b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/not_enrolled_error.json
@@ -0,0 +1,5 @@
+{
+ "code": 404,
+ "error_code": "mfa_factor_not_found",
+ "msg": "The user has not enrolled recovery codes"
+}
diff --git a/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/unenrolled.json b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/unenrolled.json
new file mode 100644
index 00000000..b4951f74
--- /dev/null
+++ b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/Data/unenrolled.json
@@ -0,0 +1,3 @@
+{
+ "id": "2b306a77-21dc-4110-ba71-537cb56b9e98"
+}
diff --git a/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/UnenrollRecoveryCodesContractTests.cs b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/UnenrollRecoveryCodesContractTests.cs
new file mode 100644
index 00000000..c89cb8b3
--- /dev/null
+++ b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/UnenrollRecoveryCodesContractTests.cs
@@ -0,0 +1,162 @@
+#region
+
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentAssertions;
+using Gotrue.Tests.Support;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Supabase.Gotrue;
+using Supabase.Gotrue.Exceptions;
+using Supabase.Gotrue.Interfaces;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using static Supabase.Gotrue.Exceptions.FailureHint.Reason;
+
+#endregion
+
+namespace Gotrue.Tests.UnenrollRecoveryCodes;
+
+///
+/// Pins how UnenrollRecoveryCodesAsync removes the signed-in user's MFA recovery codes: the
+/// DELETE /factors/recovery-codes request it puts on the wire, and the failures raised before and after the
+/// request, all against a stubbed server.
+///
+[TestClass]
+[TestCategory("Contract")]
+public class UnenrollRecoveryCodesContractTests
+{
+ private const string AccessToken = "an-access-token";
+ private IGotrueClient client = null!;
+ private MockGotrueServer server = null!;
+
+ [TestInitialize]
+ public void TestInitializer()
+ {
+ this.server = new MockGotrueServer();
+ this.client = TestClients.Against(this.server);
+ }
+
+ [TestCleanup]
+ public void TestCleanup() => this.server.Dispose();
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldDeleteTheRecoveryCodesWithTheSessionBearer()
+ {
+ this.SignIn();
+ this.MockUnenrollResponse(200, Fixture("unenrolled.json"));
+ await this.client.UnenrollRecoveryCodesAsync();
+ this.server.VerifySingleReceivedRequest()
+ .WithMethod(HttpMethod.Delete)
+ .WithPath("/factors/recovery-codes")
+ .WithHeader("Authorization", $"Bearer {AccessToken}")
+ .WithHeader("apikey", MockGotrueServer.ApiKey);
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldComplete_GivenEnrolledUser()
+ {
+ this.SignIn();
+ this.MockUnenrollResponse(200, Fixture("unenrolled.json"));
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync();
+ await unenrolling.Should().NotThrowAsync("GoTrue answers 200 with the removed factor id once the recovery codes are deleted");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldThrowServerRejection_GivenSessionBelowAal2()
+ {
+ this.SignIn();
+ this.MockUnenrollResponse(403, Fixture("insufficient_aal_error.json"));
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync();
+ (await unenrolling.Should().ThrowAsync()).Which.ErrorCode.Should().Be("insufficient_aal",
+ "DELETE /factors/recovery-codes requires an aal2 session (auth OpenAPI, 403 insufficient_aal)");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldThrowServerRejection_GivenUserWithoutRecoveryCodes()
+ {
+ this.SignIn();
+ this.MockUnenrollResponse(404, Fixture("not_enrolled_error.json"));
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync();
+ (await unenrolling.Should().ThrowAsync()).Which.ErrorCode.Should().Be("mfa_factor_not_found",
+ "there is nothing to remove when the user has not enrolled recovery codes (auth OpenAPI, 404 mfa_factor_not_found)");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldThrowNoSessionFound_GivenNoSession()
+ {
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync();
+ (await unenrolling.Should().ThrowAsync().WithMessage("A valid session is required.")).Which.Reason.Should().Be(NoSessionFound,
+ "DELETE /factors/recovery-codes requires the signed-in user's access token");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldThrowNoSessionFound_GivenSessionWithoutAccessToken()
+ {
+ this.SignIn(new Session { RefreshToken = "a-refresh-token", ExpiresIn = 3600 });
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync();
+ (await unenrolling.Should().ThrowAsync()).Which.Reason.Should().Be(NoSessionFound,
+ "DELETE /factors/recovery-codes requires the signed-in user's access token");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldNotSendARequest_GivenNoSession()
+ {
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync();
+ await unenrolling.Should().ThrowAsync();
+ this.server.CountReceivedRequests().Should().Be(0, "the missing session is detected before the wire");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldThrowOffline_GivenClientIsOffline()
+ {
+ this.SignIn();
+ this.client.Online = false;
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync();
+ (await unenrolling.Should().ThrowAsync().WithMessage("Only supported when online")).Which.Reason.Should().Be(Offline,
+ "removing the recovery codes needs the server, like the other MFA operations");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldCancelTheRequest_GivenCancelledToken()
+ {
+ this.SignIn();
+ this.MockUnenrollResponse(200, Fixture("unenrolled.json"));
+ using var cancellation = new CancellationTokenSource();
+ cancellation.Cancel();
+ var unenrolling = () => this.client.UnenrollRecoveryCodesAsync(cancellation.Token);
+ await unenrolling.Should().ThrowAsync("the cancellation token reaches the HTTP call");
+ }
+
+ [TestMethod]
+ public void UnenrollRecoveryCodesAsync_ShouldComplete_GivenCallerBlockingOnContextThatNeverRunsContinuations()
+ {
+ this.SignIn();
+ this.MockUnenrollResponse(200, Fixture("unenrolled.json"));
+ BlockingCaller.Completes(() => this.client.UnenrollRecoveryCodesAsync()).Should().BeTrue(
+ "a library must not resume on the caller's context: a UI thread blocking on the task would deadlock");
+ }
+
+ private void SignIn() => this.SignIn(new Session { AccessToken = AccessToken, RefreshToken = "a-refresh-token", ExpiresIn = 3600, User = new User { Id = "user-id-123" } });
+
+ private void SignIn(Session session)
+ {
+ var persistence = SessionPersistenceSubstitute.Tracking();
+ persistence.SaveSession(session);
+ this.client.SetPersistence(persistence);
+ this.client.LoadSession();
+ }
+
+ private void MockUnenrollResponse(int statusCode, string body) =>
+ this.server
+ .Given(Request.Create().WithPath("/factors/recovery-codes").UsingDelete())
+ .RespondWith(Response.Create()
+ .WithStatusCode(statusCode)
+ .WithHeader("Content-Type", "application/json")
+ .WithBody(body));
+
+ private static string Fixture(string name) =>
+ File.ReadAllText(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "UnenrollRecoveryCodes", "Data", name));
+}
diff --git a/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/UnenrollRecoveryCodesTests.cs b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/UnenrollRecoveryCodesTests.cs
new file mode 100644
index 00000000..230b75df
--- /dev/null
+++ b/packages/Gotrue/Gotrue.Tests/UnenrollRecoveryCodes/UnenrollRecoveryCodesTests.cs
@@ -0,0 +1,43 @@
+#region
+
+using System.Threading.Tasks;
+using FluentAssertions;
+using Gotrue.Tests.Mfa;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Supabase.Gotrue.Exceptions;
+using Supabase.Gotrue.Mfa;
+
+#endregion
+
+namespace Gotrue.Tests.UnenrollRecoveryCodes;
+
+///
+/// End-to-end removal of the signed-in user's MFA recovery codes against the live stack. The local stack
+/// cannot enroll recovery codes (the CLI does not expose the auth server's recovery codes enroll setting),
+/// so these cover the rejections the stack can reach; a successful removal is pinned by
+/// .
+///
+[TestClass]
+[TestCategory("E2E")]
+public class UnenrollRecoveryCodesTests : MfaFixture
+{
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldThrowInsufficientAal_GivenAal1Session()
+ {
+ await this.SignUpNewUser();
+ var unenrolling = () => this.Client.UnenrollRecoveryCodesAsync();
+ (await unenrolling.Should().ThrowAsync()).Which.ErrorCode.Should().Be("insufficient_aal",
+ "DELETE /factors/recovery-codes requires an aal2 session (auth OpenAPI, 403 insufficient_aal)");
+ }
+
+ [TestMethod]
+ public async Task UnenrollRecoveryCodesAsync_ShouldThrowFactorNotFound_GivenAal2SessionWithoutRecoveryCodes()
+ {
+ await this.SignUpNewUser();
+ var enrollment = await this.EnrollTotp();
+ await this.Client.ChallengeAndVerify(new MfaChallengeAndVerifyParams { FactorId = enrollment.Id, Code = TotpCode(enrollment) });
+ var unenrolling = () => this.Client.UnenrollRecoveryCodesAsync();
+ (await unenrolling.Should().ThrowAsync()).Which.ErrorCode.Should().Be("mfa_factor_not_found",
+ "an aal2 user who never enrolled recovery codes has nothing to remove (auth OpenAPI, 404 mfa_factor_not_found)");
+ }
+}
diff --git a/packages/Gotrue/Gotrue/Api.cs b/packages/Gotrue/Gotrue/Api.cs
index 4f9c2248..ea6d050b 100644
--- a/packages/Gotrue/Gotrue/Api.cs
+++ b/packages/Gotrue/Gotrue/Api.cs
@@ -657,6 +657,10 @@ public ProviderAuthState GetUriForProvider(Provider provider, SignInOptions? opt
}
}
+ ///
+ public Task UnenrollRecoveryCodesAsync(string jwt, CancellationToken cancellationToken = default) =>
+ this.MakeRequestAsync(HttpMethod.Delete, $"{this.Url}/factors/recovery-codes", null, this.CreateAuthedRequestHeaders(jwt), cancellationToken);
+
///
public Task ListFactors(string jwt, MfaAdminListFactorsParams listFactorsParams) => this.MakeRequestAsync(HttpMethod.Get, $"{this.Url}/admin/users/{PathSegment.Escape(listFactorsParams.UserId)}/factors", null, this.CreateAuthedRequestHeaders(jwt));
diff --git a/packages/Gotrue/Gotrue/Client.cs b/packages/Gotrue/Gotrue/Client.cs
index 366c02ba..08748075 100644
--- a/packages/Gotrue/Gotrue/Client.cs
+++ b/packages/Gotrue/Gotrue/Client.cs
@@ -1224,6 +1224,19 @@ public void StartAutoRefresh()
return await this.api.GetRecoveryCodesStatusAsync(accessToken, cancellationToken).ConfigureAwait(false);
}
+ ///
+ [ImplementedCapability("auth.mfa_recovery_codes.unenroll")]
+ public async Task UnenrollRecoveryCodesAsync(CancellationToken cancellationToken = default)
+ {
+ using var activity = GotrueInstrumentation.Source.StartActivity(GotrueInstrumentation.Spans.UnenrollRecoveryCodes);
+ if (!this.Online)
+ {
+ throw new GotrueException("Only supported when online", Offline);
+ }
+ var accessToken = this.CurrentSession?.AccessToken ?? throw new GotrueException("A valid session is required.", NoSessionFound);
+ await this.api.UnenrollRecoveryCodesAsync(accessToken, cancellationToken).ConfigureAwait(false);
+ }
+
///
[ImplementedCapability("auth.session.get_claims")]
public async Task GetClaimsAsync(string? jwt = null, GetClaimsOptions? options = null, CancellationToken cancellationToken = default)
diff --git a/packages/Gotrue/Gotrue/GotrueInstrumentation.cs b/packages/Gotrue/Gotrue/GotrueInstrumentation.cs
index 72fe6470..e1db7ad5 100644
--- a/packages/Gotrue/Gotrue/GotrueInstrumentation.cs
+++ b/packages/Gotrue/Gotrue/GotrueInstrumentation.cs
@@ -72,6 +72,7 @@ internal static class Spans
internal const string LinkIdentityWithIdToken = "gotrue.link_identity.id_token";
internal const string GetUserIdentities = "gotrue.get_user_identities";
internal const string GetRecoveryCodesStatus = "gotrue.get_recovery_codes_status";
+ internal const string UnenrollRecoveryCodes = "gotrue.unenroll_recovery_codes";
internal const string SignInWithOtp = "gotrue.sign_in.otp";
internal const string SignIn = "gotrue.sign_in";
internal const string SignInAnonymously = "gotrue.sign_in.anonymous";
diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs
index 0bb9ca73..032577aa 100644
--- a/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs
+++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueApi.cs
@@ -80,6 +80,14 @@ public interface IGotrueApi : IGettableHeaders
/// The recovery codes status, or null when the user has no recovery codes.
/// The server rejected the request.
Task GetRecoveryCodesStatusAsync(string jwt, CancellationToken cancellationToken = default);
+
+ ///
+ /// Removes the user's MFA recovery codes factor together with every remaining code.
+ ///
+ /// The user's access token, at the aal2 assurance level.
+ /// Cancels the request.
+ /// The server rejected the request.
+ Task UnenrollRecoveryCodesAsync(string jwt, CancellationToken cancellationToken = default);
Task ListFactors(string jwt, MfaAdminListFactorsParams listFactorsParams);
Task DeleteFactor(string jwt, MfaAdminDeleteFactorParams deleteFactorParams);
Task UpdateFactorAsync(string jwt, string userId, string factorId, FactorChanges changes, CancellationToken cancellationToken = default);
diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs
index ba36865a..e6bcaeb2 100644
--- a/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs
+++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueClient.cs
@@ -679,6 +679,29 @@ public interface IGotrueClient : IGettableHeaders
///
Task GetRecoveryCodesStatusAsync(CancellationToken cancellationToken = default);
+ ///
+ /// Removes the signed-in user's MFA recovery codes factor, invalidating every remaining code. The session must
+ /// be at the aal2 assurance level. Sessions that reached aal2 with a recovery code are lowered to
+ /// aal1 on the server; the current session keeps its tokens until it is refreshed.
+ /// Recovery codes are experimental on the auth server.
+ ///
+ /// Cancels the request.
+ ///
+ /// when the client is offline,
+ /// when no user is signed in,
+ /// or the server's rejection of the request, with its :
+ /// insufficient_aal when the session is not at aal2,
+ /// mfa_factor_not_found when the user has no recovery codes,
+ /// no_authorization for an anonymous user.
+ ///
+ ///
+ ///
+ /// await client.ChallengeAndVerify(new MfaChallengeAndVerifyParams { FactorId = totpFactorId, Code = code });
+ /// await client.UnenrollRecoveryCodesAsync();
+ ///
+ ///
+ Task UnenrollRecoveryCodesAsync(CancellationToken cancellationToken = default);
+
#endregion
}
diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt
index bcb084b3..740165df 100644
--- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt
+++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt
@@ -45,10 +45,12 @@ Supabase.Gotrue.Api.CreateCustomProviderAsync(string! jwt, Supabase.Gotrue.Creat
Supabase.Gotrue.Api.GetCustomProviderAsync(string! jwt, string! identifier, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Api.GetRecoveryCodesStatusAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Api.ListCustomProvidersAsync(string! jwt, Supabase.Gotrue.Common.CustomProviderType? type = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
+Supabase.Gotrue.Api.UnenrollRecoveryCodesAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Api.UpdateCustomProviderAsync(string! jwt, string! identifier, Supabase.Gotrue.UpdateCustomProvider.CustomProviderChanges! changes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Api.UpdateFactorAsync(string! jwt, string! userId, string! factorId, Supabase.Gotrue.UpdateFactor.FactorChanges! changes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Client.GetRecoveryCodesStatusAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Client.GetUserIdentitiesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
+Supabase.Gotrue.Client.UnenrollRecoveryCodesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Common.CustomProvider
Supabase.Gotrue.Common.CustomProvider.$() -> Supabase.Gotrue.Common.CustomProvider!
Supabase.Gotrue.Common.CustomProvider.AcceptableClientIds.get -> System.Collections.Generic.IReadOnlyList!
@@ -185,10 +187,12 @@ Supabase.Gotrue.Interfaces.IGotrueApi.GetCustomProviderAsync(st
Supabase.Gotrue.Interfaces.IGotrueApi.GetRecoveryCodesStatusAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Interfaces.IGotrueApi.ListCustomProvidersAsync(string! jwt, Supabase.Gotrue.Common.CustomProviderType? type = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
Supabase.Gotrue.Interfaces.IGotrueApi.SignOutAsync(string! jwt, Supabase.Gotrue.Constants.SignOutScope scope = Supabase.Gotrue.Constants.SignOutScope.Global, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
+Supabase.Gotrue.Interfaces.IGotrueApi.UnenrollRecoveryCodesAsync(string! jwt, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Interfaces.IGotrueApi.UpdateCustomProviderAsync(string! jwt, string! identifier, Supabase.Gotrue.UpdateCustomProvider.CustomProviderChanges! changes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Interfaces.IGotrueApi.UpdateFactorAsync(string! jwt, string! userId, string! factorId, Supabase.Gotrue.UpdateFactor.FactorChanges! changes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Interfaces.IGotrueClient.GetRecoveryCodesStatusAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Interfaces.IGotrueClient.GetUserIdentitiesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!>!
+Supabase.Gotrue.Interfaces.IGotrueClient.UnenrollRecoveryCodesAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.Mfa.Factor.Phone.get -> string?
Supabase.Gotrue.Mfa.Factor.Phone.set -> void
Supabase.Gotrue.UpdateCustomProvider.CustomProviderChanges