Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion architecture.html

Large diffs are not rendered by default.

35 changes: 30 additions & 5 deletions architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -2939,6 +2939,10 @@
"entity": "UserSession",
"file": "src/Orbit.Domain/Entities/UserSession.cs"
},
{
"entity": "UserSessionRefreshToken",
"file": "src/Orbit.Domain/Entities/UserSessionRefreshToken.cs"
},
{
"entity": "XpAwardLog",
"file": "src/Orbit.Domain/Entities/XpAwardLog.cs"
Expand Down Expand Up @@ -5574,7 +5578,8 @@
"file": "tests/Orbit.Application.Tests/Services/AuthSessionServiceTests.cs",
"references": [
"User",
"UserSession"
"UserSession",
"UserSessionRefreshToken"
]
},
{
Expand Down Expand Up @@ -6746,6 +6751,13 @@
"User"
]
},
{
"testClass": "UserSessionRefreshTokenTests",
"file": "tests/Orbit.Domain.Tests/Entities/UserSessionRefreshTokenTests.cs",
"references": [
"UserSessionRefreshToken"
]
},
{
"testClass": "UserSessionTests",
"file": "tests/Orbit.Domain.Tests/Entities/UserSessionTests.cs",
Expand Down Expand Up @@ -7767,31 +7779,44 @@
"file": "tests/Orbit.Infrastructure.Tests/Services/AuthSessionServiceConcurrentRefreshTests.cs",
"references": [
"User",
"UserSession"
"UserSession",
"UserSessionRefreshToken"
]
},
{
"testClass": "AuthSessionServiceHasSessionForTokenTests",
"file": "tests/Orbit.Infrastructure.Tests/Services/AuthSessionServiceHasSessionForTokenTests.cs",
"references": [
"User",
"UserSession"
"UserSession",
"UserSessionRefreshToken"
]
},
{
"testClass": "AuthSessionServiceLogoutRaceTests",
"file": "tests/Orbit.Infrastructure.Tests/Services/AuthSessionServiceLogoutRaceTests.cs",
"references": [
"User",
"UserSession",
"UserSessionRefreshToken"
]
},
{
"testClass": "AuthSessionServiceRevokeAllTests",
"file": "tests/Orbit.Infrastructure.Tests/Services/AuthSessionServiceRevokeAllTests.cs",
"references": [
"User",
"UserSession"
"UserSession",
"UserSessionRefreshToken"
]
},
{
"testClass": "AuthSessionServiceTests",
"file": "tests/Orbit.Infrastructure.Tests/Services/AuthSessionServiceTests.cs",
"references": [
"User",
"UserSession"
"UserSession",
"UserSessionRefreshToken"
]
},
{
Expand Down
19 changes: 19 additions & 0 deletions src/Orbit.Api/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System.Security.Claims;
using FluentValidation;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
Expand Down Expand Up @@ -233,6 +234,24 @@ public static WebApplicationBuilder AddOrbitAuthentication(this WebApplicationBu
Encoding.UTF8.GetBytes(jwtSettings.SecretKey)),
ClockSkew = TimeSpan.Zero
};
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var sessionClaim = context.Principal?.FindFirst("orbit_session_id")?.Value;
if (sessionClaim is null)
// https://github.com/thomasluizon/orbit-api/issues/660: Pre-deploy tokens remain valid for 168 hours; #660 removes this allowance.
return;

var userClaim = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (!Guid.TryParse(sessionClaim, out var sessionId)
|| !Guid.TryParse(userClaim, out var userId)
|| !await context.HttpContext.RequestServices
.GetRequiredService<IAuthSessionService>()
.IsSessionActiveAsync(sessionId, userId, context.HttpContext.RequestAborted))
context.Fail("Session is no longer active.");
}
};
})
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>("ApiKey", null)
.AddPolicyScheme("MultiScheme", "JWT or API Key", options =>
Expand Down
26 changes: 26 additions & 0 deletions src/Orbit.Domain/Entities/UserSessionRefreshToken.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Orbit.Domain.Common;

namespace Orbit.Domain.Entities;

public class UserSessionRefreshToken : Entity
{
public Guid UserSessionId { get; private set; }
public string TokenHash { get; private set; } = null!;

private UserSessionRefreshToken() { }

public static Result<UserSessionRefreshToken> Create(Guid userSessionId, string tokenHash)
{
if (userSessionId == Guid.Empty)
return Result.Failure<UserSessionRefreshToken>(DomainErrors.SessionNotActive);

if (string.IsNullOrWhiteSpace(tokenHash))
return Result.Failure<UserSessionRefreshToken>(DomainErrors.TokenHashRequired);

return Result.Success(new UserSessionRefreshToken
{
UserSessionId = userSessionId,
TokenHash = tokenHash
});
}
}
1 change: 1 addition & 0 deletions src/Orbit.Domain/Interfaces/IAuthSessionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ public interface IAuthSessionService
/// throttled per source IP, closing the token-varying rate-limit bypass.
/// </summary>
Task<bool> HasSessionForTokenAsync(string refreshToken, CancellationToken cancellationToken = default);
Task<bool> IsSessionActiveAsync(Guid sessionId, Guid userId, CancellationToken cancellationToken = default);
}
2 changes: 1 addition & 1 deletion src/Orbit.Domain/Interfaces/ITokenService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ namespace Orbit.Domain.Interfaces;

public interface ITokenService
{
string GenerateToken(Guid userId, string email);
string GenerateToken(Guid userId, string email, Guid sessionId);
}
Loading
Loading