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/
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/Authentication/Dtos/LoginDto.cs b/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs
new file mode 100644
index 0000000..d127d8a
--- /dev/null
+++ b/src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs
@@ -0,0 +1,15 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace SpanishByExample.Api.Authentication.Dtos;
+
+///
+/// DTO used by login action.
+///
+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..7d40953
--- /dev/null
+++ b/src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs
@@ -0,0 +1,18 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace SpanishByExample.Api.Authentication.Dtos;
+
+///
+/// DTO used by register action.
+///
+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..c4dd6b5
--- /dev/null
+++ b/src/SpanishByExample.Api/Controllers/AuthController.cs
@@ -0,0 +1,93 @@
+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
+{
+ #region Actions
+
+ ///
+ /// Registers a new user.
+ ///
+ /// User data.
+ /// Created user incl. ID.
+ [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 });
+ }
+
+ ///
+ /// Log-in of user.
+ ///
+ /// User data.
+ /// JWT Bearer token.
+ [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 });
+ }
+
+ #endregion
+
+ #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 5a2a40f..4be7de3 100644
--- a/src/SpanishByExample.Api/Program.cs
+++ b/src/SpanishByExample.Api/Program.cs
@@ -1,6 +1,14 @@
-using SpanishByExample.Core.Interfaces;
+// TODO add author info
+
+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 +21,8 @@
// dp: Added for Swagger support.
builder.Services.AddSwaggerGen();
+RegisterAuthentication(builder);
+
RegisterServices(builder);
var app = builder.Build();
@@ -29,14 +39,65 @@
app.UseHttpsRedirection();
+// dp: enable JWT Bearer authentication
+app.UseAuthentication();
+
app.UseAuthorization();
app.MapControllers();
app.Run();
+#region Private Methods
+
+// TODO ren to RegisterCustomServices
static void RegisterServices(WebApplicationBuilder builder)
{
builder.Services.AddScoped();
builder.Services.AddScoped();
-}
\ No newline at end of file
+}
+
+static void RegisterAuthentication(WebApplicationBuilder builder)
+{
+ // Register Identity
+
+ builder.Services.AddDbContext(options =>
+ options.UseSqlServer(builder.Configuration.GetConnectionString("SpanishByExampleDb")));
+
+ builder.Services.AddIdentity()
+ .AddEntityFrameworkStores()
+ .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;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ })
+ .AddJwtBearer(options =>
+ options.TokenValidationParameters = new()
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = jwtOptions.Issuer,
+ ValidAudience = jwtOptions.Audience,
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Key))
+ });
+}
+
+#endregion
\ No newline at end of file
diff --git a/src/SpanishByExample.Api/SpanishByExample.Api.csproj b/src/SpanishByExample.Api/SpanishByExample.Api.csproj
index 26b25b1..4e0cfeb 100644
--- a/src/SpanishByExample.Api/SpanishByExample.Api.csproj
+++ b/src/SpanishByExample.Api/SpanishByExample.Api.csproj
@@ -7,7 +7,21 @@
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
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"
+ }
}
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 @@
-
+