diff --git a/packages/Gotrue/Gotrue.Tests/Stateless/StatelessSignOutTests.cs b/packages/Gotrue/Gotrue.Tests/Stateless/StatelessSignOutTests.cs
new file mode 100644
index 00000000..2c63bff6
--- /dev/null
+++ b/packages/Gotrue/Gotrue.Tests/Stateless/StatelessSignOutTests.cs
@@ -0,0 +1,65 @@
+#region
+
+using System.Threading.Tasks;
+using FluentAssertions;
+using Gotrue.Tests.Support;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Supabase.Gotrue;
+using Supabase.Gotrue.Exceptions;
+using WireMock.RequestBuilders;
+using WireMock.ResponseBuilders;
+using static Supabase.Gotrue.Constants;
+using static Supabase.Gotrue.StatelessClient;
+
+#endregion
+
+namespace Gotrue.Tests.Stateless;
+
+///
+/// Stateless sign-out sends the requested scope to /logout, global by default, and throws when the request fails.
+///
+[TestClass]
+[TestCategory("Contract")]
+public class StatelessSignOutTests
+{
+ private MockGotrueServer server = null!;
+
+ [TestInitialize]
+ public void TestInitialize() => this.server = new MockGotrueServer();
+
+ [TestCleanup]
+ public void TestCleanup() => this.server.Dispose();
+
+ [TestMethod]
+ [DataRow(SignOutScope.Global, "global")]
+ [DataRow(SignOutScope.Local, "local")]
+ [DataRow(SignOutScope.Others, "others")]
+ public async Task SignOut_ShouldSendTheRequestedScope(SignOutScope scope, string expected)
+ {
+ this.StubLogout(200);
+ await new StatelessClient().SignOut("user-access-token", this.Options(), scope);
+ this.server.VerifySingleReceivedRequest().WithQueryParam("scope", expected);
+ }
+
+ [TestMethod]
+ public async Task SignOut_ShouldSendGlobalScope_GivenNoScope()
+ {
+ this.StubLogout(200);
+ await new StatelessClient().SignOut("user-access-token", this.Options());
+ this.server.VerifySingleReceivedRequest().WithQueryParam("scope", "global");
+ }
+
+ [TestMethod]
+ public async Task SignOut_ShouldThrow_GivenErrorResponse()
+ {
+ this.StubLogout(401);
+ var signOut = () => new StatelessClient().SignOut("user-access-token", this.Options(), SignOutScope.Local);
+ await signOut.Should().ThrowAsync();
+ }
+
+ private void StubLogout(int statusCode) =>
+ this.server.Given(Request.Create().WithPath("/logout").UsingPost())
+ .RespondWith(Response.Create().WithStatusCode(statusCode));
+
+ private StatelessClientOptions Options() => new() { Url = this.server.Url };
+}
diff --git a/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs b/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs
index db205f66..3cadca16 100644
--- a/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs
+++ b/packages/Gotrue/Gotrue/Interfaces/IGotrueStatelessClient.cs
@@ -199,6 +199,12 @@ public interface IGotrueStatelessClient
///
Task SignOut(string accessToken, StatelessClientOptions options);
+ ///
+ /// Signs out the user from the sessions in the given scope.
+ /// JWT tokens will still be valid for stateless auth until they expire.
+ ///
+ Task SignOut(string accessToken, StatelessClientOptions options, SignOutScope scope);
+
///
/// Signs up a user
///
diff --git a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt
index 67f6412b..4656ff26 100644
--- a/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt
+++ b/packages/Gotrue/Gotrue/PublicAPI.Unshipped.txt
@@ -116,3 +116,5 @@ Supabase.Gotrue.Interfaces.IGotrueApi.GetUserAsync(string! jwt,
Supabase.Gotrue.Interfaces.IGotrueClient.GetClaimsAsync(string? jwt = null, Supabase.Gotrue.Claims.GetClaimsOptions? options = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task!
Supabase.Gotrue.SignUpOptions.CaptchaToken.get -> string?
Supabase.Gotrue.SignUpOptions.CaptchaToken.set -> void
+Supabase.Gotrue.Interfaces.IGotrueStatelessClient.SignOut(string! accessToken, Supabase.Gotrue.StatelessClient.StatelessClientOptions! options, Supabase.Gotrue.Constants.SignOutScope scope) -> System.Threading.Tasks.Task!
+Supabase.Gotrue.StatelessClient.SignOut(string! accessToken, Supabase.Gotrue.StatelessClient.StatelessClientOptions! options, Supabase.Gotrue.Constants.SignOutScope scope) -> System.Threading.Tasks.Task!
diff --git a/packages/Gotrue/Gotrue/StatelessClient.cs b/packages/Gotrue/Gotrue/StatelessClient.cs
index 72c004b8..65edca12 100644
--- a/packages/Gotrue/Gotrue/StatelessClient.cs
+++ b/packages/Gotrue/Gotrue/StatelessClient.cs
@@ -178,11 +178,17 @@ public async Task SignIn(string email, StatelessClientOptions options, Sig
///
public async Task SignOut(string accessToken, StatelessClientOptions options)
{
- var result = await this.GetApi(options).SignOut(accessToken);
- result.ResponseMessage?.EnsureSuccessStatusCode();
+ await this.SignOut(accessToken, options, SignOutScope.Global).ConfigureAwait(false);
return true;
}
+ ///
+ public async Task SignOut(string accessToken, StatelessClientOptions options, SignOutScope scope)
+ {
+ var result = await this.GetApi(options).SignOut(accessToken, scope).ConfigureAwait(false);
+ result.ResponseMessage?.EnsureSuccessStatusCode();
+ }
+
///
public async Task VerifyOTP(string phone, string otpToken, StatelessClientOptions options, MobileOtpType type = MobileOtpType.SMS)
{