Skip to content

Caching tokens with redis & reset Password #28

Merged
hadeer-r merged 6 commits into
Askfm-clone:Developfrom
hadeer-r:CachingTokensWithRedis
Sep 22, 2025
Merged

Caching tokens with redis & reset Password #28
hadeer-r merged 6 commits into
Askfm-clone:Developfrom
hadeer-r:CachingTokensWithRedis

Conversation

@hadeer-r
Copy link
Copy Markdown
Contributor

@hadeer-r hadeer-r commented Sep 15, 2025

Summary by CodeRabbit

  • New Features

    • Added password reset flow with new endpoints to request a reset link and set a new password; emails are sent for these actions.
    • Improved API documentation with enhanced authentication details in the API explorer.
  • Refactor

    • Refresh-token endpoint now uses POST.
    • Logout adds a runtime user identity check and returns an error on mismatch.
    • Authentication strengthened with improved token handling and revocation for better security.

Task Grading Bot added 3 commits September 9, 2025 07:48
	- using redis to cache jwt token and refresh
	- immediate logout

refactor:
	- change the http method of refressh token to be post instead of get
	- create AppConstants in shared layer for hardcoded variables or names
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 15, 2025

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

The PR refactors auth and token handling to use Redis, adds password reset email workflows, updates startup configuration (JWT, Identity, Swagger, Redis, email), modifies DTOs for refresh tokens, deactivates EF RefreshToken model usage, and introduces configuration-backed email settings and cache key helpers.

Changes

Cohort / File(s) Summary
Auth controller updates
AskFm/AskFm.API/Controllers/AuthController.cs
Injects IUserService; changes refresh-token to POST; adds identity check in logout; switches to Logout(userId, refreshToken); adds POST /forgot-password and POST /reset-password endpoints.
Startup and configuration
AskFm/AskFm.API/Program.cs, AskFm/AskFm.API/appsettings.json
Wires Redis cache, JWT jti validation with Redis, Identity setup, Swagger security; configures token lifespans and data protection; adds email sender DI; adds config sections for expiry and email.
Email infrastructure
AskFm/AskFm.BLL/Services/IEmailSender.cs, AskFm/AskFm.BLL/Services/EmailSender.cs, AskFm/AskFm.BLL/DTO/EmailSettings.cs, AskFm/AskFm.BLL/AskFm.BLL.csproj
Introduces email sender interface and SMTP-based implementation; adds EmailSettings DTO; adds SendGrid package reference; reads settings from EmailOption config.
Redis cache services
AskFm/AskFm.BLL/Services/IRedisService.cs, AskFm/AskFm.BLL/Services/RedisCacheService.cs, AskFm/Shared/AppConstants.cs
Adds Redis abstraction, makes RedisCacheService implement it, and adds helpers for cache keys (jti, user JWT, refresh token).
Auth service refactor
AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs, AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs, AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs
Moves token lifecycle to Redis (JWT jti and refresh token); adds logout, forgot/reset password; emails reset link; includes jti claim; JwtOptions strings nullable.
DTO adjustments
AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs, AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs, AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs, AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs
Adds refresh token DTO and swaps into AuthResponseDTO; adds forgot/reset password DTOs with validation attributes.
Identity models cleanup
AskFm/AskFm.DAL/Models/ApplicationUser.cs, AskFm/AskFm.DAL/Models/RefreshToken.cs
Removes ApplicationUser.RefreshTokens; comments out the RefreshToken owned entity, deactivating EF-based refresh token storage.
User service surface
AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs
Removes ConfirmEmail() method from the interface.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant C as Client
  participant AC as AuthController
  participant AS as AuthService
  participant R as RedisCacheService
  participant UM as UserManager
  Note over AC,AS: Login / Token issuance
  C->>AC: POST /login (credentials)
  AC->>AS: GetAuthToken(...)
  AS->>UM: Validate user + password
  AS->>AS: Generate JWT with jti
  AS->>R: Set JwtCacheKey(jti) -> userId
  AS->>R: Set UserJwtCacheKey(userId) -> jti (replace old)
  AS->>R: Get UserRefreshTokenCacheKey(userId)
  alt No refresh token in Redis
    AS->>AS: generateRefreshToken()
    AS->>R: Set UserRefreshTokenCacheKey(userId) -> RefreshTokenDto
  end
  AS-->>AC: AuthResponseDTO (JWT, RefreshTokenDto)
  AC-->>C: 200 OK
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant API as API (JWT Bearer)
  participant R as RedisCacheService
  Note over API,R: JWT validation (per request)
  C->>API: Request with Bearer token (jti)
  API->>R: Get JwtCacheKey(jti)
  alt jti found
    API-->>C: Proceed (token valid)
  else jti missing
    API-->>C: 401 (token revoked/invalid)
  end
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant AC as AuthController
  participant AS as AuthService
  participant R as RedisCacheService
  Note over AC,AS: Logout
  C->>AC: POST /logout/{userId} (refreshToken)
  AC->>AS: Logout(userId, refreshToken)
  AS->>R: Remove UserJwtCacheKey(userId) + JwtCacheKey(current jti)
  AS->>R: Validate and remove UserRefreshTokenCacheKey(userId) if matches
  AS-->>AC: Success
  AC-->>C: 200 OK
