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
50 changes: 45 additions & 5 deletions AskFm/AskFm.API/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.ComponentModel.DataAnnotations;
using AskFm.BLL.DTO.UserDTOs;
using AskFm.BLL.Services;
using AskFm.BLL.Services.UserIdentityService;
Expand All @@ -13,10 +14,12 @@ namespace AskFm.API.Controllers;
public class AuthController : ControllerBase
{
private IAuthService _authService;
private IUserService _userService;

public AuthController(IAuthService authService)
public AuthController(IAuthService authService, IUserService userService)
{
_authService = authService;
_userService = userService;
}

[HttpPost]
Expand Down Expand Up @@ -56,7 +59,7 @@ public async Task<IActionResult> Login(LoginDTO login)
return Ok(result);
}

[HttpGet]
[HttpPost]
[Route("refresh-token/{id}")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> RefreshToken(int id)
Expand All @@ -80,23 +83,60 @@ public async Task<IActionResult> RefreshToken(int id)
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> Logout(int id)
{
var currentUser = await _userService.GetCurrentUserAsync();
if (currentUser.Data == null || currentUser.Data.Id != id)
{
return BadRequest("Invalid data");
}
string refreshToken = Request.Cookies["refreshToken"];
if (string.IsNullOrEmpty(refreshToken))
{
return BadRequest("token Is required");
}

ServiceResult<bool> result = await _authService.RevokeRefreshTokenAsync(id,refreshToken);

var result = await _authService.Logout(currentUser.Data.Id,refreshToken);
if (!result.success)
{
return BadRequest(result.Errors);
}


return Ok(result);
}

[HttpPost]
[AllowAnonymous]
[Route("forgot-password")]
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");
}
Comment on lines +110 to +120
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.


[HttpPost]
[AllowAnonymous]
[Route("reset-password")]
public async Task<IActionResult> ResetPassword(ResetPasswordDto resetPasswordDto)
{
if (resetPasswordDto == null)
{
return BadRequest("Invalid Data");
}

var result = await _authService.ResetPasswordAsync(resetPasswordDto);
if (!result.success)
{
return BadRequest(result.Errors);
}

return Ok("Password Reset Success");
}
private void setRefreshToken(string refreshToken,DateTime expires)
{
var cookieOption = new CookieOptions()
Expand Down
52 changes: 40 additions & 12 deletions AskFm/AskFm.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
using AskFm.BLL.Services.UserIdentityService;
using Swashbuckle.AspNetCore.SwaggerGen;
using Castle.Components.DictionaryAdapter.Xml;
using Microsoft.AspNetCore.Identity.UI.Services;
using Shared;
using IEmailSender = AskFm.BLL.Services.IEmailSender;

namespace AskFm.API;

Expand All @@ -29,7 +32,6 @@ public static void Main(string[] args)
// Add services to the container.

builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();

Env.Load();
Expand All @@ -38,12 +40,16 @@ public static void Main(string[] args)
{
throw new Exception("Connection string is null");
}

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddHttpContextAccessor();
// DbContext
builder.Services.AddDbContext<AppDbContext>(options =>
options
.UseLazyLoadingProxies()
.UseSqlServer(ConnectionString));

// -------------------------------------------------
// Register the repositories and services
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
builder.Services.AddScoped<INotificationRepository, NotificationRepository>();
builder.Services.AddScoped<INotificationService, NotificationService>();
Expand All @@ -52,11 +58,12 @@ public static void Main(string[] args)
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ICommentLikeService, CommentLikeService>();
builder.Services.AddScoped<ICommentService, CommentService>();
builder.Services.AddScoped<IEmailSender,EmailSender>();
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();

// Configure Swagger with JWT Authentication
builder.Services.AddSwaggerGen(setup =>
{
// Include 'SecurityScheme' to use JWT Authentication
var jwtSecurityScheme = new OpenApiSecurityScheme
{
BearerFormat = "JWT",
Expand All @@ -83,13 +90,15 @@ public static void Main(string[] args)
});


// Authentication & Authorization

JwtOptions jwtOptions = new JwtOptions
{
Issuer = Environment.GetEnvironmentVariable("ISSUER"),
Audience = Environment.GetEnvironmentVariable("AUDIENCE"),
SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY"),
AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP")),
AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP")),
AccessExpiration = builder.Configuration.GetValue<int>("ExpireTimes:Jwt_Token_Exp"),
AccessRefreshTokenExpiration =builder.Configuration.GetValue<int>("ExpireTimes:Refresh_Token_Exp")
};
if (jwtOptions == null)
{
Expand Down Expand Up @@ -134,15 +143,17 @@ public static void Main(string[] args)
Options.Issuer = Environment.GetEnvironmentVariable("ISSUER");
Options.Audience = Environment.GetEnvironmentVariable("AUDIENCE");
Options.SigningKey = Environment.GetEnvironmentVariable("SIGNINGKEY");
Options.AccessExpiration = int.Parse(Environment.GetEnvironmentVariable("TOKEN_EXP"));
Options.AccessRefreshTokenExpiration = int.Parse(Environment.GetEnvironmentVariable("REFRESH_TOKEN_EXP"));
Options.AccessExpiration = builder.Configuration.GetValue<int>("ExpireTimes:Jwt_Token_Exp");
Options.AccessRefreshTokenExpiration = builder.Configuration.GetValue<int>("ExpireTimes:Refresh_Token_Exp");
});

builder.Services.Configure<DataProtectionTokenProviderOptions>(options => options.TokenLifespan = TimeSpan.FromHours(2));

builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = "Bearer";
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;


})
.AddJwtBearer( Options =>
{
Expand All @@ -157,6 +168,21 @@ public static void Main(string[] args)
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)),
ClockSkew = TimeSpan.FromMinutes(0)
};
Options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var jti = context.Principal.Claims.FirstOrDefault(c => c.Type == "jti")?.Value;
var redis = context.HttpContext.RequestServices.GetRequiredService<RedisCacheService>();

