Skip to content
2 changes: 1 addition & 1 deletion API/Controller/Account/Authenticated/ChangePassword.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public sealed partial class AuthenticatedAccountController
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<IActionResult> ChangePassword(ChangePasswordRequest data)
{
if (!PasswordHashingUtils.VerifyPassword(data.OldPassword, CurrentUser.PasswordHash).Verified)
if (!HashingUtils.VerifyPassword(data.OldPassword, CurrentUser.PasswordHash).Verified)
{
return Problem(AccountError.PasswordChangeInvalidPassword);
}
Expand Down
4 changes: 2 additions & 2 deletions API/Controller/Account/Logout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ public async Task<IActionResult> Logout(
var config = options.Value;

// Remove session if valid
if (HttpContext.TryGetUserSession(out var sessionCookie))
if (HttpContext.TryGetUserSessionToken(out var sessionToken))
{
await sessionService.DeleteSessionById(sessionCookie);
await sessionService.DeleteSessionByToken(sessionToken);
}

// Make sure cookie is removed, no matter if authenticated or not
Expand Down
2 changes: 1 addition & 1 deletion API/Controller/Sessions/DeleteSessions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public sealed partial class SessionsController
[ProducesResponseType<OpenShockProblem>(StatusCodes.Status404NotFound, MediaTypeNames.Application.ProblemJson)] // SessionNotFound
public async Task<IActionResult> DeleteSession(Guid sessionId)
{
var loginSession = await _sessionService.GetSessionByPulbicId(sessionId);
var loginSession = await _sessionService.GetSessionById(sessionId);

// If the session was not found, or the user does not have the privledges to access it, return NotFound
if (loginSession == null || !CurrentUser.IsUserOrRole(loginSession.UserId, RoleType.Admin))
Expand Down
4 changes: 2 additions & 2 deletions API/Controller/Tokens/TokenController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,12 @@ public async Task<IActionResult> GetTokenById([FromRoute] Guid tokenId)
[ProducesResponseType<TokenCreatedResponse>(StatusCodes.Status200OK, MediaTypeNames.Application.Json)]
public async Task<TokenCreatedResponse> CreateToken([FromBody] CreateTokenRequest body)
{
string token = CryptoUtils.RandomString(HardLimits.ApiKeyTokenLength);
string token = CryptoUtils.RandomString(AuthConstants.GeneratedTokenLength);

var tokenDto = new ApiToken
{
UserId = CurrentUser.Id,
TokenHash = HashingUtils.HashSha256(token),
TokenHash = HashingUtils.HashToken(token),
CreatedByIp = HttpContext.GetRemoteIP(),
Permissions = body.Permissions.Distinct().ToList(),
Id = Guid.CreateVersion7(),
Expand Down
42 changes: 21 additions & 21 deletions API/Services/Account/AccountService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using BCrypt.Net;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using OneOf;
using OneOf.Types;
Expand All @@ -19,8 +18,6 @@ namespace OpenShock.API.Services.Account;
/// </summary>
public sealed class AccountService : IAccountService
{
private const HashType HashAlgo = HashType.SHA512;

private readonly OpenShockContext _db;
private readonly IEmailService _emailService;
private readonly ISessionService _sessionService;
Expand Down Expand Up @@ -65,7 +62,7 @@ private async Task<OneOf<Success<User>, AccountWithEmailOrUsernameExists>> Creat
Id = newGuid,
Name = username,
Email = email.ToLowerInvariant(),
PasswordHash = PasswordHashingUtils.HashPassword(password),
PasswordHash = HashingUtils.HashPassword(password),
EmailActivated = emailActivated,
Roles = []
};
Expand All @@ -86,14 +83,14 @@ public async Task<OneOf<Success<User>, AccountWithEmailOrUsernameExists>> Signup
var user = accountCreate.AsT0.Value;

var id = Guid.CreateVersion7();
var secret = CryptoUtils.RandomString(32);
var secretHash = BCrypt.Net.BCrypt.EnhancedHashPassword(secret, HashAlgo);
var secret = CryptoUtils.RandomString(AuthConstants.GeneratedTokenLength);
var secretHash = HashingUtils.HashToken(secret);

_db.UsersActivations.Add(new UsersActivation()
{
Id = id,
UserId = user.Id,
Secret = secretHash
SecretHash = secretHash
});

await _db.SaveChangesAsync();
Expand All @@ -120,11 +117,9 @@ await Task.Delay(100,

if (!await CheckPassword(password, user)) return new NotFound();

var randomSessionId = CryptoUtils.RandomString(64);

await _sessionService.CreateSessionAsync(randomSessionId, user.Id, loginContext.UserAgent, loginContext.Ip);
var createdSession = await _sessionService.CreateSessionAsync(user.Id, loginContext.UserAgent, loginContext.Ip);

return new Success<string>(randomSessionId);
return new Success<string>(createdSession.Token);
}

/// <inheritdoc />
Expand All @@ -137,7 +132,10 @@ public async Task<OneOf<Success, NotFound, SecretInvalid>> PasswordResetExists(G
cancellationToken: cancellationToken);

if (reset == null) return new NotFound();
if (!BCrypt.Net.BCrypt.EnhancedVerify(secret, reset.Secret, HashAlgo)) return new SecretInvalid();

var result = HashingUtils.VerifyToken(secret, reset.SecretHash);
if (!result.Verified) return new SecretInvalid();

return new Success();
}

Expand All @@ -154,12 +152,12 @@ public async Task<OneOf<Success, TooManyPasswordResets, NotFound>> CreatePasswor
if (user == null) return new NotFound();
if (user.PasswordResetCount >= 3) return new TooManyPasswordResets();

var secret = CryptoUtils.RandomString(32);
var hash = BCrypt.Net.BCrypt.EnhancedHashPassword(secret, HashAlgo);
var secret = CryptoUtils.RandomString(AuthConstants.GeneratedTokenLength);
var secretHash = HashingUtils.HashToken(secret);
var passwordReset = new PasswordReset
{
Id = Guid.CreateVersion7(),
Secret = hash,
SecretHash = secretHash,
User = user.User
};
_db.PasswordResets.Add(passwordReset);
Expand All @@ -181,10 +179,12 @@ public async Task<OneOf<Success, NotFound, SecretInvalid>> PasswordResetComplete
x.Id == passwordResetId && x.UsedOn == null && x.CreatedOn < validUntil);

if (reset == null) return new NotFound();
if (!BCrypt.Net.BCrypt.EnhancedVerify(secret, reset.Secret, HashAlgo)) return new SecretInvalid();

var result = HashingUtils.VerifyToken(secret, reset.SecretHash);
if (!result.Verified) return new SecretInvalid();

reset.UsedOn = DateTime.UtcNow;
reset.User.PasswordHash = PasswordHashingUtils.HashPassword(newPassword);
reset.User.PasswordHash = HashingUtils.HashPassword(newPassword);
await _db.SaveChangesAsync();
return new Success();
}
Expand Down Expand Up @@ -248,7 +248,7 @@ public async Task<OneOf<Success, UsernameTaken, UsernameError>> CheckUsernameAva
public async Task<OneOf<Success, NotFound>> ChangePassword(Guid userId, string newPassword)
{
var user = await _db.Users.Where(x => x.Id == userId).ExecuteUpdateAsync(calls =>
calls.SetProperty(x => x.PasswordHash, PasswordHashingUtils.HashPassword(newPassword)));
calls.SetProperty(x => x.PasswordHash, HashingUtils.HashPassword(newPassword)));
return user switch
{
<= 0 => new NotFound(),
Expand All @@ -260,7 +260,7 @@ public async Task<OneOf<Success, NotFound>> ChangePassword(Guid userId, string n

private async Task<bool> CheckPassword(string password, User user)
{
var result = PasswordHashingUtils.VerifyPassword(password, user.PasswordHash);
var result = HashingUtils.VerifyPassword(password, user.PasswordHash);

if (!result.Verified)
{
Expand All @@ -271,7 +271,7 @@ private async Task<bool> CheckPassword(string password, User user)
if (result.NeedsRehash)
{
_logger.LogInformation("Rehashing password for user ID: [{Id}]", user.Id);
user.PasswordHash = PasswordHashingUtils.HashPassword(password);
user.PasswordHash = HashingUtils.HashPassword(password);
await _db.SaveChangesAsync();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
return Fail(AuthResultError.HeaderMissingOrInvalid);
}

var tokenHash = HashingUtils.HashSha256(token);
var tokenHash = HashingUtils.HashToken(token);

var tokenDto = await _db.ApiTokens.Include(x => x.User).FirstOrDefaultAsync(x => x.TokenHash == tokenHash &&
(x.ValidUntil == null || x.ValidUntil >= DateTime.UtcNow));
if (tokenDto == null) return Fail(AuthResultError.TokenInvalid);

_batchUpdateService.UpdateTokenLastUsed(tokenDto.Id);
_batchUpdateService.UpdateApiTokenLastUsed(tokenDto.Id);
_authService.CurrentClient = tokenDto.User;
_userReferenceService.AuthReference = tokenDto;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,12 @@ public UserSessionAuthentication(

protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.TryGetUserSession(out var sessionKey))
if (!Context.TryGetUserSessionToken(out var sessionToken))
{
return Fail(AuthResultError.CookieMissingOrInvalid);
}

var session = await _sessionService.GetSessionById(sessionKey);
var session = await _sessionService.GetSessionByToken(sessionToken);
if (session == null) return Fail(AuthResultError.SessionInvalid);

if (session.Expires!.Value < DateTime.UtcNow.Subtract(Duration.LoginSessionExpansionAfter))
Expand All @@ -67,7 +67,7 @@ protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
});
}

_batchUpdateService.UpdateSessionLastUsed(sessionKey, DateTimeOffset.UtcNow);
_batchUpdateService.UpdateSessionLastUsed(sessionToken, DateTimeOffset.UtcNow);

var retrievedUser = await _db.Users.FirstAsync(user => user.Id == session.UserId);

Expand Down
2 changes: 2 additions & 0 deletions Common/Constants/AuthConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ public static class AuthConstants
public const string UserSessionHeaderName = "OpenShockSession";
public const string ApiTokenHeaderName = "OpenShockToken";
public const string HubTokenHeaderName = "DeviceToken";

public const int GeneratedTokenLength = 32;
}
1 change: 0 additions & 1 deletion Common/Constants/HardLimits.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ public static class HardLimits
public const int UserAgentMaxLength = 1024;

public const int ApiKeyNameMaxLength = 64;
public const int ApiKeyTokenLength = 64;
public const int ApiKeyMaxPermissions = 256;

public const int HubNameMinLength = 1;
Expand Down
8 changes: 4 additions & 4 deletions Common/Hubs/ShareLinkHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ public override async Task OnConnectedAsync()

GenericIni? user = null;

if (httpContext.TryGetUserSession(out var sessionCookie))
if (httpContext.TryGetUserSessionToken(out var sessionToken))
{
user = await SessionAuth(sessionCookie);
user = await SessionAuth(sessionToken);
if (user == null)
{
_logger.LogDebug("Connection tried authentication with invalid user session cookie, terminating connection...");
Expand Down Expand Up @@ -132,9 +132,9 @@ public Task Control(IReadOnlyList<Models.WebSocket.User.Control> shocks)
private CustomDataHolder CustomData => (CustomDataHolder)Context.Items[ShareLinkCustomData]!;
private const string ShareLinkCustomData = "ShareLinkCustomData";

private async Task<GenericIni?> SessionAuth(string sessionKey)
private async Task<GenericIni?> SessionAuth(string sessionToken)
{
var session = await _sessionService.GetSessionById(sessionKey);
var session = await _sessionService.GetSessionByToken(sessionToken);
if (session == null) return null;

return await _db.Users.Select(x => new GenericIni
Expand Down
6 changes: 3 additions & 3 deletions Common/Migrations/OpenShockContextModelSnapshot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnName("created_on")
.HasDefaultValueSql("CURRENT_TIMESTAMP");

b.Property<string>("Secret")
b.Property<string>("SecretHash")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
Expand Down Expand Up @@ -739,7 +739,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnName("created_on")
.HasDefaultValueSql("CURRENT_TIMESTAMP");

b.Property<string>("Secret")
b.Property<string>("SecretHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
Expand Down Expand Up @@ -779,7 +779,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnType("character varying(320)")
.HasColumnName("email");

b.Property<string>("Secret")
b.Property<string>("SecretHash")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("character varying(128)")
Expand Down
6 changes: 3 additions & 3 deletions Common/OpenShockDb/OpenShockContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.CreatedOn)
.HasDefaultValueSql("CURRENT_TIMESTAMP")
.HasColumnName("created_on");
entity.Property(e => e.Secret)
entity.Property(e => e.SecretHash)
.VarCharWithLength(HardLimits.PasswordResetSecretMaxLength)
.HasColumnName("secret");
entity.Property(e => e.UsedOn)
Expand Down Expand Up @@ -546,7 +546,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.CreatedOn)
.HasDefaultValueSql("CURRENT_TIMESTAMP")
.HasColumnName("created_on");
entity.Property(e => e.Secret)
entity.Property(e => e.SecretHash)
.VarCharWithLength(HardLimits.UserActivationSecretMaxLength)
.HasColumnName("secret");
entity.Property(e => e.UsedOn).HasColumnName("used_on");
Expand Down Expand Up @@ -578,7 +578,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.Email)
.VarCharWithLength(HardLimits.EmailAddressMaxLength)
.HasColumnName("email");
entity.Property(e => e.Secret)
entity.Property(e => e.SecretHash)
.VarCharWithLength(HardLimits.UserEmailChangeSecretMaxLength)
.HasColumnName("secret");
entity.Property(e => e.UsedOn).HasColumnName("used_on");
Expand Down
2 changes: 1 addition & 1 deletion Common/OpenShockDb/PasswordReset.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public partial class PasswordReset

public DateTime? UsedOn { get; set; }

public string Secret { get; set; } = null!;
public string SecretHash { get; set; } = null!;

public virtual User User { get; set; } = null!;
}
2 changes: 1 addition & 1 deletion Common/OpenShockDb/UsersActivation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public partial class UsersActivation

public DateTime? UsedOn { get; set; }

public string Secret { get; set; } = null!;
public string SecretHash { get; set; } = null!;

public virtual User User { get; set; } = null!;
}
2 changes: 1 addition & 1 deletion Common/OpenShockDb/UsersEmailChange.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ public partial class UsersEmailChange

public DateTime? UsedOn { get; set; }

public string Secret { get; set; } = null!;
public string SecretHash { get; set; } = null!;

public string Email { get; set; } = null!;

Expand Down
20 changes: 14 additions & 6 deletions Common/Services/BatchUpdate/BatchUpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
using System.Timers;
using Microsoft.EntityFrameworkCore;
using NRedisStack.RedisStackCommands;
using OpenShock.Common.Constants;
using OpenShock.Common.OpenShockDb;
using OpenShock.Common.Redis;
using OpenShock.Common.Utils;
using StackExchange.Redis;
using Timer = System.Timers.Timer;

Expand Down Expand Up @@ -97,9 +99,9 @@ private async Task UpdateSessions()

var json = _connectionMultiplexer.GetDatabase().JSON();

foreach (var (sessionKey, lastUsed) in _sessionLastUsed.DequeueAll())
foreach (var (sessionToken, lastUsed) in _sessionLastUsed.DequeueAll())
{
sessionsToUpdate.Add(json.SetAsync(typeof(LoginSession).FullName + ":" + sessionKey, "LastUsed", lastUsed.ToUnixTimeMilliseconds(), When.Always));
sessionsToUpdate.Add(json.SetAsync(typeof(LoginSession).FullName + ":" + sessionToken, "LastUsed", lastUsed.ToUnixTimeMilliseconds(), When.Always));
}

try
Expand All @@ -111,14 +113,20 @@ private async Task UpdateSessions()
}
}