Loading
sequenceDiagram
  autonumber
  participant C as Client
  participant AC as AuthController
  participant AS as AuthService
  participant UM as UserManager
  participant ES as IEmailSender
  Note over AC,AS: Forgot/Reset password
  C->>AC: POST /forgot-password (email)
  AC->>AS: ForgotPasswordAsync(email)
  AS->>UM: Generate password reset token
  AS->>ES: SendPasswordResetLinkAsync(email, resetUrl)
  AS-->>AC: Success
  AC-->>C: 200 Check Your Email

  C->>AC: POST /reset-password (email, token, newPassword)
  AC->>AS: ResetPasswordAsync(dto)
  AS->>UM: ResetPasswordAsync(user, token, newPassword)
  AS-->>AC: Result
  AC-->>C: 200/400
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

A rabbit taps keys with a redis-cheer,
JTI crumbs mark paths so clear.
Emails hop with reset links bright,
Tokens refresh in the moonlit night.
Swagger whispers, “All secure!”
Cache burrows deep—sessions endure.
Sip tea, ship code—thump-thump, pure. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.17% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly references the two main changes in the diff — adding Redis-backed token caching and introducing password reset endpoints and services — which matches the provided changeset. It is concise and focused and conveys the primary intent to a teammate scanning PR history. Minor capitalization and grammar issues (e.g., "redis" → "Redis", "reset Password" → "Reset Password" or "reset password") could be cleaned up but do not make the title misleading.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hadeer-r
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 15, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

🧹 Nitpick comments (20)
AskFm/Shared/AppConstants.cs (1)

3-20: Make the container class static; optional: add environment scoping.
This is a pure utility; mark the class static to prevent instantiation. Consider adding an env/application prefix to avoid cross-environment key collisions.

Apply:

