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
14 changes: 14 additions & 0 deletions AutoStack.Application/Common/Interfaces/Auth/IEncryptionService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
namespace AutoStack.Application.Common.Interfaces.Auth;

/// <summary>
/// Service for encrypting and decrypting sensitive data
/// </summary>
public interface IEncryptionService
{
/// <summary>
/// Encrypts plain text into an encrypted string
/// </summary>
/// <param name="text">The plain text to encrypt</param>
/// <returns>The encrypted text</returns>
string Encrypt(string text);

/// <summary>
/// Decrypts an encrypted string back to plain text
/// </summary>
/// <param name="encryptedText">The encrypted text to decrypt</param>
/// <returns>The decrypted plain text</returns>
string Decrypt(string encryptedText);
}
35 changes: 31 additions & 4 deletions AutoStack.Application/Common/Interfaces/Auth/IToken.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

namespace AutoStack.Application.Common.Interfaces.Auth;

/// <summary>
/// Service for generating and verifying JWT tokens for authentication
/// </summary>
public interface IToken
{
/// <summary>
Expand All @@ -11,25 +14,49 @@ public interface IToken
/// <param name="userId">The users id</param>
/// <param name="username">The users username</param>
/// <param name="email">The users email</param>
/// <returns></returns>
/// <returns>A JWT access token string</returns>
string GenerateAccessToken(Guid userId, string username, string email);

/// <summary>
/// Generates a refresh token
/// </summary>
/// <param name="userId">The users id</param>
/// <returns>Refresh token data</returns>
RefreshToken GenerateRefreshToken(Guid userId);

/// <summary>
/// Verifies a token is valid
/// </summary>
/// <param name="token">The users auth token</param>
/// <returns>The Claims principal and null if invalid</returns>
ClaimsPrincipal? VerifyToken(string token);


/// <summary>
/// Generates a short-lived token for two-factor authentication verification
/// </summary>
/// <param name="userId">The user's ID</param>
/// <returns>A JWT token for 2FA verification</returns>
string GenerateTwoFactorToken(Guid userId);

/// <summary>
/// Verifies a two-factor authentication token and extracts the user ID
/// </summary>
/// <param name="token">The 2FA token to verify</param>
/// <returns>The user ID if valid, null otherwise</returns>
Guid? VerifyTwoFactorToken(string token);

/// <summary>
/// Generates a token for two-factor authentication setup containing the secret key
/// </summary>
/// <param name="userId">The user's ID</param>
/// <param name="secretKey">The TOTP secret key</param>
/// <returns>A JWT token containing the setup information</returns>
string GenerateSetupToken(Guid userId, string secretKey);

/// <summary>
/// Verifies a setup token and extracts the user ID and secret key
/// </summary>
/// <param name="token">The setup token to verify</param>
/// <returns>A tuple of (userId, secretKey) if valid, null otherwise</returns>
(Guid, string)? VerifySetupToken(string token);
}
25 changes: 25 additions & 0 deletions AutoStack.Application/Common/Interfaces/Auth/ITotpService.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
namespace AutoStack.Application.Common.Interfaces.Auth;

/// <summary>
/// Service for Time-based One-Time Password (TOTP) operations for two-factor authentication
/// </summary>
public interface ITotpService
{
/// <summary>
/// Generates a new secret key for TOTP setup
/// </summary>
/// <returns>A base32-encoded secret key</returns>
string GenerateSecretKey();

/// <summary>
/// Validates a TOTP code against a secret key
/// </summary>
/// <param name="secretKey">The user's secret key</param>
/// <param name="code">The TOTP code to validate</param>
/// <returns>True if the code is valid, false otherwise</returns>
bool ValidateCode(string secretKey, string code);

/// <summary>
/// Generates a TOTP URI for QR code generation
/// </summary>
/// <param name="secretKey">The user's secret key</param>
/// <param name="email">The user's email address</param>
/// <param name="issuer">The application/issuer name</param>
/// <returns>A TOTP URI string</returns>
string GenerateTotpUri(string secretKey, string email, string issuer);

/// <summary>
/// Generates a QR code image as base64-encoded bytes from a TOTP URI
/// </summary>
/// <param name="totpUri">The TOTP URI to encode</param>
/// <returns>Base64-encoded QR code image bytes</returns>
byte[] GenerateQrCodeBase64(string totpUri);
}
9 changes: 9 additions & 0 deletions AutoStack.Application/Common/Interfaces/IEmailService.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
namespace AutoStack.Application.Common.Interfaces;

