Skip to content

Commit d26b3ee

Browse files
authored
Add suffix authentication checks and allowAnonymous option (#338)
* Add suffix authentication checks and allowAnonymous option Introduces a check that throws an InvalidOperationException if a suffix callback is provided and the user is not authenticated, unless allowAnonymous is set to true. Adds the allowAnonymous parameter to all UseDelta overloads, updates documentation and usage examples to explain correct middleware ordering and anonymous scenarios, and adds tests for suffix/authentication behavior.
1 parent 6ab89f6 commit d26b3ee

21 files changed

+645
-75
lines changed

docs/mdsource/postgres-ef.source.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ include: map-group-ef
3939
include: should-execute-ef
4040

4141

42+
include: suffix-auth-ef
43+
44+
4245
include: last-timestamp-ef

docs/mdsource/postgres.source.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ include: map-group
3434
include: should-execute
3535

3636

37+
include: suffix-auth
38+
39+
3740
### Custom Connection discovery
3841

3942
By default, Delta uses `HttpContext.RequestServices` to discover the NpgsqlConnection and NpgsqlTransaction:

docs/mdsource/sqlserver-ef.source.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ include: map-group-ef
4040
include: should-execute-ef
4141

4242

43+
include: suffix-auth-ef
44+
45+
4346
include: last-timestamp-ef
4447

4548

docs/mdsource/sqlserver.source.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ include: map-group
3434
include: should-execute
3535

3636

37+
include: suffix-auth
38+
39+
3740
### Custom Connection discovery
3841

3942
By default, Delta uses `HttpContext.RequestServices` to discover the SqlConnection and SqlTransaction:
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
### Suffix and Authentication
2+
3+
When using a `suffix` callback that accesses `HttpContext.User` claims, authentication middleware **must** run before `UseDelta`. If `UseDelta` runs first, the User claims won't be populated yet, and all users will get the same cache key.
4+
5+
Delta automatically detects this misconfiguration and throws an `InvalidOperationException` with a helpful message if:
6+
- A `suffix` callback is provided
7+
- The user is not authenticated (`context.User.Identity?.IsAuthenticated != true`)
8+
9+
snippet: SuffixWithAuthEF
10+
11+
12+
### AllowAnonymous
13+
14+
For endpoints that intentionally allow anonymous access but still want to use a suffix for cache differentiation (e.g., based on request headers rather than user claims), use `allowAnonymous: true`:
15+
16+
snippet: AllowAnonymousEF
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
### Suffix and Authentication
2+
3+
When using a `suffix` callback that accesses `HttpContext.User` claims, authentication middleware **must** run before `UseDelta`. If `UseDelta` runs first, the User claims won't be populated yet, and all users will get the same cache key.
4+
5+
Delta automatically detects this misconfiguration and throws an `InvalidOperationException` with a helpful message if:
6+
- A `suffix` callback is provided
7+
- The user is not authenticated (`context.User.Identity?.IsAuthenticated != true`)
8+
9+
snippet: SuffixWithAuth
10+
11+
12+
### AllowAnonymous
13+
14+
For endpoints that intentionally allow anonymous access but still want to use a suffix for cache differentiation (e.g., based on request headers rather than user claims), use `allowAnonymous: true`:
15+
16+
snippet: AllowAnonymous

docs/postgres-ef.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,58 @@ app.UseDelta<SampleDbContext>(
151151
return path.Contains("match");
152152
});
153153
```
154-
<sup><a href='/src/Delta.EFTests/Usage.cs#L16-L26' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShouldExecuteEF' title='Start of snippet'>anchor</a></sup>
154+
<sup><a href='/src/Delta.EFTests/Usage.cs#L18-L28' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShouldExecuteEF' title='Start of snippet'>anchor</a></sup>
155+
<!-- endSnippet -->
156+
<!-- endInclude -->
157+
158+
159+
### Suffix and Authentication<!-- include: suffix-auth-ef. path: /docs/mdsource/suffix-auth-ef.include.md -->
160+
161+
When using a `suffix` callback that accesses `HttpContext.User` claims, authentication middleware **must** run before `UseDelta`. If `UseDelta` runs first, the User claims won't be populated yet, and all users will get the same cache key.
162+
163+
Delta automatically detects this misconfiguration and throws an `InvalidOperationException` with a helpful message if:
164+
- A `suffix` callback is provided
165+
- The user is not authenticated (`context.User.Identity?.IsAuthenticated != true`)
166+
167+
<!-- snippet: SuffixWithAuthEF -->
168+
<a id='snippet-SuffixWithAuthEF'></a>
169+
```cs
170+
var app = builder.Build();
171+
172+
// Authentication middleware must run before UseDelta
173+
// so that User claims are available to the suffix callback
174+
app.UseAuthentication();
175+
app.UseAuthorization();
176+
177+
app.UseDelta<SampleDbContext>(
178+
suffix: httpContext =>
179+
{
180+
// Access user claims to create per-user cache keys
181+
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
182+
var tenantId = httpContext.User.FindFirst("TenantId")?.Value;
183+
return $"{userId}-{tenantId}";
184+
});
185+
```
186+
<sup><a href='/src/Delta.EFTests/Usage.cs#L33-L51' title='Snippet source file'>snippet source</a> | <a href='#snippet-SuffixWithAuthEF' title='Start of snippet'>anchor</a></sup>
187+
<!-- endSnippet -->
188+
189+
190+
### AllowAnonymous
191+
192+
For endpoints that intentionally allow anonymous access but still want to use a suffix for cache differentiation (e.g., based on request headers rather than user claims), use `allowAnonymous: true`:
193+
194+
<!-- snippet: AllowAnonymousEF -->
195+
<a id='snippet-AllowAnonymousEF'></a>
196+
```cs
197+
var app = builder.Build();
198+
199+
// For endpoints that intentionally allow anonymous access
200+
// but still want a suffix for cache differentiation
201+
app.UseDelta<SampleDbContext>(
202+
suffix: httpContext => httpContext.Request.Headers["X-Client-Version"].ToString(),
203+
allowAnonymous: true);
204+
```
205+
<sup><a href='/src/Delta.EFTests/Usage.cs#L56-L66' title='Snippet source file'>snippet source</a> | <a href='#snippet-AllowAnonymousEF' title='Start of snippet'>anchor</a></sup>
155206
<!-- endSnippet -->
156207
<!-- endInclude -->
157208

@@ -167,7 +218,7 @@ It can be called on a DbContext:
167218
```cs
168219
var timeStamp = await dbContext.GetLastTimeStamp();
169220
```
170-
<sup><a href='/src/Delta.EFTests/Usage.cs#L41-L45' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampEF' title='Start of snippet'>anchor</a></sup>
221+
<sup><a href='/src/Delta.EFTests/Usage.cs#L81-L85' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampEF' title='Start of snippet'>anchor</a></sup>
171222
<!-- endSnippet -->
172223

173224
Or a DbConnection:
@@ -177,6 +228,6 @@ Or a DbConnection:
177228
```cs
178229
var timeStamp = await connection.GetLastTimeStamp();
179230
```
180-
<sup><a href='/src/DeltaTests/Usage.cs#L188-L192' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampConnection' title='Start of snippet'>anchor</a></sup>
231+
<sup><a href='/src/DeltaTests/Usage.cs#L226-L230' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampConnection' title='Start of snippet'>anchor</a></sup>
181232
<!-- endSnippet -->
182233
<!-- endInclude -->

docs/postgres.md

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,57 @@ app.UseDelta(
120120
<!-- endInclude -->
121121

122122

123+
### Suffix and Authentication<!-- include: suffix-auth. path: /docs/mdsource/suffix-auth.include.md -->
124+
125+
When using a `suffix` callback that accesses `HttpContext.User` claims, authentication middleware **must** run before `UseDelta`. If `UseDelta` runs first, the User claims won't be populated yet, and all users will get the same cache key.
126+
127+
Delta automatically detects this misconfiguration and throws an `InvalidOperationException` with a helpful message if:
128+
- A `suffix` callback is provided
129+
- The user is not authenticated (`context.User.Identity?.IsAuthenticated != true`)
130+
131+
<!-- snippet: SuffixWithAuth -->
132+
<a id='snippet-SuffixWithAuth'></a>
133+
```cs
134+
var app = builder.Build();
135+
136+
// Authentication middleware must run before UseDelta
137+
// so that User claims are available to the suffix callback
138+
app.UseAuthentication();
139+
app.UseAuthorization();
140+
141+
app.UseDelta(
142+
suffix: httpContext =>
143+
{
144+
// Access user claims to create per-user cache keys
145+
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
146+
var tenantId = httpContext.User.FindFirst("TenantId")?.Value;
147+
return $"{userId}-{tenantId}";
148+
});
149+
```
150+
<sup><a href='/src/DeltaTests/Usage.cs#L34-L52' title='Snippet source file'>snippet source</a> | <a href='#snippet-SuffixWithAuth' title='Start of snippet'>anchor</a></sup>
151+
<!-- endSnippet -->
152+
153+
154+
### AllowAnonymous
155+
156+
For endpoints that intentionally allow anonymous access but still want to use a suffix for cache differentiation (e.g., based on request headers rather than user claims), use `allowAnonymous: true`:
157+
158+
<!-- snippet: AllowAnonymous -->
159+
<a id='snippet-AllowAnonymous'></a>
160+
```cs
161+
var app = builder.Build();
162+
163+
// For endpoints that intentionally allow anonymous access
164+
// but still want a suffix for cache differentiation
165+
app.UseDelta(
166+
suffix: httpContext => httpContext.Request.Headers["X-Client-Version"].ToString(),
167+
allowAnonymous: true);
168+
```
169+
<sup><a href='/src/DeltaTests/Usage.cs#L57-L67' title='Snippet source file'>snippet source</a> | <a href='#snippet-AllowAnonymous' title='Start of snippet'>anchor</a></sup>
170+
<!-- endSnippet -->
171+
<!-- endInclude -->
172+
173+
123174
### Custom Connection discovery
124175

125176
By default, Delta uses `HttpContext.RequestServices` to discover the NpgsqlConnection and NpgsqlTransaction:
@@ -162,7 +213,7 @@ application.UseDelta(
162213
getConnection: httpContext =>
163214
httpContext.RequestServices.GetRequiredService<NpgsqlConnection>());
164215
```
165-
<sup><a href='/src/DeltaTests/Usage.cs#L337-L344' title='Snippet source file'>snippet source</a> | <a href='#snippet-CustomDiscoveryConnectionPostgres' title='Start of snippet'>anchor</a></sup>
216+
<sup><a href='/src/DeltaTests/Usage.cs#L375-L382' title='Snippet source file'>snippet source</a> | <a href='#snippet-CustomDiscoveryConnectionPostgres' title='Start of snippet'>anchor</a></sup>
166217
<!-- endSnippet -->
167218

