Skip to content

Commit 28aa99c

Browse files
committed
Authorize annotation now matches user ID and user name claims
Previously the authorize annotation only checked DefaultRoleClaimType. Now it also checks DefaultUserIdClaimType and DefaultNameClaimType, consistent with sse_scope authorize behavior. The SSE matching scope was also aligned to check all three claim types.
1 parent 10e8c9b commit 28aa99c

5 files changed

Lines changed: 113 additions & 2 deletions

File tree

NpgsqlRest/NpgsqlRestEndpoint.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1436,7 +1436,10 @@ await Results.Problem(
14361436
bool ok = false;
14371437
foreach (var claim in context.User?.Claims ?? [])
14381438
{
1439-
if (string.Equals(claim.Type, Options.AuthenticationOptions.DefaultRoleClaimType, StringComparison.Ordinal))
1439+
if (
1440+
string.Equals(claim.Type, Options.AuthenticationOptions.DefaultUserIdClaimType, StringComparison.Ordinal) ||
1441+
string.Equals(claim.Type, Options.AuthenticationOptions.DefaultNameClaimType, StringComparison.Ordinal) ||
1442+
string.Equals(claim.Type, Options.AuthenticationOptions.DefaultRoleClaimType, StringComparison.Ordinal))
14401443
{
14411444
if (endpoint.AuthorizeRoles.Contains(claim.Value) is true)
14421445
{

NpgsqlRest/NpgsqlRestSseEventSource.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,10 @@ public async Task InvokeAsync(HttpContext context)
101101
bool ok = false;
102102
foreach (var claim in context.User?.Claims ?? [])
103103
{
104-
if (string.Equals(claim.Type, Options.AuthenticationOptions.DefaultRoleClaimType, StringComparison.Ordinal))
104+
if (
105+
string.Equals(claim.Type, Options.AuthenticationOptions.DefaultUserIdClaimType, StringComparison.Ordinal) ||
106+
string.Equals(claim.Type, Options.AuthenticationOptions.DefaultNameClaimType, StringComparison.Ordinal) ||
107+
string.Equals(claim.Type, Options.AuthenticationOptions.DefaultRoleClaimType, StringComparison.Ordinal))
105108
{
106109
if (endpoint?.AuthorizeRoles.Contains(claim.Value) is true)
107110
{

NpgsqlRestTests/AuthTests/AuthorizedTests.cs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ public static void AuthTests()
1919
2020
create function authorized_roles4() returns text language sql as 'select ''roles4''';
2121
comment on function authorized_roles4() is 'authorize test_role1 test_role2 test_role3';
22+
23+
create function authorized_by_name() returns text language sql as 'select ''by_name''';
24+
comment on function authorized_by_name() is 'authorize user';
25+
26+
create function authorized_by_name_wrong() returns text language sql as 'select ''by_name_wrong''';
27+
comment on function authorized_by_name_wrong() is 'authorize wrong_user';
28+
29+
create function authorized_by_userid() returns text language sql as 'select ''by_userid''';
30+
comment on function authorized_by_userid() is 'authorize user123';
31+
32+
create function authorized_by_userid_wrong() returns text language sql as 'select ''by_userid_wrong''';
33+
comment on function authorized_by_userid_wrong() is 'authorize wrong_id';
34+
35+
create function authorized_mixed() returns text language sql as 'select ''mixed''';
36+
comment on function authorized_mixed() is 'authorize wrong_role, user123';
2237
""");
2338
}
2439
}
@@ -121,4 +136,71 @@ public async Task Test_authorized_roles4()
121136
content2.Should().Contain("\"status\":403");
122137
content2.Should().Contain("\"title\":\"Forbidden\"");
123138
}
139+
140+
[Fact]
141+
public async Task Test_authorized_by_name()
142+
{
143+
using var client = test.Application.CreateClient();
144+
client.Timeout = TimeSpan.FromHours(1);
145+
146+
using var response1 = await client.PostAsync("/api/authorized-by-name/", null);
147+
response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
148+
149+
using var login = await client.GetAsync("/login");
150+
151+
using var response2 = await client.PostAsync("/api/authorized-by-name/", null);
152+
response2.StatusCode.Should().Be(HttpStatusCode.OK);
153+
}
154+
155+
[Fact]
156+
public async Task Test_authorized_by_name_wrong()
157+
{
158+
using var client = test.Application.CreateClient();
159+
client.Timeout = TimeSpan.FromHours(1);
160+
161+
using var login = await client.GetAsync("/login");
162+
163+
using var response = await client.PostAsync("/api/authorized-by-name-wrong/", null);
164+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
165+
}
166+
167+
[Fact]
168+
public async Task Test_authorized_by_userid()
169+
{
170+
using var client = test.Application.CreateClient();
171+
client.Timeout = TimeSpan.FromHours(1);
172+
173+
using var response1 = await client.PostAsync("/api/authorized-by-userid/", null);
174+
response1.StatusCode.Should().Be(HttpStatusCode.Unauthorized);
175+
176+
using var login = await client.GetAsync("/login");
177+
178+
using var response2 = await client.PostAsync("/api/authorized-by-userid/", null);
179+
response2.StatusCode.Should().Be(HttpStatusCode.OK);
180+
}
181+
182+
[Fact]
183+
public async Task Test_authorized_by_userid_wrong()
184+
{
185+
using var client = test.Application.CreateClient();
186+
client.Timeout = TimeSpan.FromHours(1);
187+
188+
using var login = await client.GetAsync("/login");
189+
190+
using var response = await client.PostAsync("/api/authorized-by-userid-wrong/", null);
191+
response.StatusCode.Should().Be(HttpStatusCode.Forbidden);
192+
}
193+
194+
[Fact]
195+
public async Task Test_authorized_mixed()
196+
{
197+
using var client = test.Application.CreateClient();
198+
client.Timeout = TimeSpan.FromHours(1);
199+
200+
using var login = await client.GetAsync("/login");
201+
202+
// wrong_role doesn't match, but user123 matches user_id claim
203+
using var response = await client.PostAsync("/api/authorized-mixed/", null);
204+
response.StatusCode.Should().Be(HttpStatusCode.OK);
205+
}
124206
}

NpgsqlRestTests/Setup/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public static void Main()
159159
app.MapGet("/login", () => Results.SignIn(new ClaimsPrincipal(new ClaimsIdentity(
160160
claims: new[]
161161
{
162+
new Claim(authOptions.DefaultUserIdClaimType, "user123"),
162163
new Claim(authOptions.DefaultNameClaimType, "user"),
163164
new Claim(authOptions.DefaultRoleClaimType, "role1"),
164165
new Claim(authOptions.DefaultRoleClaimType, "role2"),

changelog.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ export async function tsclientTestProxyPassthrough() : Promise<Response> {
2626

2727
This allows callers to handle the upstream response appropriately (`.json()`, `.blob()`, `.text()`, etc.), just like `proxy_out` endpoints.
2828

29+
### `authorize` Annotation Now Matches User ID and User Name Claims
30+
31+
The `authorize` comment annotation previously only matched against role claims (`DefaultRoleClaimType`). It now also matches against user ID (`DefaultUserIdClaimType`) and user name (`DefaultNameClaimType`) claims, aligning with the behavior that `sse_scope authorize` already had.
32+
33+
This means you can now restrict endpoint access to specific users, not just roles:
34+
35+
```sql
36+
-- Authorize by role (existing behavior)
37+
comment on function get_reports() is 'authorize admin';
38+
39+
-- Authorize by user name (new)
40+
comment on function get_my_profile() is 'authorize john';
41+
42+
-- Authorize by user ID (new)
43+
comment on function get_account() is 'authorize user123';
44+
45+
-- Mix of roles and user identifiers (new)
46+
comment on function get_data() is 'authorize admin, user123, jane';
47+
```
48+
49+
The SSE `matching` scope was also aligned to check all three claim types, making authorization behavior consistent across all features.
50+
2951
---
3052

3153
## Version [3.11.0](https://github.com/NpgsqlRest/NpgsqlRest/tree/3.11.0) (2026-03-10)

0 commit comments

Comments
 (0)