Skip to content

Commit 36e7f9c

Browse files
committed
Finalize Server Project's Layers
1 parent 7978411 commit 36e7f9c

29 files changed

+474
-62
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using CodeBeam.UltimateAuth.Core.Contracts;
2+
3+
namespace CodeBeam.UltimateAuth.Core.Abstractions
4+
{
5+
public interface IRefreshTokenResolver<TUserId>
6+
{
7+
Task<ResolvedRefreshSession<TUserId>?> ResolveAsync(string? tenantId, string refreshToken, DateTimeOffset now, CancellationToken ct = default);
8+
}
9+
10+
}

src/CodeBeam.UltimateAuth.Core/Abstractions/Stores/ITokenStore.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using CodeBeam.UltimateAuth.Core.Domain;
1+
using CodeBeam.UltimateAuth.Core.Contracts;
2+
using CodeBeam.UltimateAuth.Core.Domain;
23

34
namespace CodeBeam.UltimateAuth.Core.Abstractions
45
{
@@ -22,11 +23,11 @@ Task StoreRefreshTokenAsync(
2223
/// Validates a provided refresh token against the stored hash.
2324
/// Returns true if valid and not expired or revoked.
2425
/// </summary>
25-
Task<bool> ValidateRefreshTokenAsync(
26+
Task<RefreshTokenValidationResult<TUserId>> ValidateRefreshTokenAsync(
2627
string? tenantId,
27-
TUserId userId,
28-
AuthSessionId sessionId,
29-
string providedRefreshToken);
28+
string providedRefreshToken,
29+
DateTimeOffset now);
30+
3031

3132
/// <summary>
3233
/// Revokes the refresh token associated with the specified session.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace CodeBeam.UltimateAuth.Core.Abstractions
2+
{
3+
public interface ITokenStoreFactory
4+
{
5+
ITokenStoreKernel Create(string? tenantId);
6+
}
7+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using CodeBeam.UltimateAuth.Core.Domain;
2+
3+
namespace CodeBeam.UltimateAuth.Core.Abstractions
4+
{
5+
/// <summary>
6+
/// Low-level persistence abstraction for token-related data.
7+
/// Handles refresh tokens and optional access token identifiers (jti).
8+
/// </summary>
9+
public interface ITokenStoreKernel
10+
{
11+
Task SaveRefreshTokenAsync(
12+
string? tenantId,
13+
StoredRefreshToken token);
14+
15+
Task<StoredRefreshToken?> GetRefreshTokenAsync(
16+
string? tenantId,
17+
string tokenHash);
18+
19+
Task RevokeRefreshTokenAsync(
20+
string? tenantId,
21+
string tokenHash,
22+
DateTimeOffset at);
23+
24+
Task RevokeAllRefreshTokensAsync(
25+
string? tenantId,
26+
string? userId,
27+
DateTimeOffset at);
28+
29+
Task DeleteExpiredRefreshTokensAsync(
30+
string? tenantId,
31+
DateTimeOffset now);
32+
33+
Task StoreTokenIdAsync(
34+
string? tenantId,
35+
string jti,
36+
DateTimeOffset expiresAt);
37+
38+
Task<bool> IsTokenIdRevokedAsync(
39+
string? tenantId,
40+
string jti);
41+
42+
Task RevokeTokenIdAsync(
43+
string? tenantId,
44+
string jti,
45+
DateTimeOffset at);
46+
}
47+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using CodeBeam.UltimateAuth.Core.Domain;
2+
3+
namespace CodeBeam.UltimateAuth.Core.Contracts
4+
{
5+
public sealed record ResolvedRefreshSession<TUserId>
6+
{
7+
public bool IsValid { get; init; }
8+
public bool IsReuseDetected { get; init; }
9+
10+
public ISession<TUserId>? Session { get; init; }
11+
public ISessionChain<TUserId>? Chain { get; init; }
12+
13+
private ResolvedRefreshSession() { }
14+
15+
public static ResolvedRefreshSession<TUserId> Invalid()
16+
=> new()
17+
{
18+
IsValid = false
19+
};
20+
21+
public static ResolvedRefreshSession<TUserId> Reused()
22+
=> new()
23+
{
24+
IsValid = false,
25+
IsReuseDetected = true
26+
};
27+
28+
public static ResolvedRefreshSession<TUserId> Valid(
29+
ISession<TUserId> session,
30+
ISessionChain<TUserId> chain)
31+
=> new()
32+
{
33+
IsValid = true,
34+
Session = session,
35+
Chain = chain
36+
};
37+
}
38+
}
Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
using CodeBeam.UltimateAuth.Core.Contracts;
2-
3-
namespace CodeBeam.UltimateAuth.Core.Contracts
1+
namespace CodeBeam.UltimateAuth.Core.Contracts
42
{
53
public sealed record SessionRefreshResult
64
{
75
public AccessToken AccessToken { get; init; } = default!;
86
public RefreshToken? RefreshToken { get; init; }
7+
8+
public bool IsValid => AccessToken is not null;
9+
10+
private SessionRefreshResult() { }
11+
12+
public static SessionRefreshResult Success(AccessToken accessToken, RefreshToken? refreshToken)
13+
=> new()
14+
{
15+
AccessToken = accessToken,
16+
RefreshToken = refreshToken
17+
};
18+
19+
public static SessionRefreshResult Invalid() => new();
920
}
1021
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace CodeBeam.UltimateAuth.Core.Contracts
2+
{
3+
public enum RefreshTokenFailureReason
4+
{
5+
Invalid,
6+
Expired,
7+
Revoked,
8+
Reused
9+
}
10+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using CodeBeam.UltimateAuth.Core.Domain;
2+
3+
namespace CodeBeam.UltimateAuth.Core.Contracts
4+
{
5+
public sealed record RefreshTokenValidationResult<TUserId>
6+
{
7+
public bool IsValid { get; init; }
8+
9+
public TUserId? UserId { get; init; }
10+
11+
public AuthSessionId? SessionId { get; init; }
12+
13+
private RefreshTokenValidationResult() { }
14+
15+
public static RefreshTokenValidationResult<TUserId> Invalid()
16+
=> new()
17+
{
18+
IsValid = false
19+
};
20+
21+
public static RefreshTokenValidationResult<TUserId> Valid(
22+
TUserId userId,
23+
AuthSessionId sessionId)
24+
=> new()
25+
{
26+
IsValid = true,
27+
UserId = userId,
28+
SessionId = sessionId
29+
};
30+
}
31+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
namespace CodeBeam.UltimateAuth.Core.Domain
2+
{
3+
/// <summary>
4+
/// Represents a persisted refresh token bound to a session.
5+
/// Stored as a hashed value for security reasons.
6+
/// </summary>
7+
public sealed record StoredRefreshToken
8+
{
9+
public string TokenHash { get; init; } = default!;
10+
11+
public AuthSessionId SessionId { get; init; } = default!;
12+
13+
public DateTimeOffset ExpiresAt { get; init; }
14+
15+
public DateTimeOffset? RevokedAt { get; init; }
16+
17+
public bool IsRevoked => RevokedAt.HasValue;
18+
}
19+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using CodeBeam.UltimateAuth.Core.Abstractions;
2+
using CodeBeam.UltimateAuth.Core.Contracts;
3+
4+
namespace CodeBeam.UltimateAuth.Core.Infrastructure
5+
{
6+
internal sealed class StoreRefreshTokenResolver<TUserId>
7+
: IRefreshTokenResolver<TUserId>
8+
{
9+
private readonly ISessionStoreFactory _sessionStoreFactory;
10+
private readonly ITokenStoreFactory _tokenStoreFactory;
11+
private readonly ITokenHasher _hasher;
12+
13+
public StoreRefreshTokenResolver(
14+
ISessionStoreFactory sessionStoreFactory,
15+
ITokenStoreFactory tokenStoreFactory,
16+
ITokenHasher hasher)
17+
{
18+
_sessionStoreFactory = sessionStoreFactory;
19+
_tokenStoreFactory = tokenStoreFactory;
20+
_hasher = hasher;
21+
}
22+
23+
public async Task<ResolvedRefreshSession<TUserId>?> ResolveAsync(
24+
string? tenantId,
25+
string refreshToken,
26+
DateTimeOffset now,
27+
CancellationToken ct = default)
28+
{
29+
var tokenHash = _hasher.Hash(refreshToken);
30+
31+
var tokenStore = _tokenStoreFactory.Create(tenantId);
32+
var sessionStore = _sessionStoreFactory.Create<TUserId>(tenantId);
33+
34+
var stored = await tokenStore.GetRefreshTokenAsync(
35+
tenantId,
36+
tokenHash);
37+
38+
if (stored is null)
39+
return null;
40+
41+
if (stored.IsRevoked)
42+
{
43+
return ResolvedRefreshSession<TUserId>.Reused();
44+
}
45+
46+
if (stored.ExpiresAt <= now)
47+
{
48+
await tokenStore.RevokeRefreshTokenAsync(
49+
tenantId,
50+
tokenHash,
51+
now);
52+
53+
return ResolvedRefreshSession<TUserId>.Invalid();
54+
}
55+
56+
var session = await sessionStore.GetSessionAsync(
57+
tenantId,
58+
stored.SessionId);
59+
60+
if (session is null)
61+
return null;
62+
63+
if (session.IsRevoked || session.ExpiresAt <= now)
64+
return null;
65+
66+
var chain = await sessionStore.GetChainAsync(
67+
tenantId,
68+
session.ChainId);
69+
70+
if (chain is null || chain.IsRevoked)
71+
return null;
72+
73+
await tokenStore.RevokeRefreshTokenAsync(
74+
tenantId,
75+
tokenHash,
76+
now);
77+
78+
return ResolvedRefreshSession<TUserId>.Valid(
79+
session,
80+
chain);
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)