168219
To use custom connection and transaction discovery:
@@ -180,7 +231,7 @@ application.UseDelta(
180231
return new(connection, transaction);
181232
});
182233
```
183-
<sup><a href='/src/DeltaTests/Usage.cs#L365-L377' title='Snippet source file'>snippet source</a> | <a href='#snippet-CustomDiscoveryConnectionAndTransactionPostgres' title='Start of snippet'>anchor</a></sup>
234+
<sup><a href='/src/DeltaTests/Usage.cs#L403-L415' title='Snippet source file'>snippet source</a> | <a href='#snippet-CustomDiscoveryConnectionAndTransactionPostgres' title='Start of snippet'>anchor</a></sup>
184235
<!-- endSnippet -->
185236

186237

@@ -193,6 +244,6 @@ application.UseDelta(
193244
```cs
194245
var timeStamp = await connection.GetLastTimeStamp();
195246
```
196-
<sup><a href='/src/DeltaTests/Usage.cs#L188-L192' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampConnection' title='Start of snippet'>anchor</a></sup>
247+
<sup><a href='/src/DeltaTests/Usage.cs#L226-L230' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampConnection' title='Start of snippet'>anchor</a></sup>
197248
<!-- endSnippet -->
198249
<!-- endInclude -->

docs/sqlserver-ef.md

Lines changed: 60 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,58 @@ app.UseDelta<SampleDbContext>(
204204
return path.Contains("match");
205205
});
206206
```
207-
<sup><a href='/src/Delta.EFTests/Usage.cs#L16-L26' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShouldExecuteEF' title='Start of snippet'>anchor</a></sup>
207+
<sup><a href='/src/Delta.EFTests/Usage.cs#L18-L28' title='Snippet source file'>snippet source</a> | <a href='#snippet-ShouldExecuteEF' title='Start of snippet'>anchor</a></sup>
208+
<!-- endSnippet -->
209+
<!-- endInclude -->
210+
211+
212+
### Suffix and Authentication<!-- include: suffix-auth-ef. path: /docs/mdsource/suffix-auth-ef.include.md -->
213+
214+
When using a `suffix` callback that accesses `HttpContext.User` claims, authentication middleware **must** run before `UseDelta`. If `UseDelta` runs first, the User claims won't be populated yet, and all users will get the same cache key.
215+
216+
Delta automatically detects this misconfiguration and throws an `InvalidOperationException` with a helpful message if:
217+
- A `suffix` callback is provided
218+
- The user is not authenticated (`context.User.Identity?.IsAuthenticated != true`)
219+
220+
<!-- snippet: SuffixWithAuthEF -->
221+
<a id='snippet-SuffixWithAuthEF'></a>
222+
```cs
223+
var app = builder.Build();
224+
225+
// Authentication middleware must run before UseDelta
226+
// so that User claims are available to the suffix callback
227+
app.UseAuthentication();
228+
app.UseAuthorization();
229+
230+
app.UseDelta<SampleDbContext>(
231+
suffix: httpContext =>
232+
{
233+
// Access user claims to create per-user cache keys
234+
var userId = httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
235+
var tenantId = httpContext.User.FindFirst("TenantId")?.Value;
236+
return $"{userId}-{tenantId}";
237+
});
238+
```
239+
<sup><a href='/src/Delta.EFTests/Usage.cs#L33-L51' title='Snippet source file'>snippet source</a> | <a href='#snippet-SuffixWithAuthEF' title='Start of snippet'>anchor</a></sup>
240+
<!-- endSnippet -->
241+
242+
243+
### AllowAnonymous
244+
245+
For endpoints that intentionally allow anonymous access but still want to use a suffix for cache differentiation (e.g., based on request headers rather than user claims), use `allowAnonymous: true`:
246+
247+
<!-- snippet: AllowAnonymousEF -->
248+
<a id='snippet-AllowAnonymousEF'></a>
249+
```cs
250+
var app = builder.Build();
251+
252+
// For endpoints that intentionally allow anonymous access
253+
// but still want a suffix for cache differentiation
254+
app.UseDelta<SampleDbContext>(
255+
suffix: httpContext => httpContext.Request.Headers["X-Client-Version"].ToString(),
256+
allowAnonymous: true);
257+
```
258+
<sup><a href='/src/Delta.EFTests/Usage.cs#L56-L66' title='Snippet source file'>snippet source</a> | <a href='#snippet-AllowAnonymousEF' title='Start of snippet'>anchor</a></sup>
208259
<!-- endSnippet -->
209260
<!-- endInclude -->
210261

