Skip to content

Commit df96dbf

Browse files
committed
Create HappyPath
1 parent cdcb780 commit df96dbf

File tree

20 files changed

+648
-28
lines changed

20 files changed

+648
-28
lines changed

UltimateAuth.slnx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33
<File Path="Readme.md" />
44
<File Path="Roadmap.md" />
55
</Folder>
6-
<Project Path="src/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Server.Users.csproj" Id="30d5db36-6dc8-46f6-9139-8b6b3d6053d5" />
76
<Project Path="src/CodeBeam.UltimateAuth.Client/CodeBeam.UltimateAuth.Client.csproj" Id="eb60a3b7-ba9d-48c9-98ad-b28e879b23bf" />
87
<Project Path="src/CodeBeam.UltimateAuth.Core/CodeBeam.UltimateAuth.Core.csproj" />
98
<Project Path="src/CodeBeam.UltimateAuth.Server/CodeBeam.UltimateAuth.Server.csproj" Id="0a8cdd12-a8c4-4530-87e8-ae778c46322b" />
9+
<Project Path="src/CodeBeam.UltimateAuth.Users/CodeBeam.UltimateAuth.Server.Users.csproj" Id="30d5db36-6dc8-46f6-9139-8b6b3d6053d5" />
10+
<Project Path="src/security/CodeBeam.UltimateAuth.Security.Argon2/CodeBeam.UltimateAuth.Security.Argon2.csproj" Id="6abfb7a6-ea36-42db-a843-38054dd40fd8" />
11+
<Project Path="src/sessions/CodeBeam.UltimateAuth.Sessions.InMemory/CodeBeam.UltimateAuth.Sessions.InMemory.csproj" Id="fc9bfef0-8a89-4639-81ee-3f84f6e33816" />
12+
<Project Path="src/tokens/CodeBeam.UltimateAuth.Tokens.InMemory/CodeBeam.UltimateAuth.Tokens.InMemory.csproj" Id="8220884e-4958-4b49-8c69-56ce9d2b6c6f" />
1013
</Solution>
Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,36 @@
1-
namespace CodeBeam.UltimateAuth.Core.Contracts
1+
using CodeBeam.UltimateAuth.Core.Domain;
2+
3+
namespace CodeBeam.UltimateAuth.Core.Contracts
24
{
35
public sealed record SessionRefreshResult
46
{
5-
public AccessToken AccessToken { get; init; } = default!;
7+
public SessionRefreshStatus Status { get; init; }
8+
9+
public AccessToken? AccessToken { get; init; }
10+
611
public RefreshToken? RefreshToken { get; init; }
712

8-
public bool IsValid => AccessToken is not null;
13+
public bool IsSuccess => Status == SessionRefreshStatus.Success;
914

1015
private SessionRefreshResult() { }
1116

12-
public static SessionRefreshResult Success(AccessToken accessToken, RefreshToken? refreshToken)
17+
public static SessionRefreshResult Success(
18+
AccessToken accessToken,
19+
RefreshToken? refreshToken)
1320
=> new()
1421
{
22+
Status = SessionRefreshStatus.Success,
1523
AccessToken = accessToken,
1624
RefreshToken = refreshToken
1725
};
1826

27+
public static SessionRefreshResult ReauthRequired()
28+
=> new()
29+
{
30+
Status = SessionRefreshStatus.ReauthRequired
31+
};
32+
33+
// TODO: ?
1934
public static SessionRefreshResult Invalid() => new();
2035
}
2136
}

src/CodeBeam.UltimateAuth.Core/Contracts/Token/RefreshTokenValidationResult.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,30 @@ public sealed record RefreshTokenValidationResult<TUserId>
66
{
77
public bool IsValid { get; init; }
88

9+
public bool IsReuseDetected { get; init; }
10+
911
public TUserId? UserId { get; init; }
1012

1113
public AuthSessionId? SessionId { get; init; }
1214

1315
private RefreshTokenValidationResult() { }
1416

17+
// ----------------------------
18+
// FACTORIES
19+
// ----------------------------
20+
1521
public static RefreshTokenValidationResult<TUserId> Invalid()
1622
=> new()
1723
{
18-
IsValid = false
24+
IsValid = false,
25+
IsReuseDetected = false
26+
};
27+
28+
public static RefreshTokenValidationResult<TUserId> ReuseDetected()
29+
=> new()
30+
{
31+
IsValid = false,
32+
IsReuseDetected = true
1933
};
2034

2135
public static RefreshTokenValidationResult<TUserId> Valid(
@@ -24,6 +38,7 @@ public static RefreshTokenValidationResult<TUserId> Valid(
2438
=> new()
2539
{
2640
IsValid = true,
41+
IsReuseDetected = false,
2742
UserId = userId,
2843
SessionId = sessionId
2944
};

src/CodeBeam.UltimateAuth.Core/Domain/Session/ISession.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public interface ISession<TUserId>
7878

7979
bool ShouldUpdateLastSeen(DateTimeOffset now);
8080
ISession<TUserId> Touch(DateTimeOffset now);
81+
ISession<TUserId> Revoke(DateTimeOffset at);
8182

8283
}
8384
}

src/CodeBeam.UltimateAuth.Core/Domain/Session/UAuthSession.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,11 +126,11 @@ public ISession<TUserId> Touch(DateTimeOffset at)
126126
);
127127
}
128128

