diff --git a/Gotrue/Api.cs b/Gotrue/Api.cs index 76e14ce..d81ddb2 100644 --- a/Gotrue/Api.cs +++ b/Gotrue/Api.cs @@ -68,20 +68,21 @@ public Api(string url, Dictionary? headers = null) var body = new Dictionary { { "email", email }, { "password", password } }; var endpoint = $"{Url}/signup"; - if (options != null) - { - if (!string.IsNullOrEmpty(options.RedirectTo)) - { - endpoint = Helpers.AddQueryParams(endpoint, new Dictionary { { "redirect_to", options.RedirectTo! } }).ToString(); - } - if (options.Data != null) - { - body.Add("data", options.Data); - } - } + if (options != null) + { + if (options.Data != null) + { + body.Add("data", options.Data); + } + if (!string.IsNullOrEmpty(options.CaptchaToken)) + { + body.Add("captcha_token", options.CaptchaToken); + } + } + - var response = await Helpers.MakeRequest(HttpMethod.Post, endpoint, body, Headers); + var response = await Helpers.MakeRequest(HttpMethod.Post, endpoint, body, Headers); if (!string.IsNullOrEmpty(response.Content)) { diff --git a/Gotrue/Helpers.cs b/Gotrue/Helpers.cs index bcee01b..f08f7a2 100644 --- a/Gotrue/Helpers.cs +++ b/Gotrue/Helpers.cs @@ -105,7 +105,23 @@ internal static ProviderAuthState GetUrlForProvider(string url, Constants.Provid result.PKCEVerifier = codeVerifier; } - if (attr == null) + // Handle state parameter for CSRF protection + string stateParameter; + if (!string.IsNullOrEmpty(options.State)) + { + // Developer provided their own state - use it + stateParameter = options.State; + } + else + { + // Auto-generate state for convenience and security + stateParameter = Helpers.GenerateNonce(); + } + + query.Add("state", stateParameter); + result.State = stateParameter; + + if (attr == null) throw new Exception("Unknown provider"); query.Add("provider", attr.Mapping); diff --git a/Gotrue/ProviderAuthState.cs b/Gotrue/ProviderAuthState.cs index 28a1169..fe148db 100644 --- a/Gotrue/ProviderAuthState.cs +++ b/Gotrue/ProviderAuthState.cs @@ -26,5 +26,11 @@ public ProviderAuthState(Uri uri) { Uri = uri; } + + /// + /// The state parameter for CSRF protection. + /// This should be stored by the developer and validated when the OAuth callback is received. + /// + public string? State { get; set; } } } diff --git a/Gotrue/SignInOptions.cs b/Gotrue/SignInOptions.cs index 4223345..87560d9 100644 --- a/Gotrue/SignInOptions.cs +++ b/Gotrue/SignInOptions.cs @@ -29,5 +29,12 @@ public class SignInOptions /// PKCE is recommended for mobile and server-side applications. /// public OAuthFlowType FlowType { get; set; } = OAuthFlowType.Implicit; + + + /// + /// The state parameter for CSRF protection. + /// This should be stored by the developer and validated when the OAuth callback is received. + /// + public string? State { get; set; } } } diff --git a/Gotrue/SignUpOptions.cs b/Gotrue/SignUpOptions.cs index 7517522..2914d0f 100644 --- a/Gotrue/SignUpOptions.cs +++ b/Gotrue/SignUpOptions.cs @@ -10,5 +10,11 @@ public class SignUpOptions : SignInOptions /// Optional user metadata. /// public Dictionary? Data { get; set; } + + + /// + /// Captcha token for verification when captcha is enabled + /// + public string? CaptchaToken { get; set; } } } diff --git a/GotrueTests/AnonKeyClientTests.cs b/GotrueTests/AnonKeyClientTests.cs index 02b3523..74b77cc 100644 --- a/GotrueTests/AnonKeyClientTests.cs +++ b/GotrueTests/AnonKeyClientTests.cs @@ -123,7 +123,24 @@ public async Task SignUpUserPhone() AreEqual("Testing", session.User.UserMetadata["firstName"]); } - [TestMethod("Client: Triggers Token Refreshed Event")] + [TestMethod("Client: Sign Up with Captcha Token")] + public async Task SignUpUserWithCaptchaToken() + { + IsTrue(AuthStateIsEmpty()); + + var email = $"{RandomString(12)}@supabase.io"; + var options = new SignUpOptions + { + CaptchaToken = "test-captcha-token-12345" + }; + + var session = await _client.SignUp(email, PASSWORD, options); + + VerifyGoodSession(session); + } + + + [TestMethod("Client: Triggers Token Refreshed Event")] public async Task ClientTriggersTokenRefreshedEvent() { var tsc = new TaskCompletionSource();