@@ -220,7 +271,7 @@ It can be called on a DbContext:
220271
```cs
221272
var timeStamp = await dbContext.GetLastTimeStamp();
222273
```
223-
<sup><a href='/src/Delta.EFTests/Usage.cs#L41-L45' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampEF' title='Start of snippet'>anchor</a></sup>
274+
<sup><a href='/src/Delta.EFTests/Usage.cs#L81-L85' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampEF' title='Start of snippet'>anchor</a></sup>
224275
<!-- endSnippet -->
225276

226277
Or a DbConnection:
@@ -230,7 +281,7 @@ Or a DbConnection:
230281
```cs
231282
var timeStamp = await connection.GetLastTimeStamp();
232283
```
233-
<sup><a href='/src/DeltaTests/Usage.cs#L188-L192' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampConnection' title='Start of snippet'>anchor</a></sup>
284+
<sup><a href='/src/DeltaTests/Usage.cs#L226-L230' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetLastTimeStampConnection' title='Start of snippet'>anchor</a></sup>
234285
<!-- endSnippet -->
235286
<!-- endInclude -->
236287

@@ -255,7 +306,7 @@ foreach (var db in trackedDatabases)
255306
Trace.WriteLine(db);
256307
}
257308
```
258-
<sup><a href='/src/DeltaTests/Usage.cs#L204-L212' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetDatabasesWithTracking' title='Start of snippet'>anchor</a></sup>
309+
<sup><a href='/src/DeltaTests/Usage.cs#L242-L250' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetDatabasesWithTracking' title='Start of snippet'>anchor</a></sup>
259310
<!-- endSnippet -->
260311

261312
Uses the following SQL:
@@ -285,7 +336,7 @@ foreach (var db in trackedTables)
285336
Trace.WriteLine(db);
286337
}
287338
```
288-
<sup><a href='/src/DeltaTests/Usage.cs#L230-L238' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetTrackedTables' title='Start of snippet'>anchor</a></sup>
339+
<sup><a href='/src/DeltaTests/Usage.cs#L268-L276' title='Snippet source file'>snippet source</a> | <a href='#snippet-GetTrackedTables' title='Start of snippet'>anchor</a></sup>
289340
<!-- endSnippet -->
290341

