From fe4002238021cba14fd9f7ca3c7106d891512a66 Mon Sep 17 00:00:00 2001 From: dp Date: Sat, 28 Feb 2026 05:11:25 +0100 Subject: [PATCH 1/6] Issue #16: Configured Program.cs for authentication. --- .../Authentication/AuthDbContext.cs | 20 +++++++++ src/SpanishByExample.Api/Program.cs | 45 ++++++++++++++++++- .../SpanishByExample.Api.csproj | 7 +++ .../SpanishByExample.Tests.csproj | 2 +- 4 files changed, 72 insertions(+), 2 deletions(-) create mode 100644 src/SpanishByExample.Api/Authentication/AuthDbContext.cs diff --git a/src/SpanishByExample.Api/Authentication/AuthDbContext.cs b/src/SpanishByExample.Api/Authentication/AuthDbContext.cs new file mode 100644 index 0000000..70182d6 --- /dev/null +++ b/src/SpanishByExample.Api/Authentication/AuthDbContext.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; + +namespace SpanishByExample.Api.Authentication; + +/// +/// This context is only for: +/// - Users +/// - Roles +/// - Identity tables +/// +public class AuthDbContext : IdentityDbContext +{ + public AuthDbContext(DbContextOptions options) + : base(options) + { + + } +} diff --git a/src/SpanishByExample.Api/Program.cs b/src/SpanishByExample.Api/Program.cs index 5a2a40f..05f533d 100644 --- a/src/SpanishByExample.Api/Program.cs +++ b/src/SpanishByExample.Api/Program.cs @@ -1,6 +1,12 @@ -using SpanishByExample.Core.Interfaces; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; +using SpanishByExample.Api.Authentication; using SpanishByExample.Business; +using SpanishByExample.Core.Interfaces; using SpanishByExample.DataAccess; +using System.Text; var builder = WebApplication.CreateBuilder(args); @@ -13,6 +19,8 @@ // dp: Added for Swagger support. builder.Services.AddSwaggerGen(); +RegisterAuthentication(builder); + RegisterServices(builder); var app = builder.Build(); @@ -29,6 +37,9 @@ app.UseHttpsRedirection(); +// dp: enable JWT Bearer authentication +app.UseAuthentication(); + app.UseAuthorization(); app.MapControllers(); @@ -39,4 +50,36 @@ static void RegisterServices(WebApplicationBuilder builder) { builder.Services.AddScoped(); builder.Services.AddScoped(); +} + +static void RegisterAuthentication(WebApplicationBuilder builder) +{ + // Register Identity + + builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnectionString"))); + + builder.Services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + + // Register JWT + + builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + options.TokenValidationParameters = new() + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = "SpanishByExample", + ValidAudience = "SpanishByExample", + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YourSuperSecretKeyHere")) + }); } \ No newline at end of file diff --git a/src/SpanishByExample.Api/SpanishByExample.Api.csproj b/src/SpanishByExample.Api/SpanishByExample.Api.csproj index 26b25b1..c34b340 100644 --- a/src/SpanishByExample.Api/SpanishByExample.Api.csproj +++ b/src/SpanishByExample.Api/SpanishByExample.Api.csproj @@ -7,7 +7,14 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/SpanishByExample.Tests/SpanishByExample.Tests.csproj b/tests/SpanishByExample.Tests/SpanishByExample.Tests.csproj index 6fa4e2d..fed68a2 100644 --- a/tests/SpanishByExample.Tests/SpanishByExample.Tests.csproj +++ b/tests/SpanishByExample.Tests/SpanishByExample.Tests.csproj @@ -10,7 +10,7 @@ - + From 319a90d2fe77c6bed361f22714e32de981d2c946 Mon Sep 17 00:00:00 2001 From: dp Date: Sat, 28 Feb 2026 05:45:04 +0100 Subject: [PATCH 2/6] Added Migrations/ folder to .gitignore. --- src/.gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/.gitignore b/src/.gitignore index df00081..9d1101c 100644 --- a/src/.gitignore +++ b/src/.gitignore @@ -1,2 +1,5 @@ # dp -appsettings.Development.json \ No newline at end of file +appsettings.Development.json + +# dp: EF Core Migrations +SpanishByExample.Api/Migrations/ From fd7cb5fa9ee156f69c710d0b1d7de115593b6acd Mon Sep 17 00:00:00 2001 From: dp Date: Mon, 2 Mar 2026 11:09:42 +0100 Subject: [PATCH 3/6] Issue #16: Implemented and tested authentication. --- .../Authentication/Dtos/LoginDto.cs | 12 +++ .../Authentication/Dtos/RegisterDto.cs | 15 ++++ .../Authentication/JwtOptions.cs | 18 +++++ .../Controllers/AuthController.cs | 79 +++++++++++++++++++ src/SpanishByExample.Api/Program.cs | 17 +++- src/SpanishByExample.Api/appsettings.json | 10 ++- 6 files changed, 147 insertions(+), 4 deletions(-) create mode 100644 src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs create mode 100644 src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs create mode 100644 src/SpanishByExample.Api/Authentication/JwtOptions.cs create mode 100644 src/SpanishByExample.Api/Controllers/AuthController.cs diff --git a/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs b/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs new file mode 100644 index 0000000..bbf9784 --- /dev/null +++ b/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace SpanishByExample.Api.Authentication.Dtos; + +public class LoginDto +{ + [Required] + public string Username { get; set; } = null!; + + [Required] + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs b/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs new file mode 100644 index 0000000..ec332a0 --- /dev/null +++ b/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace SpanishByExample.Api.Authentication.Dtos; + +public class RegisterDto +{ + [Required] + public string Username { get; set; } = null!; + + [Required, EmailAddress] + public string Email{ get; set; } = null!; + + [Required] + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/src/SpanishByExample.Api/Authentication/JwtOptions.cs b/src/SpanishByExample.Api/Authentication/JwtOptions.cs new file mode 100644 index 0000000..aa59bc0 --- /dev/null +++ b/src/SpanishByExample.Api/Authentication/JwtOptions.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +/// +/// JWT Options from the configuration. +/// +public class JwtOptions +{ + public const string SectionName = "Jwt"; + + [Required] + public string Key { get; set; } = null!; + + [Required] + public string Issuer { get; set; } = null!; + + [Required] + public string Audience { get; set; } = null!; +} \ No newline at end of file diff --git a/src/SpanishByExample.Api/Controllers/AuthController.cs b/src/SpanishByExample.Api/Controllers/AuthController.cs new file mode 100644 index 0000000..8cfe1f0 --- /dev/null +++ b/src/SpanishByExample.Api/Controllers/AuthController.cs @@ -0,0 +1,79 @@ +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.IdentityModel.Tokens; +using SpanishByExample.Api.Authentication.Dtos; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using Microsoft.Extensions.Options; + +namespace SpanishByExample.Api.Controllers; + +/// +/// Controller for authentication using ASP.NET Core Identity and JWT Bearer Authentication. +/// +[Route("api/[controller]")] +[ApiController] +public class AuthController(IOptions _jwtOptions, UserManager _userManager) : ControllerBase +{ + [HttpPost("register")] + public async Task Register(RegisterDto registerDto) + { + var user = new IdentityUser + { + UserName = registerDto.Username, + Email = registerDto.Email, + }; + + var result = await _userManager.CreateAsync(user, registerDto.Password); + + if (!result.Succeeded) + { + return BadRequest(result.Errors); + } + + return Ok(new { user.Id, user.UserName, user.Email }); + } + + [HttpPost("login")] + public async Task Login(LoginDto loginDto) + { + var user = await _userManager.FindByNameAsync(loginDto.Username); + + if (user == null || !await _userManager.CheckPasswordAsync(user, loginDto.Password)) + { + return Unauthorized(); + } + + var token = GenerateJwtToken(user); + + return Ok(new { token }); + } + + #region Private Methods + + private string GenerateJwtToken(IdentityUser user) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, user.Id) + }; + + var jwtConfig = _jwtOptions.Value; + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.Key)); + + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var token = new JwtSecurityToken( + issuer: jwtConfig.Issuer, + audience: jwtConfig.Audience, + claims: claims, + expires: DateTime.UtcNow.AddHours(2), + signingCredentials: creds); + + return new JwtSecurityTokenHandler().WriteToken(token); + } + + #endregion +} diff --git a/src/SpanishByExample.Api/Program.cs b/src/SpanishByExample.Api/Program.cs index 05f533d..b1d1e66 100644 --- a/src/SpanishByExample.Api/Program.cs +++ b/src/SpanishByExample.Api/Program.cs @@ -64,8 +64,19 @@ static void RegisterAuthentication(WebApplicationBuilder builder) .AddDefaultTokenProviders(); + // Read JWT Options from config + + builder.Services.AddOptions() + .Bind(builder.Configuration.GetRequiredSection(JwtOptions.SectionName)) + .ValidateDataAnnotations() + .ValidateOnStart(); + + var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName) + .Get() ?? throw new InvalidOperationException("Jwt configuration missing."); + + // Register JWT - + builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; @@ -78,8 +89,8 @@ static void RegisterAuthentication(WebApplicationBuilder builder) ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, - ValidIssuer = "SpanishByExample", - ValidAudience = "SpanishByExample", + ValidIssuer = jwtOptions.Issuer, + ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YourSuperSecretKeyHere")) }); } \ No newline at end of file diff --git a/src/SpanishByExample.Api/appsettings.json b/src/SpanishByExample.Api/appsettings.json index 10f68b8..7f1d394 100644 --- a/src/SpanishByExample.Api/appsettings.json +++ b/src/SpanishByExample.Api/appsettings.json @@ -1,9 +1,17 @@ { + "ConnectionStrings": { + "DefaultConnectionString": "" + }, "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "Jwt": { + "Key": "", + "Issuer": "SpanishByExample", + "Audience": "SpanishByExample" + } } From 2d4698d08bb411b5d65689d3a64f2da60f1aea7e Mon Sep 17 00:00:00 2001 From: dp Date: Thu, 5 Mar 2026 07:24:06 +0100 Subject: [PATCH 4/6] Removed Migrations/ from project file. --- src/SpanishByExample.Api/SpanishByExample.Api.csproj | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/SpanishByExample.Api/SpanishByExample.Api.csproj b/src/SpanishByExample.Api/SpanishByExample.Api.csproj index c34b340..4e0cfeb 100644 --- a/src/SpanishByExample.Api/SpanishByExample.Api.csproj +++ b/src/SpanishByExample.Api/SpanishByExample.Api.csproj @@ -6,6 +6,13 @@ enable + + + + + + + From f6ded99a6a56c9cf3522bca3fcde74918a2838ad Mon Sep 17 00:00:00 2001 From: dp Date: Thu, 5 Mar 2026 07:46:46 +0100 Subject: [PATCH 5/6] Code cleanup. --- .../Authentication/Dtos/LoginDto.cs | 3 +++ .../Authentication/Dtos/RegisterDto.cs | 3 +++ .../Controllers/AuthController.cs | 14 ++++++++++++++ src/SpanishByExample.Api/Program.cs | 9 ++++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs b/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs index bbf9784..d127d8a 100644 --- a/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs +++ b/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs @@ -2,6 +2,9 @@ namespace SpanishByExample.Api.Authentication.Dtos; +/// +/// DTO used by login action. +/// public class LoginDto { [Required] diff --git a/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs b/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs index ec332a0..7d40953 100644 --- a/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs +++ b/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs @@ -2,6 +2,9 @@ namespace SpanishByExample.Api.Authentication.Dtos; +/// +/// DTO used by register action. +/// public class RegisterDto { [Required] diff --git a/src/SpanishByExample.Api/Controllers/AuthController.cs b/src/SpanishByExample.Api/Controllers/AuthController.cs index 8cfe1f0..c4dd6b5 100644 --- a/src/SpanishByExample.Api/Controllers/AuthController.cs +++ b/src/SpanishByExample.Api/Controllers/AuthController.cs @@ -16,6 +16,13 @@ namespace SpanishByExample.Api.Controllers; [ApiController] public class AuthController(IOptions _jwtOptions, UserManager _userManager) : ControllerBase { + #region Actions + + /// + /// Registers a new user. + /// + /// User data. + /// Created user incl. ID. [HttpPost("register")] public async Task Register(RegisterDto registerDto) { @@ -35,6 +42,11 @@ public async Task Register(RegisterDto registerDto) return Ok(new { user.Id, user.UserName, user.Email }); } + /// + /// Log-in of user. + /// + /// User data. + /// JWT Bearer token. [HttpPost("login")] public async Task Login(LoginDto loginDto) { @@ -49,6 +61,8 @@ public async Task Login(LoginDto loginDto) return Ok(new { token }); } + + #endregion #region Private Methods diff --git a/src/SpanishByExample.Api/Program.cs b/src/SpanishByExample.Api/Program.cs index b1d1e66..08fe27e 100644 --- a/src/SpanishByExample.Api/Program.cs +++ b/src/SpanishByExample.Api/Program.cs @@ -1,3 +1,5 @@ +// TODO add author info + using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; @@ -46,6 +48,9 @@ app.Run(); +#region Private Methods + +// TODO ren to RegisterCustomServices static void RegisterServices(WebApplicationBuilder builder) { builder.Services.AddScoped(); @@ -93,4 +98,6 @@ static void RegisterAuthentication(WebApplicationBuilder builder) ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YourSuperSecretKeyHere")) }); -} \ No newline at end of file +} + +#endregion \ No newline at end of file From 42e6ddeb55f701e220ef405ee11dcc7df09a241c Mon Sep 17 00:00:00 2001 From: dp Date: Thu, 5 Mar 2026 07:57:38 +0100 Subject: [PATCH 6/6] Issue #16: Fix: Program reads appsettings.json. --- src/SpanishByExample.Api/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SpanishByExample.Api/Program.cs b/src/SpanishByExample.Api/Program.cs index 08fe27e..4be7de3 100644 --- a/src/SpanishByExample.Api/Program.cs +++ b/src/SpanishByExample.Api/Program.cs @@ -62,7 +62,7 @@ static void RegisterAuthentication(WebApplicationBuilder builder) // Register Identity builder.Services.AddDbContext(options => - options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnectionString"))); + options.UseSqlServer(builder.Configuration.GetConnectionString("SpanishByExampleDb"))); builder.Services.AddIdentity() .AddEntityFrameworkStores() @@ -96,7 +96,7 @@ static void RegisterAuthentication(WebApplicationBuilder builder) ValidateIssuerSigningKey = true, ValidIssuer = jwtOptions.Issuer, ValidAudience = jwtOptions.Audience, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("YourSuperSecretKeyHere")) + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Key)) }); }