129-
public UAuthSession<TUserId> Revoke(DateTimeOffset at)
129+
public ISession<TUserId> Revoke(DateTimeOffset at)
130130
{
131131
if (IsRevoked) return this;
132132

133-
return new(
133+
return new UAuthSession<TUserId>(
134134
SessionId,
135135
TenantId,
136136
UserId,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace CodeBeam.UltimateAuth.Core.Domain
2+
{
3+
public enum SessionRefreshStatus
4+
{
5+
Success,
6+
ReauthRequired
7+
}
8+
}

src/CodeBeam.UltimateAuth.Server/Services/UAuthFlowService.cs

Lines changed: 44 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using CodeBeam.UltimateAuth.Core.Contracts;
33
using CodeBeam.UltimateAuth.Core.Domain;
44
using CodeBeam.UltimateAuth.Server.Infrastructure;
5+
using CodeBeam.UltimateAuth.Server.Infrastructure.Orchestrator;
56
using Microsoft.AspNetCore.Http;
67

78
namespace CodeBeam.UltimateAuth.Server.Services
@@ -12,19 +13,22 @@ internal sealed class UAuthFlowService<TUserId> : IUAuthFlowService
1213
private readonly ISessionOrchestrator<TUserId> _orchestrator;
1314
private readonly ISessionQueryService<TUserId> _queries;
1415
private readonly ITokenIssuer _tokens;
16+
private readonly ITokenStore<TUserId> _tokenStore;
1517
private readonly IRefreshTokenResolver<TUserId> _refreshTokens;
1618

1719
public UAuthFlowService(
1820
IUAuthUserService<TUserId> users,
1921
ISessionOrchestrator<TUserId> orchestrator,
2022
ISessionQueryService<TUserId> queries,
2123
ITokenIssuer tokens,
24+
ITokenStore<TUserId> tokenStore,
2225
IRefreshTokenResolver<TUserId> refreshTokens)
2326
{
2427
_users = users;
2528
_orchestrator = orchestrator;
2629
_queries = queries;
2730
_tokens = tokens;
31+
_tokenStore = tokenStore;
2832
_refreshTokens = refreshTokens;
2933
}
3034

@@ -181,43 +185,63 @@ public Task<ReauthResult> ReauthenticateAsync(ReauthRequest request, Cancellatio
181185
public async Task<SessionRefreshResult> RefreshSessionAsync(SessionRefreshRequest request, CancellationToken ct = default)
182186
{
183187
var now = DateTimeOffset.UtcNow;
184-
var resolved = await _refreshTokens.ResolveAsync(request.TenantId, request.RefreshToken, now, ct);
185188

186-
if (resolved is null)
187-
return SessionRefreshResult.Invalid();
189+
// Validate refresh token (STORE is authority)
190+
var validation = await _tokenStore.ValidateRefreshTokenAsync(
191+
request.TenantId,
192+
request.RefreshToken,
193+
now);
188194

189-
if (!resolved.IsValid)
195+
if (!validation.IsValid)
190196
{
191-
// TODO: Add reuse detection handling here
192-
//if (resolved.IsReuseDetected)
193-
//{
194-
// await _sessions.RevokeChainAsync(
195-
// tenantId,
196-
// resolved.Chain!.ChainId,
197-
// now);
198-
//}
199-
200-
//return SessionRefreshResult.ReauthRequired();
197+
if (validation.IsReuseDetected && validation.SessionId is not null)
198+
{
199+
var chainId = await _queries.ResolveChainIdAsync(
200+
request.TenantId,
201+
validation.SessionId.Value,
202+
ct);
203+
204+
if (chainId is not null)
205+
{
206+
var authContext = AuthContext.System(
207+
request.TenantId,
208+
AuthOperation.Revoke,
209+
now);
210+
211+
await _orchestrator.ExecuteAsync(
212+
authContext,
213+
new RevokeChainCommand<TUserId>(chainId.Value),
214+
ct);
215+
}
216+
}
217+
218+
return SessionRefreshResult.ReauthRequired();
201219
}
202220

203-
var session = resolved.Session;
221+
var session = await _queries.GetSessionAsync(request.TenantId, validation.SessionId!.Value);
222+
223+
if (session is null)
224+
return SessionRefreshResult.ReauthRequired();
204225

205226
var rotationContext = new SessionRotationContext<TUserId>
206227
{
207228
TenantId = request.TenantId,
208-
CurrentSessionId = session.SessionId,
209-
UserId = session.UserId,
229+
CurrentSessionId = validation.SessionId!.Value,
230+
UserId = validation.UserId!,
210231
Now = now
211232
};
212233

213-
var authContext = AuthContext.ForAuthenticatedUser(request.TenantId, AuthOperation.Refresh, now, DeviceContext.From(session.Device));
234+
var refreshAuthContext = AuthContext.ForAuthenticatedUser(request.TenantId, AuthOperation.Refresh, now, DeviceContext.From(session.Device));
214235

215-
var issuedSession = await _orchestrator.ExecuteAsync(authContext, new RotateSessionCommand<TUserId>(rotationContext), ct);
236+
var issuedSession = await _orchestrator.ExecuteAsync(
237+
refreshAuthContext,
238+
new RotateSessionCommand<TUserId>(rotationContext),
239+
ct);
216240

217241
var tokenContext = new TokenIssuanceContext
218242
{
219243
TenantId = request.TenantId,
220-
UserId = session.UserId!.ToString()!,
244+
UserId = validation.UserId!.ToString()!,
221245
SessionId = issuedSession.Session.SessionId
222246
};
223247

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace CodeBeam.UltimateAuth.Security.Argon2
2+
{
3+
public sealed class Argon2Options
4+
{
5+
// OWASP recommended baseline
6+
public int MemorySizeKb { get; init; } = 64 * 1024; // 64 MB
7+
public int Iterations { get; init; } = 3;
8+
public int Parallelism { get; init; } = Environment.ProcessorCount;
9+
10+
public int SaltSize { get; init; } = 16;
11+
public int HashSize { get; init; } = 32;
12+
}
13+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
using System.Security.Cryptography;
2+
using System.Text;
3+
using CodeBeam.UltimateAuth.Core.Abstractions;
4+
using Konscious.Security.Cryptography;
5+
6+
namespace CodeBeam.UltimateAuth.Security.Argon2
7+
{
8+
public sealed class Argon2PasswordHasher : IUAuthPasswordHasher
9+
{
10+
private readonly Argon2Options _options;
11+
12+
public Argon2PasswordHasher(Argon2Options options)
13+
{
14+
_options = options;
15+
}
16+
17+
public string Hash(string password)
18+
{
19+
if (string.IsNullOrEmpty(password))
20+
throw new ArgumentException("Password cannot be null or empty.", nameof(password));
21+
22+
var salt = RandomNumberGenerator.GetBytes(_options.SaltSize);
23+
24+
var argon2 = CreateArgon2(password, salt);
25+
26+
var hash = argon2.GetBytes(_options.HashSize);
27+
28+
// format:
29+
// {salt}.{hash}
30+
return $"{Convert.ToBase64String(salt)}.{Convert.ToBase64String(hash)}";
31+
}
32+
33+
public bool Verify(string password, string hash)
34+
{
35+
if (string.IsNullOrWhiteSpace(password) || string.IsNullOrWhiteSpace(hash))
36+
return false;
37+
38+
var parts = hash.Split('.');
39+
if (parts.Length != 2)
40+
return false;
41+
42+
var salt = Convert.FromBase64String(parts[0]);
43+
var expectedHash = Convert.FromBase64String(parts[1]);
44+
45+
var argon2 = CreateArgon2(password, salt);
46+
var actualHash = argon2.GetBytes(expectedHash.Length);
47+
48+
return CryptographicOperations.FixedTimeEquals(actualHash, expectedHash);
49+
}
50+
51+
private Argon2id CreateArgon2(string password, byte[] salt)
52+
{
53+
return new Argon2id(Encoding.UTF8.GetBytes(password))
54+
{
55+
Salt = salt,
56+
DegreeOfParallelism = _options.Parallelism,
57+
Iterations = _options.Iterations,
58+
MemorySize = _options.MemorySizeKb
59+
};
60+
}
61+
}
62+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<GenerateDocumentationFile>true</GenerateDocumentationFile>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Konscious.Security.Cryptography.Argon2" Version="1.3.1" />
12+
</ItemGroup>
13+
14+
<ItemGroup>
15+
<ProjectReference Include="..\..\CodeBeam.UltimateAuth.Core\CodeBeam.UltimateAuth.Core.csproj" />
16+
</ItemGroup>
17+
18+
</Project>

0 commit comments

Comments
 (0)