291342
Uses the following SQL:
@@ -310,7 +361,7 @@ Determine if change tracking is enabled for a database.
310361
```cs
311362
var isTrackingEnabled = await sqlConnection.IsTrackingEnabled();
312363
```
313-
<sup><a href='/src/DeltaTests/Usage.cs#L315-L319' title='Snippet source file'>snippet source</a> | <a href='#snippet-IsTrackingEnabled' title='Start of snippet'>anchor</a></sup>
364+
<sup><a href='/src/DeltaTests/Usage.cs#L353-L357' title='Snippet source file'>snippet source</a> | <a href='#snippet-IsTrackingEnabled' title='Start of snippet'>anchor</a></sup>
314365
<!-- endSnippet -->
315366

316367
Uses the following SQL:
@@ -337,7 +388,7 @@ Enable change tracking for a database.
337388
```cs
338389
await sqlConnection.EnableTracking();
339390
```
340-
<sup><a href='/src/DeltaTests/Usage.cs#L309-L313' title='Snippet source file'>snippet source</a> | <a href='#snippet-EnableTracking' title='Start of snippet'>anchor</a></sup>
391+
<sup><a href='/src/DeltaTests/Usage.cs#L347-L351' title='Snippet source file'>snippet source</a> | <a href='#snippet-EnableTracking' title='Start of snippet'>anchor</a></sup>
341392
<!-- endSnippet -->
342393