/// <summary>
/// Service for sending email messages
/// </summary>
public interface IEmailService
{
/// <summary>
/// Sends an email message to the specified recipient
/// </summary>
/// <param name="email">The recipient email address</param>
/// <param name="subject">The email subject</param>
/// <param name="message">The email message body</param>
Task SendEmailAsync(string email, string subject, string message);
}
17 changes: 17 additions & 0 deletions AutoStack.Application/Common/Interfaces/IFileStorageService.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
namespace AutoStack.Application.Common.Interfaces;

/// <summary>
/// Service for managing file storage operations (avatars, uploads, etc.)
/// </summary>
public interface IFileStorageService
{
/// <summary>
/// Uploads a user avatar to storage and returns the URL
/// </summary>
/// <param name="fileStream">The file stream containing the avatar image</param>
/// <param name="fileName">The name of the file</param>
/// <param name="contentType">The MIME type of the file</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>The URL of the uploaded avatar</returns>
Task<string> UploadAvatarAsync(Stream fileStream, string fileName, string contentType, CancellationToken cancellationToken);

/// <summary>
/// Deletes an avatar from storage
/// </summary>
/// <param name="fileUrl">The URL of the avatar to delete</param>
/// <param name="cancellationToken">Cancellation token</param>
Task DeleteAvatarAsync(string fileUrl, CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace AutoStack.Application.DTOs.TwoFactor;

/// <summary>
/// Response DTO for initiating two-factor authentication setup containing QR code and secret
/// </summary>
public class BeginTwoFactorSetupResponse
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace AutoStack.Application.DTOs.TwoFactor;

/// <summary>
/// Response DTO for confirming two-factor authentication setup containing recovery codes
/// </summary>
public class ConfirmTwoFactorSetupResponse
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace AutoStack.Application.DTOs.TwoFactor;

/// <summary>
/// Response DTO containing newly generated recovery codes
/// </summary>
public class RegenerateRecoveryCodesResponse
{
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace AutoStack.Application.DTOs.TwoFactor;

/// <summary>
/// Response DTO containing the current two-factor authentication status for a user
/// </summary>
public class TwoFactorStatusResponse
{
/// <summary>
Expand Down
18 changes: 18 additions & 0 deletions AutoStack.Infrastructure/Options/FileStorageOptions.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
namespace AutoStack.Infrastructure.Options;

/// <summary>
/// Configuration options for file storage (avatar uploads)
/// </summary>
public class FileStorageOptions
{
public const string SectionName = "FileStorage";

/// <summary>
/// Gets or sets the path where avatars are stored
/// </summary>
public string AvatarPath { get; set; } = "uploads/avatars";

/// <summary>
/// Gets or sets the base URL for accessing uploaded files
/// </summary>
public string BaseUrl { get; set; } = string.Empty;

/// <summary>
/// Gets or sets the maximum file size in bytes (default: 5MB)
/// </summary>
public long MaxFileSizeBytes { get; set; } = 5242880; // 5MB

/// <summary>
/// Gets or sets the allowed file extensions for uploads
/// </summary>
public string[] AllowedExtensions { get; set; } = { ".jpg", ".jpeg", ".png", ".webp", ".gif" };
}
36 changes: 36 additions & 0 deletions AutoStack.Infrastructure/Security/CookieManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,51 @@

namespace AutoStack.Infrastructure.Security;

/// <summary>
/// Service for managing authentication cookies (access and refresh tokens)
/// </summary>
public interface ICookieManager
{
/// <summary>
/// Sets the access token cookie in the HTTP response
/// </summary>
/// <param name="httpContext">The HTTP context</param>
/// <param name="accessToken">The access token value</param>
/// <param name="expirationMinutes">Time before access token expires</param>
void SetAccessTokenCookie(HttpContext httpContext, string accessToken, int expirationMinutes);

/// <summary>
/// Sets the refresh token cookie in the HTTP response
/// </summary>
/// <param name="httpContext">The HTTP context</param>
/// <param name="refreshToken">The refresh token value</param>
/// <param name="expirationDays">Days before refresh token expires</param>
void SetRefreshTokenCookie(HttpContext httpContext, string refreshToken, int expirationDays);

/// <summary>
/// Gets the access token from the request cookies
/// </summary>
/// <param name="httpContext">The HTTP context</param>
/// <returns>The access token if present, null otherwise</returns>
string? GetAccessTokenFromCookie(HttpContext httpContext);

/// <summary>
/// Gets the refresh token from the request cookies
/// </summary>
/// <param name="httpContext">The HTTP context</param>
/// <returns>The refresh token if present, null otherwise</returns>
string? GetRefreshTokenFromCookie(HttpContext httpContext);

/// <summary>
/// Clears the authentication cookies from the response
/// </summary>
/// <param name="httpContext">The HTTP context</param>
void ClearAuthCookies(HttpContext httpContext);
}

/// <summary>
/// Implementation of cookie manager for authentication cookies
/// </summary>
public class CookieManager : ICookieManager
{
private readonly CookieSettings _cookieSettings;
Expand Down
6 changes: 6 additions & 0 deletions AutoStack.Infrastructure/Security/Models/TwoFactorSettings.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
namespace AutoStack.Infrastructure.Security.Models;

/// <summary>
/// Configuration settings for two-factor authentication
/// </summary>
public class TwoFactorSettings
{
/// <summary>
/// Gets or sets the encryption key used to encrypt TOTP secret keys
/// </summary>
public string EncryptionKey { get; set; }

Check warning on line 11 in AutoStack.Infrastructure/Security/Models/TwoFactorSettings.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Non-nullable property 'EncryptionKey' must contain a non-null value when exiting constructor. Consider adding the 'required' modifier or declaring the property as nullable.
}
3 changes: 3 additions & 0 deletions AutoStack.Infrastructure/Services/LocalFileStorageService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

namespace AutoStack.Infrastructure.Services;

/// <summary>
/// Local file system implementation of file storage service for avatar uploads
/// </summary>
public class LocalFileStorageService : IFileStorageService
{
private readonly FileStorageOptions _options;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

namespace AutoStack.Presentation.Middleware;

/// <summary>
/// Middleware that extracts access tokens from cookies and adds them to Authorization header
/// Enables browser-based authentication using cookies instead of requiring explicit Bearer token headers
/// </summary>
public class CookieAuthenticationMiddleware
{
private readonly RequestDelegate _next;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

namespace AutoStack.Presentation.Middleware;

/// <summary>
/// Global exception handler middleware that catches and logs unhandled exceptions
/// </summary>
public class GlobalExceptionHandlerMiddleware(
ILogger<GlobalExceptionHandlerMiddleware> logger,
IHostEnvironment environment)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
namespace AutoStack.Presentation.Middleware;

/// <summary>
/// Middleware that adds security headers to HTTP responses to protect against common web vulnerabilities
/// </summary>
public class SecurityHeadersMiddleware
{
private readonly RequestDelegate _next;
Expand Down
5 changes: 5 additions & 0 deletions AutoStack.Presentation/Models/DisableTwoFactorRequest.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
namespace AutoStack.Presentation.Models;

/// <summary>
/// Request model for disabling two-factor authentication
/// </summary>
/// <param name="Password">The user's current password for verification</param>
/// <param name="TotpCode">The current TOTP code for verification</param>
public record DisableTwoFactorRequest(
string Password,
string TotpCode
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
namespace AutoStack.Presentation.Models;

/// <summary>
/// Request model for regenerating two-factor authentication recovery codes
/// </summary>
/// <param name="Password">The user's current password for verification</param>
/// <param name="TotpCode">The current TOTP code for verification</param>
public record RegenerateRecoveryCodesRequest(
string Password,
string TotpCode
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

namespace AutoStack.Presentation.Transformers;

/// <summary>
/// OpenAPI document transformer that adds Bearer JWT authentication scheme to all endpoints
/// </summary>
internal sealed class BearerSecuritySchemeTransformer : IOpenApiDocumentTransformer
{
private readonly IAuthenticationSchemeProvider _authenticationSchemeProvider;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

namespace AutoStack.Presentation.Transformers;

/// <summary>
/// OpenAPI document transformer that removes server-side properties from request schemas
/// </summary>
internal sealed class RemoveServerSidePropertiesTransformer : IOpenApiDocumentTransformer
{
public Task TransformAsync(OpenApiDocument document, OpenApiDocumentTransformerContext context, CancellationToken cancellationToken)
Expand All @@ -10,9 +13,9 @@
// Remove UserId from CreateStack request body since it's set server-side from JWT
foreach (var path in document.Paths.Values)
{
foreach (var operation in path.Operations.Values)

Check warning on line 16 in AutoStack.Presentation/Transformers/RemoveServerSidePropertiesTransformer.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Dereference of a possibly null reference.
{
if (operation.RequestBody?.Content.TryGetValue("application/json", out var mediaType) == true)

Check warning on line 18 in AutoStack.Presentation/Transformers/RemoveServerSidePropertiesTransformer.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Dereference of a possibly null reference.
{
// Check if this is the CreateStack operation by checking if it has the properties we expect
if (mediaType.Schema?.Properties?.ContainsKey("userId") == true &&
Expand Down
Loading