-public class AppConstants
+public static class AppConstants
 {
AskFm/AskFm.BLL/AskFm.BLL.csproj (1)

17-17: Remove unused SendGrid PackageReference or wire up SendGrid

AskFm/AskFm.BLL/Services/EmailSender.cs contains only using SendGrid / using SendGrid.Helpers.Mail (lines 10–11) but instantiates SmtpClient (line 83) — SendGrid types aren’t used. Remove the PackageReference in AskFm/AskFm.BLL/AskFm.BLL.csproj (line 17) and delete the unused usings, or replace the SmtpClient implementation with a SendGrid client integration.

AskFm/AskFm.API/appsettings.json (1)

12-15: Clarify expiration units and naming.
Verified: ExpireTimes:Jwt_Token_Exp is treated as minutes (JwtOptions.AccessExpiration; AuthService uses AddMinutes/TimeSpan.FromMinutes) and ExpireTimes:Refresh_Token_Exp is treated as days (JwtOptions.AccessRefreshTokenExpiration; AuthService uses AddDays). Recommend renaming keys to be explicit (e.g., AccessExpirationMinutes, RefreshExpirationDays) or documenting the units in the README.

AskFm/AskFm.BLL/Services/IEmailSender.cs (2)

1-3: Remove unused using.

Microsoft.AspNetCore.Identity isn’t used here.

-using Microsoft.AspNetCore.Identity;

6-12: Add CancellationToken and fix minor spacing.

Async APIs should accept a CancellationToken; also remove extra spaces after (.

-public interface IEmailSender
-{
-    public Task<ServiceResult<bool>> SendPasswordResetLinkAsync( string email, string resetLink);
-    public Task<ServiceResult<bool>> SendConfirmationLinkAsync(string email, string confirmationLink);
-    public Task<ServiceResult<bool>> SendPasswordResetCodeAsync( string email, string resetCode);
-    Task<ServiceResult<bool>> SendEmailAsync (string email, string subject, string message);
-}
+public interface IEmailSender
+{
+    Task<ServiceResult<bool>> SendPasswordResetLinkAsync(string email, string resetLink, CancellationToken ct = default);
+    Task<ServiceResult<bool>> SendConfirmationLinkAsync(string email, string confirmationLink, CancellationToken ct = default);
+    Task<ServiceResult<bool>> SendPasswordResetCodeAsync(string email, string resetCode, CancellationToken ct = default);
+    Task<ServiceResult<bool>> SendEmailAsync(string email, string subject, string message, CancellationToken ct = default);
+}

Add this import if not present:

using System.Threading;
AskFm/AskFm.BLL/Services/RedisCacheService.cs (1)

15-24: Add CancellationToken, guard inputs, and harden deserialization.

Prevents silent hangs and crashes on corrupt cache entries; enables cooperative cancellation.

-    public async Task SetCacheAsync<T>(string key, T value, TimeSpan expirationTime)
+    public async Task SetCacheAsync<T>(string key, T value, TimeSpan expirationTime, CancellationToken ct = default)
     {
+        if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("key cannot be null/empty", nameof(key));
         var options = new DistributedCacheEntryOptions
         {
             AbsoluteExpirationRelativeToNow = expirationTime
         };
 
         var jsonData = JsonSerializer.Serialize(value);
-        await _cache.SetStringAsync(key, jsonData, options);
+        await _cache.SetStringAsync(key, jsonData, options, ct);
     }
 
-    public async Task<T?> GetCacheAsync<T>(string key)
+    public async Task<T?> GetCacheAsync<T>(string key, CancellationToken ct = default)
     {
-        var jsonData = await _cache.GetStringAsync(key);
-        return jsonData is null ? default : JsonSerializer.Deserialize<T?>(jsonData);
+        if (string.IsNullOrWhiteSpace(key)) throw new ArgumentException("key cannot be null/empty", nameof(key));
+        var jsonData = await _cache.GetStringAsync(key, ct);
+        if (string.IsNullOrEmpty(jsonData)) return default;
+        try
+        {
+            return JsonSerializer.Deserialize<T>(jsonData);
+        }
+        catch (JsonException)
+        {
+            return default;
+        }
     }
 
-    public async Task RemoveCacheAsync(string key)
+    public async Task RemoveCacheAsync(string key, CancellationToken ct = default)
     {
-        await _cache.RemoveAsync(key);
+        if (string.IsNullOrWhiteSpace(key)) return;
+        await _cache.RemoveAsync(key, ct);
     }

Note: Update IRedisService accordingly (see its comment).

Also applies to: 26-30, 32-35

AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs (1)

1-1: Remove unused using.

AskFm.DAL.Models isn’t referenced in this DTO anymore.

-using AskFm.DAL.Models;
AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs (3)

15-16: Async naming consistency.

Rename to include Async to match the rest of the interface.

-    Task<ServiceResult<bool>> UpdatePassword(int userId, UpdatePasswordDTO updatePasswordDto);
-    Task<ServiceResult<ReadUserDTO>> ResetEmail(int userId, string updatedEmail);
+    Task<ServiceResult<bool>> UpdatePasswordAsync(int userId, UpdatePasswordDTO updatePasswordDto);
+    Task<ServiceResult<ReadUserDTO>> ResetEmailAsync(int userId, string updatedEmail);

6-17: Consider CancellationToken across service methods.

Add CancellationToken ct = default to public async methods for consistency and cooperative cancellation.


21-24: Remove stray comment block or move to XML docs.

Inline endpoint notes in an interface are noise.

AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs (1)

7-9: Optional: bound email length.

Prevents excessively large payloads; 256 is common for emails.

     [Required]
     [EmailAddress]
-    public string Email { get; set; }   
+    [StringLength(256)]
+    public string Email { get; set; }   
AskFm/AskFm.BLL/Services/IRedisService.cs (2)

1-3: Remove unused usings.

These aren’t needed in the interface.

-using Microsoft.Extensions.Caching.Distributed;
-using System.Text.Json;

6-13: Expose CancellationToken in the contract.

Aligns with RedisCacheService and enables cooperative cancellation.

-public interface IRedisService
-{
-    public Task SetCacheAsync<T>(string key, T value, TimeSpan expirationTime);
-
-    public Task<T?> GetCacheAsync<T>(string key);
-
-    public Task RemoveCacheAsync(string key);
-}
+public interface IRedisService
+{
+    Task SetCacheAsync<T>(string key, T value, TimeSpan expirationTime, CancellationToken ct = default);
+    Task<T?> GetCacheAsync<T>(string key, CancellationToken ct = default);
+    Task RemoveCacheAsync(string key, CancellationToken ct = default);
+}

Add if not present:

using System.Threading;
AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs (1)

14-19: Add password metadata annotations.

Helpful for UI scaffolding and basic length constraints; server-side policy can still enforce complexity.

     [Required]
-    public string NewPassword { get; set; }
+    [DataType(DataType.Password)]
+    [StringLength(100, MinimumLength = 8)]
+    public string NewPassword { get; set; }
 
     [Required]
     [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
-    public string ConfirmPassword { get; set; }
+    [DataType(DataType.Password)]
+    public string ConfirmPassword { get; set; }
AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs (1)

12-14: Rename Logout → LogoutAsync, add CancellationToken, and clarify overlap with RevokeRefreshTokenAsync.

  • Change signatures: IAuthService.AuthService Logout → LogoutAsync(int userId, string refreshToken, CancellationToken ct = default) and update AuthService implementation and the call site (AskFm/AskFm.API/Controllers/AuthController.cs:97).
  • Add optional CancellationToken to ForgotPasswordAsync and ResetPasswordAsync (interface + implementation) to allow cancellation.
  • RevokeRefreshTokenAsync is a public API (IAuthService.cs:11 / AuthService.cs:151) and Logout currently calls it then revokes the JWT; either (a) propagate CancellationToken into RevokeRefreshTokenAsync and pass it from LogoutAsync, or (b) document the distinct semantics (RevokeRefreshTokenAsync = refresh-token-only; LogoutAsync = refresh + JWT) or consolidate to avoid overlapping public APIs.
AskFm/AskFm.BLL/Services/EmailSender.cs (2)

10-11: Remove unused SendGrid references

The SendGrid imports are not used in this implementation. Remove these unnecessary dependencies.

-using SendGrid;
-using SendGrid.Helpers.Mail;

83-87: SmtpClient should be disposed

SmtpClient implements IDisposable and should be properly disposed to release network resources.

The fix is included in the error handling suggestion above with the using statement.

AskFm/AskFm.API/Controllers/AuthController.cs (1)

127-130: Redundant null check

The null check on line 127 is redundant since model validation should handle this through data annotations.

-    if (resetPasswordDto == null)
-    {
-        return BadRequest("Invalid Data");
-    }
-
     var result = await _authService.ResetPasswordAsync(resetPasswordDto);
AskFm/AskFm.API/Program.cs (1)

140-144: Consider more specific error message for revoked tokens

The error message "Token revoked or expired" could be split to distinguish between revocation and expiration for better debugging.

 var cachedToken = await redis.GetCacheAsync<int>(AppConstants.JwtCacheKey(jti));
 if (cachedToken <= 0)
 {
-    context.Fail("Token revoked or expired");
+    context.Fail("Token has been revoked");
 }
AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs (1)

280-282: ExpireAfter should use days, not the raw config value

The ExpireAfter field appears to represent days but is being set to the raw configuration value which might be confusing.

Consider renaming the configuration key or the property for clarity:

-            ExpireAfter = _configuration.GetValue<int>("ExpireTimes:Refresh_Token_Exp")
+            ExpireAfter = _configuration.GetValue<int>("ExpireTimes:Refresh_Token_Days")
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9337a04 and 3ab007f.

📒 Files selected for processing (20)
  • AskFm/AskFm.API/Controllers/AuthController.cs (4 hunks)
  • AskFm/AskFm.API/Program.cs (6 hunks)
  • AskFm/AskFm.API/appsettings.json (1 hunks)
  • AskFm/AskFm.BLL/AskFm.BLL.csproj (1 hunks)
  • AskFm/AskFm.BLL/DTO/EmailSettings.cs (1 hunks)
  • AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs (1 hunks)
  • AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs (1 hunks)
  • AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs (1 hunks)
  • AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/EmailSender.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/IEmailSender.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/IRedisService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/RedisCacheService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs (8 hunks)
  • AskFm/AskFm.BLL/Services/UserIdentityService/IAuthService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/UserIdentityService/IUserService.cs (1 hunks)
  • AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs (1 hunks)
  • AskFm/AskFm.DAL/Models/ApplicationUser.cs (1 hunks)
  • AskFm/AskFm.DAL/Models/RefreshToken.cs (1 hunks)
  • AskFm/Shared/AppConstants.cs (1 hunks)
🔇 Additional comments (13)
AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs (1)

5-7: Enforce required JwtOptions (Issuer/Audience/SigningKey) and enable options validation

File: AskFm/AskFm.BLL/Services/UserIdentityService/JwtOptions.cs (lines 5-7)
Make Issuer/Audience/SigningKey non-nullable and validated to avoid broken token generation/validation.

+using System.ComponentModel.DataAnnotations;
 public class JwtOptions
 {
-    public string? Issuer { get; set; }
-    public string?  Audience { get; set; }
-    public string? SigningKey  { get; set; }
+    [Required, MinLength(1)]
+    public string Issuer { get; set; } = default!;
+    [Required, MinLength(1)]
+    public string Audience { get; set; } = default!;
+    [Required, MinLength(32)]
+    public string SigningKey  { get; set; } = default!;
     public int AccessExpiration { get; set; } // Minutes
     public int AccessRefreshTokenExpiration { get; set; } // days
 }

Ensure options validation is enabled in DI:
services.AddOptions().Bind(config.GetSection("JwtOptions")).ValidateDataAnnotations().ValidateOnStart();

Verify the DI registration above exists in Program/Startup and that configuration values are present.

AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs (1)

7-9: LGTM.

Validation attributes are appropriate.

AskFm/AskFm.API/Controllers/AuthController.cs (3)

62-63: Good improvement: Changed refresh-token from GET to POST

Changing the refresh token endpoint from GET to POST is a security best practice, as it prevents token leakage through URL logging.


86-90: User ID validation prevents privilege escalation

Good security practice - validating that the path parameter matches the authenticated user prevents users from logging out other users' sessions.


113-119: Generic success message prevents user enumeration

Good security practice - returning "Check Your Email" regardless of whether the email exists prevents attackers from discovering valid email addresses.

AskFm/AskFm.API/Program.cs (4)

95-96: Token expiration times moved to configuration

Good practice - moving token expiration times from environment variables to configuration makes them easier to manage.

Also applies to: 108-109


133-147: JWT revocation check implementation

Excellent implementation of JWT revocation using Redis. The check for revoked tokens in OnTokenValidated prevents the use of invalidated JWTs.


184-190: Redis configuration uses connection string

Good - using a connection string for Redis configuration is more flexible than hardcoding host/port.


111-111: Password reset token lifespan configured

Setting DataProtectionTokenProviderOptions.TokenLifespan to 2 hours is reasonable for password reset tokens.

AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs (4)

239-240: JWT tracking in Redis enables single-device login

The implementation stores only one JWT per user in Redis, effectively implementing single-device login. This is a valid design choice but should be documented.


249-250: Refresh token cache expiration aligns with token lifetime

Good practice - setting the Redis cache expiration to match the refresh token's actual expiration ensures consistency.


178-187: Logout implementation properly revokes both tokens

The logout flow correctly revokes both the refresh token and JWT, ensuring complete session termination.


308-315: RevokeJwtToken helper properly cleans up Redis entries

The helper method correctly removes both the JWT mapping and user's current JWT reference from Redis.

Comment on lines +16 to +21
"EmailOption": {
"client": "smtp.gmail.com",
"password": "password",
"from": "email",
"port":567
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

SMTP port is likely wrong; fix 567 → 587 (or 465 for SSL). Also avoid storing plaintext passwords.
Port 567 will fail for Gmail. Use 587 (STARTTLS) or 465 (SSL) and move EmailOption.password to environment variables or user-secrets.

Apply:

   "EmailOption": {
     "client": "smtp.gmail.com",
-    "password": "password",
+    "password": "",
     "from": "email",
-    "port":567
+    "port": 587
   },

Consider removing EmailOption from source-controlled appsettings.json entirely and sourcing via appsettings.Development.json + secrets.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"EmailOption": {
"client": "smtp.gmail.com",
"password": "password",
"from": "email",
"port":567
},
"EmailOption": {
"client": "smtp.gmail.com",
"password": "",
"from": "email",
"port": 587
},
🤖 Prompt for AI Agents
In AskFm/AskFm.API/appsettings.json around lines 16 to 21 the SMTP port is
incorrect (567) and the password is stored in plaintext; change the port to 587
(or 465 if you intend to use SSL) and remove the plaintext password from this
source-controlled file, instead reading EmailOption.password from environment
variables or user-secrets (or move the whole EmailOption into
appsettings.Development.json and protect secrets with dotnet user-secrets); also
update the configuration code to prefer environment/user-secret values for the
email password and host/port so no sensitive credentials remain in source
control.

"from": "email",
"port":567
},
"ClientUrYour App Namel": " http://localhost:5180",
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix typo and leading space in client URL key/value.
"ClientUrYour App Namel" won’t bind; likely intended "ClientUrl". Also remove the leading space in the URL.

Apply:

-"ClientUrYour App Namel": " http://localhost:5180",
+"ClientUrl": "http://localhost:5180",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"ClientUrYour App Namel": " http://localhost:5180",
"ClientUrl": "http://localhost:5180",
🤖 Prompt for AI Agents
In AskFm/AskFm.API/appsettings.json around line 22, the JSON key and value are
mistyped and have a leading space; change the key "ClientUrYour App Namel" to
the intended "ClientUrl" (or the exact config key your app expects) and remove
the leading space from the URL value so it reads "http://localhost:5180" (ensure
quotes and JSON formatting remain valid).

Comment on lines +110 to +120
public async Task<IActionResult> ForgotPassword(ForgotPasswordDto forgotPasswordDto)
{

var result = await _authService.ForgotPasswordAsync(forgotPasswordDto.Email);
if (!result.success)
{
return BadRequest(result.Errors);
}

return Ok("Check Your Email");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Password reset endpoint lacks rate limiting

The forgot-password endpoint is vulnerable to abuse without rate limiting. Attackers could spam users with reset emails.

Consider implementing rate limiting at the API level or tracking reset attempts per email address in Redis with a time window.

🤖 Prompt for AI Agents
In AskFm/AskFm.API/Controllers/AuthController.cs around lines 110 to 120, the
ForgotPassword action currently calls _authService.ForgotPasswordAsync without
any throttling; add rate limiting to prevent abuse by either (1) applying
API-level rate limit middleware (e.g., AspNetCoreRateLimit or built-in endpoint
rate limiting) for this route, or (2) tracking per-email reset attempts in
Redis/CDS: inject IDistributedCache or a Redis client, increment a counter keyed
by normalized email with a TTL (e.g., 1 hour), return 429 TooManyRequests when
the count exceeds the threshold, and only call _authService.ForgotPasswordAsync
when under limit; also keep the endpoint response non-enumerative (always return
a generic success message) and log rate-limit events for monitoring.

Comment on lines +3 to +10
public class EmailSettings
{
public string From {get; set;}
public string Client {get; set;}
public string Password {get;set;}
public int Port {get; set; }

} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Add validation attributes; ensure the Port from config is actually used by EmailSender.
Guard against empty From/Client/Password and invalid Port.

Apply:

+using System.ComponentModel.DataAnnotations;
 public class EmailSettings
 {
-    public string From {get; set;}
-    public string Client {get; set;}
-    public string Password {get;set;}
-    public int Port {get; set; }
+    [Required, EmailAddress]
+    public string From { get; set; } = default!;
+    [Required]
+    public string Client { get; set; } = default!;
+    [Required]
+    public string Password { get; set; } = default!;
+    [Range(1, 65535)]
+    public int Port { get; set; }
 }

🏁 Script executed:

#!/bin/bash
# Verify EmailSender uses settings.Port and not a hard-coded 587
rg -n -C2 --type cs 'new\s+SmtpClient\(|\.Port\s*=' AskFm | sed -n '1,120p'

Length of output: 324


Use configured SMTP port and add validation to EmailSettings

  • Replace hard-coded 587 in AskFm/AskFm.BLL/Services/EmailSender.cs (line 83):
    var client = new SmtpClient(_emailSettings.Client, 587) -> use _emailSettings.Port (e.g., new SmtpClient(_emailSettings.Client, _emailSettings.Port)).
  • Apply DataAnnotations to AskFm/AskFm.BLL/DTO/EmailSettings.cs. Apply the following diff:
+using System.ComponentModel.DataAnnotations;
 public class EmailSettings
 {
-    public string From {get; set;}
-    public string Client {get; set;}
-    public string Password {get;set;}
-    public int Port {get; set; }
+    [Required, EmailAddress]
+    public string From { get; set; } = default!;
+    [Required]
+    public string Client { get; set; } = default!;
+    [Required]
+    public string Password { get; set; } = default!;
+    [Range(1, 65535)]
+    public int Port { get; set; }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public class EmailSettings
{
public string From {get; set;}
public string Client {get; set;}
public string Password {get;set;}
public int Port {get; set; }
}
using System.ComponentModel.DataAnnotations;
public class EmailSettings
{
[Required, EmailAddress]
public string From { get; set; } = default!;
[Required]
public string Client { get; set; } = default!;
[Required]
public string Password { get; set; } = default!;
[Range(1, 65535)]
public int Port { get; set; }
}

Comment on lines +1 to +11
using Microsoft.EntityFrameworkCore;

namespace AskFm.BLL.DTO;
public class RefreshTokenDto
{
public string Token { get; set; }
public DateTime ExpireOn { get; set; }
public bool IsExpired => DateTime.Now >= ExpireOn;
public int ExpireAfter { get; set; }
public DateTime CreatedOn { get; set; }
} No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Remove unused EF using; prefer UTC for expiry checks; consider making the DTO immutable.
Avoid EF dependency in DTOs. Use UtcNow for consistency and serialization safety.

Apply:

-using Microsoft.EntityFrameworkCore;
-
 namespace AskFm.BLL.DTO;
 public class RefreshTokenDto
 {
     public string Token { get; set; }
     public DateTime ExpireOn { get; set; }
-    public bool IsExpired => DateTime.Now >= ExpireOn;
+    public bool IsExpired => DateTime.UtcNow >= ExpireOn;
     public int ExpireAfter { get; set; }
     public DateTime CreatedOn { get; set; }
 }

🏁 Script executed:

#!/bin/bash
# Ensure all token timestamps are generated and stored in UTC
rg -n -C2 --type cs 'DateTime\.Now|DateTimeOffset\.Now|DateTime\.UtcNow' AskFm | sed -n '1,200p'

Length of output: 5709


Remove unused EF using; prefer UTC for expiry checks; unify timestamps.

Remove Microsoft.EntityFrameworkCore from RefreshTokenDto and change IsExpired to use DateTime.UtcNow; replace other DateTime.Now usages with UtcNow (or document intentional local-time use).

-using Microsoft.EntityFrameworkCore;
-
 namespace AskFm.BLL.DTO;
 public class RefreshTokenDto
 {
     public string Token { get; set; }
     public DateTime ExpireOn { get; set; }
-    public bool IsExpired => DateTime.Now >= ExpireOn;
+    public bool IsExpired => DateTime.UtcNow >= ExpireOn;
     public int ExpireAfter { get; set; }
     public DateTime CreatedOn { get; set; }
 }
  • Fix other DateTime.Now occurrences found by the search:

    • AskFm/AskFm.BLL/Services/CommentLikeService.cs:122 — existingLike.CreatedAt = DateTime.Now -> DateTime.UtcNow
    • AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs:173 — appUser.LastSeen = DateTime.Now -> DateTime.UtcNow
    • AskFm/AskFm.API/Controllers/WeatherForecastController.cs:26 — DateOnly.FromDateTime(DateTime.Now.AddDays(index)) — confirm whether local time is intended; otherwise use UTC-based conversion.
  • Optional: make RefreshTokenDto immutable (readonly properties + ctor).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
using Microsoft.EntityFrameworkCore;
namespace AskFm.BLL.DTO;
public class RefreshTokenDto
{
public string Token { get; set; }
public DateTime ExpireOn { get; set; }
public bool IsExpired => DateTime.Now >= ExpireOn;
public int ExpireAfter { get; set; }
public DateTime CreatedOn { get; set; }
}
namespace AskFm.BLL.DTO;
public class RefreshTokenDto
{
public string Token { get; set; }
public DateTime ExpireOn { get; set; }
public bool IsExpired => DateTime.UtcNow >= ExpireOn;
public int ExpireAfter { get; set; }
public DateTime CreatedOn { get; set; }
}
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/DTO/RefreshTokenDto.cs lines 1-11, remove the unused "using
Microsoft.EntityFrameworkCore;" and change the IsExpired calc to use
DateTime.UtcNow instead of DateTime.Now; also update other DateTime.Now
occurrences noted by the reviewer: in
AskFm/AskFm.BLL/Services/CommentLikeService.cs line 122 set
existingLike.CreatedAt = DateTime.UtcNow, in
AskFm/AskFm.BLL/Services/UserIdentityService/UserService.cs line 173 set
appUser.LastSeen = DateTime.UtcNow, and review
AskFm/AskFm.API/Controllers/WeatherForecastController.cs line 26 to use a
UTC-based conversion (e.g.,
DateOnly.FromDateTime(DateTime.UtcNow.AddDays(index)) if local time is not
required); optionally consider making RefreshTokenDto immutable by converting
properties to readonly and adding a constructor.

Comment on lines +198 to +200
var resetUrl =
$"{_configuration.GetValue<string>("ClientUrl")}/app/Auth/reset-password?email={email}&token={token}";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Reset URL should send the actual link in email body

The reset URL is being sent as the email body directly instead of being used in the email template. The EmailSender has a SendPasswordResetLinkAsync method that should be used.

-        var resetUrl =
-            $"{_configuration.GetValue<string>("ClientUrl")}/app/Auth/reset-password?email={email}&token={token}";
-        
-        _emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl);
+        var resetUrl =
+            $"{_configuration.GetValue<string>("ClientUrl")}/app/Auth/reset-password?email={Uri.EscapeDataString(email)}&token={Uri.EscapeDataString(token)}";
+        
+        await _emailSender.SendPasswordResetLinkAsync(email, resetUrl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var resetUrl =
$"{_configuration.GetValue<string>("ClientUrl")}/app/Auth/reset-password?email={email}&token={token}";
var resetUrl =
$"{_configuration.GetValue<string>("ClientUrl")}/app/Auth/reset-password?email={Uri.EscapeDataString(email)}&token={Uri.EscapeDataString(token)}";
await _emailSender.SendPasswordResetLinkAsync(email, resetUrl);
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs around lines
198-200, the code builds a resetUrl and sends it as the raw email body; instead
call the EmailSender's SendPasswordResetLinkAsync method so the link is placed
into the proper email template. Replace the manual send with a call to
SendPasswordResetLinkAsync (passing the recipient email and either the token or
the fully-built url per the method signature), ensure query parameters are
URL-encoded (email and token) when constructing the url, and remove the direct
SendEmailAsync usage so the template-based sender is used.

}
var token = await _userManager.GeneratePasswordResetTokenAsync(user);
var resetUrl =
$"{_configuration.GetValue<string>("ClientUrl")}/app/Auth/reset-password?email={email}&token={token}";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

URL parameters need encoding

Email and token parameters in the reset URL should be URL-encoded to handle special characters.

The fix is included in the suggestion above using Uri.EscapeDataString.

🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs around line 199,
the reset-password URL is built without encoding the email and token query
parameters; update the code to URL-encode both email and token (e.g., wrap each
with Uri.EscapeDataString or equivalent) before concatenating into the query
string so special characters are safely transmitted.

var resetUrl =
$"{_configuration.GetValue<string>("ClientUrl")}/app/Auth/reset-password?email={email}&token={token}";

_emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue

Missing await on email sending

The SendEmailAsync call is not awaited, which means the method will complete before the email is actually sent, and any exceptions will be lost.

-        _emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl);
+        await _emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
_emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl);
await _emailSender.SendEmailAsync(email, "AskFm: Reset Password", resetUrl);
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/Services/UserIdentityService/AuthService.cs around line 201,
the call to _emailSender.SendEmailAsync(...) is not awaited; update the method
to await the async call (await _emailSender.SendEmailAsync(...)) so the email
send completes before the method returns, ensure the enclosing method signature
is async and returns Task or Task<T> as appropriate, and propagate or handle
exceptions from the awaited call (optionally use ConfigureAwait(false) if
context capture should be avoided).


// tokens
public virtual ICollection<RefreshToken>? RefreshTokens { get; set; }
// public virtual ICollection<RefreshToken>? RefreshTokens { get; set; }
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Removing RefreshTokens nav requires DB migration; confirm none of the code still expects it.
If EF previously owned this collection, generate a migration to drop the owned type/table. Also verify no references remain.

Consider fully removing related config instead of leaving commented code.


🏁 Script executed:

#!/bin/bash
rg -n '\bRefreshTokens\b'

Length of output: 1291


Generate a migration to remove the RefreshTokens owned collection and delete remaining references.

Migrations and the model snapshot still include ownsMany("AskFm.DAL.Models.RefreshToken", "RefreshTokens"); add a migration to drop the owned type/table (or restore the navigation if you intend to keep it) and remove the commented nav/config.

Locations:

  • AskFm/AskFm.DAL/Models/ApplicationUser.cs:34 (commented nav).
  • AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.Designer.cs (ownsMany / navigation).
  • AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.Designer.cs (ownsMany / navigation).
  • AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.Designer.cs (ownsMany / navigation).
  • AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs (ownsMany / navigation).

Comment thread AskFm/AskFm.DAL/Models/RefreshToken.cs Outdated
Comment on lines +1 to +14
// using Microsoft.EntityFrameworkCore;
//
// namespace AskFm.DAL.Models;
// // [Owned]
// public class RefreshToken
// {
// public string Token { get; set; }
// public DateTime ExpireOn { get; set; }
// public bool IsExpired => DateTime.Now >= ExpireOn;
// public DateTime? RevokedOn { get; set; }
// public bool IsActive => RevokedOn == null && !IsExpired;
//
// public DateTime CreatedOn { get; set; }
// } No newline at end of file
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Delete dead code file instead of commenting it out; verify migrations.
Keeping a fully commented entity is confusing and may mislead EF model readers. Remove the file and ensure a migration reflects the removal.


🏁 Script executed:

#!/bin/bash
rg -n '\bRefreshToken\b'
rg -n 'Owned\(|\[Owned\]' || true
# Inspect pending migrations referencing RefreshToken
rg -n --glob '!**/bin/**' --glob '!**/obj/**' '(HasMany|OwnsMany|OwnsOne).*RefreshToken' || true

Length of output: 2769


Restore or remove RefreshToken consistently — do not leave the entity commented out.

Migrations and the model snapshot still reference AskFm.DAL.Models.RefreshToken; the commented-out class will cause compile/migration inconsistencies.

  • Keep it: un-comment AskFm.DAL/Models/RefreshToken.cs and ApplicationUser.RefreshTokens so EF mappings compile.
  • Remove it: delete the model, remove OwnsMany/ToTable and other references (DbContext, DTOs, controllers) and add a migration to drop the RefreshToken table and update the model snapshot.

Relevant files: AskFm/AskFm.DAL/Models/RefreshToken.cs, AskFm/AskFm.DAL/Models/ApplicationUser.cs, AskFm/AskFm.DAL/Migrations/20250824164944_AddRefreshTokenTable.cs, AskFm/AskFm.DAL/Migrations/20250824210328_AddColumnToRefreshTokenTable.cs, AskFm/AskFm.DAL/Migrations/20250824224832_FixIsActiveColumn_Follow.Designer.cs, AskFm/AskFm.DAL/Migrations/AppDbContextModelSnapshot.cs (and related Designer.cs migration files).

🤖 Prompt for AI Agents
In AskFm/AskFm.DAL/Models/RefreshToken.cs lines 1-14: the RefreshToken entity
was left commented-out causing EF model/migration mismatch; either restore it or
fully remove all usages. To keep it: un-comment the RefreshToken class and
ensure ApplicationUser.RefreshTokens is un-commented, restore any
[Owned]/mapping attributes and DbContext OwnsMany/ToTable configuration, rebuild
and add a new migration so the snapshot matches the model. To remove it: delete
RefreshToken.cs, remove all OwnsMany/ToTable and property references in
ApplicationUser, DbContext, DTOs and controllers, then create a migration that
drops the RefreshToken table and update the model snapshot and Designer files
before committing.

@hadeer-r hadeer-r merged commit 28cc556 into Askfm-clone:Develop Sep 22, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant