diff --git a/OptixServe.Api/Configuration/AppSettings.cs b/OptixServe.Api/Configuration/AppSettings.cs index 751b9eb..7a53c2f 100644 --- a/OptixServe.Api/Configuration/AppSettings.cs +++ b/OptixServe.Api/Configuration/AppSettings.cs @@ -4,6 +4,7 @@ public record OptixServeSettings { public ApiSettings? Api { get; set; } = new(); public DatabaseSettings? Database { get; set; } = new(); + public JwtSettings? Jwt { get; set; } = new(); } public record ApiSettings @@ -12,6 +13,14 @@ public record ApiSettings public int? Port { get; set; } = 10086; } +public record JwtSettings +{ + public string Secret { get; set; } = string.Empty; + public string Issuer { get; set; } = "OptixServe"; + public string Audience { get; set; } = "OptixServeUsers"; + public int TokenExpirationMinutes { get; set; } = 60; +} + public enum DatabaseType { Sqlite, diff --git a/OptixServe.Api/Dtos/Auth.cs b/OptixServe.Api/Dtos/Auth.cs new file mode 100644 index 0000000..c867991 --- /dev/null +++ b/OptixServe.Api/Dtos/Auth.cs @@ -0,0 +1,12 @@ +namespace OptixServe.Api.Dtos; + +public record LoginRequestDto +{ + public string? UserName { get; set; } + public string? Password { get; set; } +} + +public record LoginResponseDto +{ + public string? Token { get; set; } +} \ No newline at end of file diff --git a/OptixServe.Api/Endpoints/UserEndpoint.cs b/OptixServe.Api/Endpoints/UserEndpoint.cs index 10ad026..24abe86 100644 --- a/OptixServe.Api/Endpoints/UserEndpoint.cs +++ b/OptixServe.Api/Endpoints/UserEndpoint.cs @@ -1,12 +1,16 @@ using System.Text.Json.Serialization; using OptixServe.Core.Services; using OptixServe.Api.Dtos; +using OptixServe.Api.Services; +using Microsoft.AspNetCore.Authorization; namespace OptixServe.Api.Endpoints; [JsonSerializable(typeof(UserDto))] [JsonSerializable(typeof(IEnumerable))] +[JsonSerializable(typeof(LoginRequestDto))] +[JsonSerializable(typeof(LoginResponseDto))] // For returning the token string public partial class UserJsonContext : JsonSerializerContext { } public static class UserEndpoint @@ -15,8 +19,28 @@ public static class UserEndpoint { var group = parentGroup.MapGroup("/users"); - group.MapGet("/", GetAllUsers); - group.MapGet("/{id}", GetUserById); + group.MapPost("/login", LoginUser); + group.MapGet("/", GetAllUsers).RequireAuthorization(); + group.MapGet("/{id}", GetUserById).RequireAuthorization(); + } + + public static IResult LoginUser(LoginRequestDto loginRequest, IUserService userService, ITokenService tokenService) + { + if (string.IsNullOrEmpty(loginRequest.UserName) || string.IsNullOrEmpty(loginRequest.Password)) + { + return Results.BadRequest("Username and password are required."); + } + + // Password hashing and salting will be implemented later. + var user = userService.GetUserByUsername(loginRequest.UserName); + + if (user == null || user.Password != loginRequest.Password) + { + return Results.Unauthorized(); + } + + var token = tokenService.GenerateToken(user); + return Results.Ok(new LoginResponseDto { Token = token }); } public static IResult GetAllUsers(IUserService userService) @@ -34,4 +58,4 @@ public static class UserEndpoint return Results.Ok(new UserDto { Id = user.Id, UserName = user.UserName }); } -} \ No newline at end of file +} diff --git a/OptixServe.Api/OptixServe.Api.csproj b/OptixServe.Api/OptixServe.Api.csproj index f69bfae..c3e18e4 100644 --- a/OptixServe.Api/OptixServe.Api.csproj +++ b/OptixServe.Api/OptixServe.Api.csproj @@ -5,6 +5,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/OptixServe.Api/Program.cs b/OptixServe.Api/Program.cs index 8da4d29..6d25783 100644 --- a/OptixServe.Api/Program.cs +++ b/OptixServe.Api/Program.cs @@ -1,6 +1,10 @@ using System.CommandLine; +using System.Text; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; using OptixServe.Api.Configuration; using OptixServe.Api.Endpoints; +using OptixServe.Api.Services; using OptixServe.Core.Data; using OptixServe.Core.Services; using OptixServe.Api.Utilites; @@ -50,6 +54,9 @@ class Program var app = builder.Build(); + app.UseAuthentication(); + app.UseAuthorization(); + using (var scope = app.Services.CreateScope()) { var initializer = scope.ServiceProvider.GetRequiredService(); @@ -127,6 +134,25 @@ static class StartupHelper // Application services builder.Services.AddScoped(); + builder.Services.AddScoped(); + + // Add Authentication and Authorization + builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + var jwtSettings = onConfigSettings?.Jwt ?? throw new ArgumentNullException(nameof(builder), "JWT settings are not configured."); + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings.Issuer, + ValidAudience = jwtSettings.Audience, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.Secret)) + }; + }); + builder.Services.AddAuthorization(); } /// diff --git a/OptixServe.Api/Services/TokenService.cs b/OptixServe.Api/Services/TokenService.cs new file mode 100644 index 0000000..f124f92 --- /dev/null +++ b/OptixServe.Api/Services/TokenService.cs @@ -0,0 +1,44 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using OptixServe.Api.Configuration; +using OptixServe.Core.Models; + +namespace OptixServe.Api.Services; + +public interface ITokenService +{ + public string GenerateToken(User user); +} + +public class TokenService(IOptions optixServeSettings) : ITokenService +{ + private readonly JwtSettings _jwtSettings = optixServeSettings.Value.Jwt ?? throw new ArgumentNullException(nameof(optixServeSettings), "JWT settings are not configured."); + + public string GenerateToken(User user) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = Encoding.ASCII.GetBytes(_jwtSettings.Secret); + + var claims = new List + { + new (ClaimTypes.NameIdentifier, user.Id.ToString()), + new (ClaimTypes.Name, user.UserName) + // Add roles if applicable: new Claim(ClaimTypes.Role, user.Role) + }; + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(claims), + Expires = DateTime.UtcNow.AddMinutes(_jwtSettings.TokenExpirationMinutes), + Issuer = _jwtSettings.Issuer, + Audience = _jwtSettings.Audience, + SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } +} \ No newline at end of file diff --git a/OptixServe.Api/appsettings.json b/OptixServe.Api/appsettings.json index 0d75ce5..e4a86f3 100644 --- a/OptixServe.Api/appsettings.json +++ b/OptixServe.Api/appsettings.json @@ -14,6 +14,12 @@ "Database": { "Type": "Sqlite", "Host": "optixserve.db" + }, + "Jwt": { + "Secret": "YOUR_SECRET_KEY_HERE_DO_NOT_SHARE_THIS_AND_MAKE_IT_LONG_ENOUGH", + "Issuer": "OptixServe", + "Audience": "OptixServeUsers", + "TokenExpirationMinutes": 60 } } -} +} \ No newline at end of file diff --git a/OptixServe.Core/Services/UserService.cs b/OptixServe.Core/Services/UserService.cs index 01c4d7d..394f05c 100644 --- a/OptixServe.Core/Services/UserService.cs +++ b/OptixServe.Core/Services/UserService.cs @@ -7,6 +7,7 @@ public interface IUserService { IEnumerable GetUsers(); User? GetUserById(string id); + User? GetUserByUsername(string username); } public class UserService(AppDbContext dbContext) : IUserService @@ -18,6 +19,11 @@ public class UserService(AppDbContext dbContext) : IUserService return _dbContext.Users.FirstOrDefault(u => u.Id == id); } + public User? GetUserByUsername(string username) + { + return _dbContext.Users.FirstOrDefault(u => u.UserName == username); + } + public IEnumerable GetUsers() { return _dbContext.Users.AsEnumerable();