343394
Uses the following SQL:
@@ -365,7 +416,7 @@ Disable change tracking for a database and all tables within that database.
365416
```cs
366417
await sqlConnection.DisableTracking();
367418
```
368-
<sup><a href='/src/DeltaTests/Usage.cs#L294-L298' title='Snippet source file'>snippet source</a> | <a href='#snippet-DisableTracking' title='Start of snippet'>anchor</a></sup>
419+
<sup><a href='/src/DeltaTests/Usage.cs#L332-L336' title='Snippet source file'>snippet source</a> | <a href='#snippet-DisableTracking' title='Start of snippet'>anchor</a></sup>
369420
<!-- endSnippet -->
370421

371422
Uses the following SQL:
@@ -402,7 +453,7 @@ Enables change tracking for all tables listed, and disables change tracking for
402453
```cs
403454
await sqlConnection.SetTrackedTables(["Companies"]);
404455
```
405-
<sup><a href='/src/DeltaTests/Usage.cs#L224-L228' title='Snippet source file'>snippet source</a> | <a href='#snippet-SetTrackedTables' title='Start of snippet'>anchor</a></sup>
456+
<sup><a href='/src/DeltaTests/Usage.cs#L262-L266' title='Snippet source file'>snippet source</a> | <a href='#snippet-SetTrackedTables' title='Start of snippet'>anchor</a></sup>
406457
<!-- endSnippet -->
407458

408459
Uses the following SQL:

0 commit comments

Comments
 (0)