Skip to content

Commit bac7b82

Browse files
authored
feat: add async authentication authority and policy evaluation (#107)
* feat: add async authentication authority and policy evaluation * Add AuthAuthorityTests
1 parent 6723566 commit bac7b82

11 files changed

Lines changed: 225 additions & 30 deletions

File tree

‎src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthAuthority.cs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,14 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;
44

55
public interface IAuthAuthority
66
{
7-
AccessDecisionResult Decide(AuthContext context, IEnumerable<IAuthorityPolicy>? policies = null);
7+
[Obsolete("Synchronous authority evaluation is no longer supported. Use DecideAsync instead.")]
8+
AccessDecisionResult Decide(AuthContext context, IEnumerable<IAuthorityPolicy>? policies = null)
9+
=> throw new NotSupportedException("Synchronous authority evaluation is not supported. Use DecideAsync.");
10+
11+
ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, IEnumerable<IAuthorityPolicy>? policies = null, CancellationToken ct = default)
12+
{
13+
#pragma warning disable CS0618
14+
return ValueTask.FromResult(Decide(context, policies));
15+
#pragma warning restore CS0618
16+
}
817
}

‎src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityInvariant.cs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,14 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;
55
// TODO: Add ClockSkewInvariant to handle cases where client and server clocks are not synchronized, which can lead to valid tokens being rejected due to "not valid yet" or "expired" errors. This invariant would check the token's "nbf" (not before) and "exp" (expiration) claims against the current time, allowing for a configurable clock skew (e.g., 5 minutes) to accommodate minor discrepancies in system clocks.
66
public interface IAuthorityInvariant
77
{
8-
AccessDecisionResult Decide(AuthContext context);
8+
[Obsolete("Implement DecideAsync instead.")]
9+
AccessDecisionResult Decide(AuthContext context)
10+
=> throw new NotSupportedException("Implement DecideAsync for asynchronous invariant evaluation.");
11+
12+
ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, CancellationToken ct = default)
13+
{
14+
#pragma warning disable CS0618
15+
return ValueTask.FromResult(Decide(context));
16+
#pragma warning restore CS0618
17+
}
918
}

‎src/CodeBeam.UltimateAuth.Core/Abstractions/Authority/IAuthorityPolicy.cs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,15 @@ namespace CodeBeam.UltimateAuth.Core.Abstractions;
55
public interface IAuthorityPolicy
66
{
77
bool AppliesTo(AuthContext context);
8-
AccessDecisionResult Decide(AuthContext context);
8+
9+
[Obsolete("Implement DecideAsync instead.")]
10+
AccessDecisionResult Decide(AuthContext context)
11+
=> throw new NotSupportedException("Implement DecideAsync for asynchronous policy evaluation.");
12+
13+
ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, CancellationToken ct = default)
14+
{
15+
#pragma warning disable CS0618
16+
return ValueTask.FromResult(Decide(context));
17+
#pragma warning restore CS0618
18+
}
919
}

‎src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/AuthAuthority.cs‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,17 @@ public AuthAuthority(IEnumerable<IAuthorityInvariant> invariants, IEnumerable<IA
1414
_policies = policies ?? Array.Empty<IAuthorityPolicy>();
1515
}
1616

