Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Gotrue/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ internal static ProviderAuthState GetUrlForProvider(string url, Constants.Provid
if (attr == null)
throw new Exception("Unknown provider");

var state = !string.IsNullOrEmpty(options.State) ? options.State : GenerateNonce();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TIL that Go doesn't have a keyword for immutable variables that are not known at compile time 🤯

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here. We have the const keyword but for compile-time only, and var just infers the type.
Otherwise, immutability is something you have to address through the type. There's no other way to "enforce" it

query.Add("state", state);
result.State = state;

query.Add("provider", attr.Mapping);

if (!string.IsNullOrEmpty(options.Scopes))
Expand Down
6 changes: 6 additions & 0 deletions Gotrue/ProviderAuthState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public class ProviderAuthState
/// </summary>
public string? PKCEVerifier { get; set; }

/// <summary>
/// The state parameter included in the OAuth URL for CSRF protection (RFC 6749 §10.12).
/// Validate this against the state echoed back in the OAuth callback.
/// </summary>
public string State { get; set; } = null!;

/// <summary>
/// Constructor
/// </summary>
Expand Down
8 changes: 8 additions & 0 deletions Gotrue/SignInOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,13 @@ public class SignInOptions
/// PKCE is recommended for mobile and server-side applications.
/// </summary>
public OAuthFlowType FlowType { get; set; } = OAuthFlowType.Implicit;

/// <summary>
/// An optional state parameter for CSRF protection (RFC 6749 §10.12).
/// If not provided, one will be generated automatically.
/// Store the returned <see cref="ProviderAuthState.State"/> value and validate it
/// against the state echoed back in the OAuth callback.
/// </summary>
public string? State { get; set; }
}
}
8 changes: 6 additions & 2 deletions GotrueTests/AnonKeyClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -243,10 +243,14 @@ public async Task ClientSendsMagicLoginEmailAlias()
public async Task ClientReturnsAuthUrlForProvider()
{
var result1 = await _client.SignIn(Constants.Provider.Google);
AreEqual($"{TestClients.CliAuthUrl}/authorize?provider=google", result1.Uri.ToString());
IsTrue(result1.Uri.ToString().StartsWith($"{TestClients.CliAuthUrl}/authorize"));
IsTrue(result1.Uri.Query.Contains("provider=google"));
IsTrue(result1.Uri.Query.Contains("state="));

var result2 = await _client.SignIn(Constants.Provider.Google, new SignInOptions { Scopes = "special scopes please" });
AreEqual($"{TestClients.CliAuthUrl}/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString());
IsTrue(result2.Uri.Query.Contains("provider=google"));
IsTrue(result2.Uri.Query.Contains("scopes=special+scopes+please"));
IsTrue(result2.Uri.Query.Contains("state="));
}

[TestMethod("Client: Returns Verification Code for Provider")]
Expand Down
21 changes: 21 additions & 0 deletions GotrueTests/PKCE/PkceContractTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,27 @@ public async Task ResetPasswordForEmailPkce_ChallengeIsHashOfVerifier()
request.ReadJsonBodyField("code_challenge").Should().Be(S256(state.PKCEVerifier!), "server must receive BASE64URL(SHA256(verifier)) per RFC 7636 §4.2");
}

[TestMethod("SignInWithProvider: auto-generates state and includes it in the URL")]
public async Task SignInWithProvider_AutoGeneratesState()
{
var result = await client.SignIn(Provider.Github, new SignInOptions { FlowType = OAuthFlowType.PKCE });
result.State.Should().NotBeNullOrEmpty();
result.Uri.Query.Should().Contain($"state={result.State}");
}

[TestMethod("SignInWithProvider: uses developer-provided state and includes it in the URL")]
public async Task SignInWithProvider_UsesDeveloperProvidedState()
{
var customState = "my-server-generated-csrf-token";
var result = await client.SignIn(Provider.Github, new SignInOptions
{
FlowType = OAuthFlowType.PKCE,
State = customState
});
result.State.Should().Be(customState);
result.Uri.Query.Should().Contain($"state={customState}");
}

private static string S256(string verifier)
{
using var sha = SHA256.Create();
Expand Down
8 changes: 6 additions & 2 deletions GotrueTests/StatelessClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,14 @@ public async Task SendsMagicLoginEmailAlias()
public void ReturnsAuthUrlForProvider()
{
var result1 = _client.SignIn(Provider.Google, Options);
Assert.AreEqual($"{TestClients.CliAuthUrl}/authorize?provider=google", result1.Uri.ToString());
Assert.IsTrue(result1.Uri.ToString().StartsWith($"{TestClients.CliAuthUrl}/authorize"));
Assert.IsTrue(result1.Uri.Query.Contains("provider=google"));
Assert.IsTrue(result1.Uri.Query.Contains("state="));

var result2 = _client.SignIn(Provider.Google, Options, new SignInOptions { Scopes = "special scopes please" });
Assert.AreEqual($"{TestClients.CliAuthUrl}/authorize?provider=google&scopes=special+scopes+please", result2.Uri.ToString());
Assert.IsTrue(result2.Uri.Query.Contains("provider=google"));
Assert.IsTrue(result2.Uri.Query.Contains("scopes=special+scopes+please"));
Assert.IsTrue(result2.Uri.Query.Contains("state="));
}

[TestMethod("StatelessClient: Update user")]
Expand Down
Loading