var cachedToken = await redis.GetCacheAsync<int>(AppConstants.JwtCacheKey(jti));
if (cachedToken <= 0)
{
context.Fail("Token revoked or expired");
}
}
};


// Enable JWT authentication for SignalR
Options.Events = new JwtBearerEvents
Expand All @@ -175,6 +201,9 @@ public static void Main(string[] args)
};
});
builder.Services.AddAuthorization();


// Identity
builder.Services.AddIdentity<ApplicationUser, IdentityRole<int>>(options =>
{
//password configuration
Expand Down Expand Up @@ -204,16 +233,15 @@ public static void Main(string[] args)
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();


// Redis Cache
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = builder.Configuration.GetConnectionString("Redis");
options.InstanceName = "AskFmCache";
});

builder.Services.AddSingleton<RedisCacheService>();



var app = builder.Build();

// Configure the HTTP request pipeline.
Expand Down
11 changes: 11 additions & 0 deletions AskFm/AskFm.API/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,16 @@
"Microsoft.AspNetCore": "Warning"
}
},
"ExpireTimes": {
"Jwt_Token_Exp":10,
"Refresh_Token_Exp":30
},
"EmailOption": {
"client": "smtp.gmail.com",
"password": "password",
"from": "email",
"port":567
},
Comment on lines +16 to +21
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.

"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).

"AllowedHosts": "*"
}
3 changes: 2 additions & 1 deletion AskFm/AskFm.BLL/AskFm.BLL.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<PackageReference Include="AutoMapper" Version="15.0.1"/>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1"/>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
<PackageReference Include="SendGrid" Version="9.29.3" />
</ItemGroup>

</ItemGroup>

</Project>
16 changes: 16 additions & 0 deletions AskFm/AskFm.BLL/DTO/EmailSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;

namespace AskFm.BLL.DTO;

public class EmailSettings
{
[Required, EmailAddress]
public string From {get; set;}
[Required]
public string Client {get; set;}
[Required]
public string Password {get;set;}
[Range(1, 65535)]
public int Port {get; set; }

}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
using Microsoft.EntityFrameworkCore;

namespace AskFm.DAL.Models;
[Owned]
public class RefreshToken
namespace AskFm.BLL.DTO;
public class RefreshTokenDto
{
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 int ExpireAfter { get; set; }
public DateTime CreatedOn { get; set; }
}
2 changes: 1 addition & 1 deletion AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ public class AuthResponseDTO
{
public bool IsAuthenticated { get; set; }
public string Token { get; set; }
public RefreshToken RefreshToken { get; set; }
public RefreshTokenDto RefreshToken { get; set; }
public ReadUserDTO User { get; set; }
Comment on lines 8 to 10
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

Don’t return refresh tokens in API payloads.

Prefer HttpOnly, Secure cookies; if you must keep the property, mark it ignored for serialization.

 using AskFm.DAL.Models;
 
 namespace AskFm.BLL.DTO.UserDTOs;
 
 public class AuthResponseDTO
 {
     public bool IsAuthenticated { get; set; }
     public string Token { get; set; }
-    public RefreshTokenDto RefreshToken { get; set; }
+    [JsonIgnore] // avoid exposing via JSON
+    public RefreshTokenDto RefreshToken { get; set; }
     public ReadUserDTO User { get; set; }
 }

Add if not present:

using System.Text.Json.Serialization;
🤖 Prompt for AI Agents
In AskFm/AskFm.BLL/DTO/UserDTOs/AuthResponseDTO.cs lines 8-10, the DTO currently
exposes the RefreshToken property in API payloads; either remove this property
from the DTO or mark it to be ignored during JSON serialization and stop sending
refresh tokens in responses, add the required using
System.Text.Json.Serialization; prefer storing refresh tokens in HttpOnly,
Secure cookies on the server side instead of returning them in the response
body.

}
10 changes: 10 additions & 0 deletions AskFm/AskFm.BLL/DTO/UserDTOs/ForgotPasswordDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.ComponentModel.DataAnnotations;

namespace AskFm.BLL.DTO.UserDTOs;

public class ForgotPasswordDto
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
20 changes: 20 additions & 0 deletions AskFm/AskFm.BLL/DTO/UserDTOs/ResetPasswordDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;

namespace AskFm.BLL.DTO.UserDTOs;

public class ResetPasswordDto
{
[Required]
[EmailAddress]
public string Email { get; set; }

[Required]
public string Token { get; set; }

[Required]
public string NewPassword { get; set; }

[Required]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
Loading
Loading