17-
public AccessDecisionResult Decide(AuthContext context, IEnumerable<IAuthorityPolicy>? policies = null)
17+
public async ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, IEnumerable<IAuthorityPolicy>? policies = null, CancellationToken ct = default)
1818
{
19+
ct.ThrowIfCancellationRequested();
20+
1921
foreach (var invariant in _invariants)
2022
{
21-
var result = invariant.Decide(context);
23+
ct.ThrowIfCancellationRequested();
24+
25+
var result = await invariant.DecideAsync(
26+
context, ct);
27+
2228
if (!result.IsAllowed)
2329
return result;
2430
}
@@ -29,10 +35,12 @@ public AccessDecisionResult Decide(AuthContext context, IEnumerable<IAuthorityPo
2935

3036
foreach (var policy in effectivePolicies)
3137
{
38+
ct.ThrowIfCancellationRequested();
39+
3240
if (!policy.AppliesTo(context))
3341
continue;
3442

35-
var result = policy.Decide(context);
43+
var result = await policy.DecideAsync(context, ct);
3644

3745
if (!result.IsAllowed)
3846
return result;
@@ -45,5 +53,4 @@ public AccessDecisionResult Decide(AuthContext context, IEnumerable<IAuthorityPo
4553
? AccessDecisionResult.Challenge("Additional verification required.")
4654
: AccessDecisionResult.Allow();
4755
}
48-
4956
}

‎src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/DeviceRequiredInvariant.cs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure;
66

77
public sealed class DeviceRequiredInvariant : IAuthorityInvariant
88
{
9-
public AccessDecisionResult Decide(AuthContext context)
9+
public ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, CancellationToken ct = default)
1010
{
1111
if (!RequiresDevice(context))
12-
return AccessDecisionResult.Allow();
12+
return ValueTask.FromResult(AccessDecisionResult.Allow());
1313

1414
if (!context.Device.HasDeviceId)
15-
return AccessDecisionResult.Deny("DeviceId is required for this operation.");
15+
return ValueTask.FromResult(AccessDecisionResult.Deny("DeviceId is required for this operation."));
1616

17-
return AccessDecisionResult.Allow();
17+
return ValueTask.FromResult(AccessDecisionResult.Allow());
1818
}
1919

2020
private static bool RequiresDevice(AuthContext context)

‎src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/ExpiredSessionInvariant.cs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,21 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure;
66

77
public sealed class ExpiredSessionInvariant : IAuthorityInvariant
88
{
9-
public AccessDecisionResult Decide(AuthContext context)
9+
public ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, CancellationToken ct = default)
1010
{
1111
if (context.Operation == AuthOperation.Login)
12-
return AccessDecisionResult.Allow();
12+
return ValueTask.FromResult(AccessDecisionResult.Allow());
1313

1414
var session = context.Session;
1515

1616
if (session is null)
17-
return AccessDecisionResult.Allow();
17+
return ValueTask.FromResult(AccessDecisionResult.Allow());
1818

1919
if (session.State == SessionState.Expired)
2020
{
21-
return AccessDecisionResult.Deny("Session has expired.");
21+
return ValueTask.FromResult(AccessDecisionResult.Deny("Session has expired."));
2222
}
2323

24-
return AccessDecisionResult.Allow();
24+
return ValueTask.FromResult(AccessDecisionResult.Allow());
2525
}
2626
}

‎src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/InvalidOrRevokedSessionInvariant.cs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,25 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure;
66

77
public sealed class InvalidOrRevokedSessionInvariant : IAuthorityInvariant
88
{
9-
public AccessDecisionResult Decide(AuthContext context)
9+
public ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, CancellationToken ct = default)
1010
{
1111
if (context.Operation == AuthOperation.Login)
12-
return AccessDecisionResult.Allow();
12+
return ValueTask.FromResult(AccessDecisionResult.Allow());
1313

1414
var session = context.Session;
1515

1616
if (session is null)
17-
return AccessDecisionResult.Deny("Session is required for this operation.");
17+
return ValueTask.FromResult(AccessDecisionResult.Deny("Session is required for this operation."));
1818

1919
if (session.State == SessionState.Invalid ||
2020
session.State == SessionState.NotFound ||
2121
session.State == SessionState.Revoked ||
2222
session.State == SessionState.SecurityMismatch ||
2323
session.State == SessionState.DeviceMismatch)
2424
{
25-
return AccessDecisionResult.Deny($"Session state is invalid: {session.State}");
25+
return ValueTask.FromResult(AccessDecisionResult.Deny($"Session state is invalid: {session.State}"));
2626
}
2727

28-
return AccessDecisionResult.Allow();
28+
return ValueTask.FromResult(AccessDecisionResult.Allow());
2929
}
3030
}

‎src/CodeBeam.UltimateAuth.Core/Infrastructure/Authority/TenantResolvedInvariant.cs‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,14 @@ namespace CodeBeam.UltimateAuth.Core.Infrastructure;
55

66
public sealed class TenantResolvedInvariant : IAuthorityInvariant
77
{
8-
public AccessDecisionResult Decide(AuthContext context)
8+
public ValueTask<AccessDecisionResult> DecideAsync(AuthContext context, CancellationToken ct = default)
99
{
10-
if (context.Tenant.IsUnresolved)
11-
{
12-
return AccessDecisionResult.Deny("Tenant is not resolved.");
13-
}
10+
ct.ThrowIfCancellationRequested();
1411

15-
return AccessDecisionResult.Allow();
12+
var result = context.Tenant.IsUnresolved
13+
? AccessDecisionResult.Deny("Tenant is not resolved.")
14+
: AccessDecisionResult.Allow();
15+
16+
return ValueTask.FromResult(result);
1617
}
1718
}

‎src/CodeBeam.UltimateAuth.Server/Infrastructure/Orchestrator/UAuthSessionOrchestrator.cs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public async Task<TResult> ExecuteAsync<TResult>(AuthContext authContext, ISessi
2323

2424
_executed = true;
2525

26-
var decision = _authority.Decide(authContext);
26+
var decision = await _authority.DecideAsync(authContext, ct: ct);
2727

2828
switch (decision.Decision)
2929
{

‎tests/CodeBeam.UltimateAuth.Tests.Integration/AuthorizationManagementTests.cs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,7 @@ public async Task AssignRole_TargetMismatch_ShouldRejectWithoutMutation()
3333
var target = await factory.CreateLoginUserAsync();
3434
var other = await factory.CreateLoginUserAsync();
3535

36-
await GrantAsync(factory, admin,
37-
UAuthActions.Authorization.Roles.AssignAdmin);
36+
await GrantAsync(factory, admin, UAuthActions.Authorization.Roles.AssignAdmin);
3837

3938
var role = await SeedRoleAsync(factory);
4039

0 commit comments

Comments
 (0)