public void UpdateTokenLastUsed(Guid tokenId)
public void UpdateApiTokenLastUsed(Guid apiTokenId)
{
_tokenLastUsed.Enqueue(tokenId, false);
_tokenLastUsed.Enqueue(apiTokenId, false);
}

public void UpdateSessionLastUsed(string sessionKey, DateTimeOffset lastUsed)
public void UpdateSessionLastUsed(string sessionToken, DateTimeOffset lastUsed)
{
_sessionLastUsed.Enqueue(sessionKey, lastUsed);
// Only hash new tokens, old ones are 64 chars long
if (sessionToken.Length == AuthConstants.GeneratedTokenLength)
Copy link

Copilot AI Apr 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Token normalization by checking the token length is repeated here; centralizing this logic in a utility method would reduce maintenance risk and ensure consistency.

Copilot uses AI. Check for mistakes.
{
sessionToken = HashingUtils.HashToken(sessionToken);
}

_sessionLastUsed.Enqueue(sessionToken, lastUsed);
}

public Task StartAsync(CancellationToken cancellationToken)
Expand Down
6 changes: 3 additions & 3 deletions Common/Services/BatchUpdate/IBatchUpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public interface IBatchUpdateService
/// <summary>
/// Update time of last used for a token
/// </summary>
/// <param name="tokenId"></param>
public void UpdateTokenLastUsed(Guid tokenId);
public void UpdateSessionLastUsed(string sessionKey, DateTimeOffset lastUsed);
/// <param name="apiTokenId"></param>
public void UpdateApiTokenLastUsed(Guid apiTokenId);
public void UpdateSessionLastUsed(string sessionToken, DateTimeOffset lastUsed);
}
Loading