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
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public class AdminApiKeyHeaderContractTests
private const string ServiceKey = "sb_secret_service_role_key";
private const string UserId = "user-123";

// JWT-shaped test token; no valid signature needed.
private const string UserToken =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." +
"eyJzdWIiOiJ1c2VyLTEyMyIsInJvbGUiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoyMDAwMDAwMDAwfQ." +
"not-a-real-signature";

private MockGotrueServer server = null!;

[TestInitialize]
Expand All @@ -33,6 +39,8 @@ public void TestInitialize()
this.server = new MockGotrueServer();
this.server.Given(Request.Create().WithPath($"/admin/users/{UserId}").UsingDelete())
.RespondWith(Response.Create().WithStatusCode(200).WithHeader("Content-Type", "application/json").WithBody("{}"));
this.server.Given(Request.Create().WithPath("/user").UsingGet())
.RespondWith(Response.Create().WithStatusCode(200).WithHeader("Content-Type", "application/json").WithBody("{}"));
}

[TestCleanup]
Expand Down Expand Up @@ -75,4 +83,15 @@ public async Task DeleteUser_ShouldNotDuplicateApiKey_GivenMetaStyleCasing()
await admin.DeleteUser(UserId);
this.server.VerifySingleReceivedRequest().WithHeader("apiKey", "project-anon-key");
}

[TestMethod]
public async Task GetUser_ShouldNotSendUserTokenAsApiKey_GivenNoApiKeyConfigured()
{
// Copying a user token into apikey caused a 401 (#424).
var admin = new AdminClient(ServiceKey, new ClientOptions { Url = this.server.Url });
await admin.GetUser(UserToken);
this.server.VerifySingleReceivedRequest()
.WithHeader("Authorization", $"Bearer {UserToken}")
.WithoutHeader("apikey");
}
}
7 changes: 7 additions & 0 deletions packages/Gotrue/Gotrue.Tests/Support/MockGotrueServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ internal ReceivedRequest WithHeader(string name, string expected)
return this;
}

internal ReceivedRequest WithoutHeader(string name)
{
this.request.Headers!.Keys.Should().NotContain(key => string.Equals(key, name, StringComparison.OrdinalIgnoreCase),
$"'{name}' should not have been sent");
return this;
}

internal ReceivedRequest WithJsonContentType()
{
this.request.Headers.Should().ContainKey("Content-Type").WhoseValue.Single().Should().StartWith("application/json");
Expand Down
9 changes: 4 additions & 5 deletions packages/Gotrue/Gotrue/Api.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
Expand Down Expand Up @@ -523,11 +524,9 @@ private Dictionary<string, string> CreateAuthedRequestHeaders(string jwt)
["Authorization"] = $"Bearer {jwt}"
};

// New-format API keys (sb_publishable_/sb_secret_) are rejected on Authorization-only requests;
// the gateway requires an apikey header too. Preserve an apikey the caller already supplied (the
// stateful client injects the project key via GetHeaders — note the "apiKey" casing), otherwise
// fall back to the bearer token, matching supabase-js standalone admin usage.
if (!headers.Keys.Any(key => string.Equals(key, "apikey", StringComparison.OrdinalIgnoreCase)))
// Opaque keys need an apikey header too. Keep any configured key and never copy a JWT here (#424).
var hasApiKey = headers.Keys.Any(key => string.Equals(key, "apikey", StringComparison.OrdinalIgnoreCase));
if (!hasApiKey && !new JwtSecurityTokenHandler().CanReadToken(jwt))
headers["apikey"] = jwt;

return headers;
Expand Down
Loading