diff --git a/GermanApp/Application/DTOs/Auth/AuthResponse.cs b/GermanApp/Application/DTOs/Auth/AuthResponse.cs
new file mode 100644
index 0000000..4e32bb2
--- /dev/null
+++ b/GermanApp/Application/DTOs/Auth/AuthResponse.cs
@@ -0,0 +1,13 @@
+namespace GermanApp.Application.DTOs.Auth;
+
+///
+/// DTO for authentication response containing JWT token.
+///
+public record AuthResponse
+{
+ public int UserId { get; init; }
+ public string Username { get; init; } = string.Empty;
+ public string Email { get; init; } = string.Empty;
+ public string Token { get; init; } = string.Empty;
+ public DateTime ExpiresAt { get; init; }
+}
diff --git a/GermanApp/Application/DTOs/Auth/LoginDto.cs b/GermanApp/Application/DTOs/Auth/LoginDto.cs
new file mode 100644
index 0000000..3be9a34
--- /dev/null
+++ b/GermanApp/Application/DTOs/Auth/LoginDto.cs
@@ -0,0 +1,16 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace GermanApp.Application.DTOs.Auth;
+
+///
+/// DTO for user login.
+///
+public record LoginDto
+{
+ [Required]
+ [EmailAddress]
+ public string Email { get; init; } = string.Empty;
+
+ [Required]
+ public string Password { get; init; } = string.Empty;
+}
diff --git a/GermanApp/Application/DTOs/Auth/RegisterDto.cs b/GermanApp/Application/DTOs/Auth/RegisterDto.cs
new file mode 100644
index 0000000..a0b046d
--- /dev/null
+++ b/GermanApp/Application/DTOs/Auth/RegisterDto.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace GermanApp.Application.DTOs.Auth;
+
+///
+/// DTO for user registration.
+///
+public record RegisterDto
+{
+ [Required]
+ [StringLength(50, MinimumLength = 3)]
+ public string Username { get; init; } = string.Empty;
+
+ [Required]
+ [EmailAddress]
+ [StringLength(100)]
+ public string Email { get; init; } = string.Empty;
+
+ [Required]
+ [StringLength(100, MinimumLength = 8)]
+ public string Password { get; init; } = string.Empty;
+}
diff --git a/GermanApp/Application/Interfaces/IAuthService.cs b/GermanApp/Application/Interfaces/IAuthService.cs
new file mode 100644
index 0000000..6b0dd75
--- /dev/null
+++ b/GermanApp/Application/Interfaces/IAuthService.cs
@@ -0,0 +1,32 @@
+using GermanApp.Application.DTOs.Auth;
+using GermanApp.Domain.Entities;
+
+namespace GermanApp.Application.Interfaces;
+
+///
+/// Interface for authentication services.
+/// Part of the Application layer.
+///
+public interface IAuthService
+{
+ ///
+ /// Registers a new user.
+ ///
+ /// User registration data
+ /// Authentication response with token
+ Task RegisterAsync(RegisterDto registerDto);
+
+ ///
+ /// Authenticates a user and returns a JWT token.
+ ///
+ /// User login data
+ /// Authentication response with token
+ Task LoginAsync(LoginDto loginDto);
+
+ ///
+ /// Gets the current authenticated user.
+ ///
+ /// User ID from token claims
+ /// The user entity
+ Task GetCurrentUserAsync(int userId);
+}
diff --git a/GermanApp/GermanApp.csproj b/GermanApp/GermanApp.csproj
index bed793f..7b3fb89 100644
--- a/GermanApp/GermanApp.csproj
+++ b/GermanApp/GermanApp.csproj
@@ -20,6 +20,9 @@
all
runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
diff --git a/GermanApp/Infrastructure/Services/AuthService.cs b/GermanApp/Infrastructure/Services/AuthService.cs
new file mode 100644
index 0000000..ad6f361
--- /dev/null
+++ b/GermanApp/Infrastructure/Services/AuthService.cs
@@ -0,0 +1,132 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using GermanApp.Application.DTOs.Auth;
+using GermanApp.Application.Interfaces;
+using GermanApp.Domain.Entities;
+using GermanApp.Infrastructure.Data.DbContext;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.IdentityModel.Tokens;
+
+namespace GermanApp.Infrastructure.Services;
+
+///
+/// Authentication service implementation.
+/// Part of the Infrastructure layer.
+///
+public class AuthService : IAuthService
+{
+ private readonly AppDbContext _dbContext;
+ private readonly IPasswordHasher _passwordHasher;
+ private readonly IConfiguration _configuration;
+
+ public AuthService(
+ AppDbContext dbContext,
+ IPasswordHasher passwordHasher,
+ IConfiguration configuration)
+ {
+ _dbContext = dbContext;
+ _passwordHasher = passwordHasher;
+ _configuration = configuration;
+ }
+
+ ///
+ /// Registers a new user.
+ ///
+ public async Task RegisterAsync(RegisterDto registerDto)
+ {
+ // Check if username or email already exists
+ if (await _dbContext.Users.AnyAsync(u => u.Username == registerDto.Username))
+ throw new InvalidOperationException("Username already taken");
+
+ if (await _dbContext.Users.AnyAsync(u => u.Email == registerDto.Email))
+ throw new InvalidOperationException("Email already in use");
+
+ // Hash password and create user
+ var user = User.Create(registerDto.Username, registerDto.Email.ToLowerInvariant(), string.Empty);
+ var passwordHash = _passwordHasher.HashPassword(user, registerDto.Password);
+ user.ChangePassword(passwordHash);
+
+ _dbContext.Users.Add(user);
+ await _dbContext.SaveChangesAsync();
+
+ // Generate JWT token
+ var token = GenerateJwtToken(user);
+
+ return new AuthResponse
+ {
+ UserId = user.Id,
+ Username = user.Username,
+ Email = user.Email,
+ Token = token,
+ ExpiresAt = DateTime.UtcNow.AddHours(24)
+ };
+ }
+
+ ///
+ /// Authenticates a user and returns a JWT token.
+ ///
+ public async Task LoginAsync(LoginDto loginDto)
+ {
+ var user = await _dbContext.Users.FirstOrDefaultAsync(u => u.Email == loginDto.Email);
+
+ if (user == null)
+ throw new UnauthorizedAccessException("Invalid email or password");
+
+ // Verify password
+ var result = _passwordHasher.VerifyHashedPassword(user, user.PasswordHash, loginDto.Password);
+ if (result == PasswordVerificationResult.Failed)
+ throw new UnauthorizedAccessException("Invalid email or password");
+
+ // Generate JWT token
+ var token = GenerateJwtToken(user);
+
+ return new AuthResponse
+ {
+ UserId = user.Id,
+ Username = user.Username,
+ Email = user.Email,
+ Token = token,
+ ExpiresAt = DateTime.UtcNow.AddHours(24)
+ };
+ }
+
+ ///
+ /// Gets the current authenticated user.
+ ///
+ public async Task GetCurrentUserAsync(int userId)
+ {
+ return await _dbContext.Users.FirstOrDefaultAsync(u => u.Id == userId);
+ }
+
+ ///
+ /// Generates a JWT token for the given user.
+ ///
+ private string GenerateJwtToken(User user)
+ {
+ var securityKey = new SymmetricSecurityKey(
+ Encoding.UTF8.GetBytes(_configuration["Jwt:Key"] ?? "super-secret-key-at-least-32-characters"));
+
+ var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
+ new Claim(ClaimTypes.Name, user.Username),
+ new Claim(ClaimTypes.Email, user.Email),
+ new Claim(ClaimTypes.Role, "User")
+ };
+
+ var token = new JwtSecurityToken(
+ issuer: _configuration["Jwt:Issuer"] ?? "DeutschLernen",
+ audience: _configuration["Jwt:Audience"] ?? "DeutschLernen",
+ claims: claims,
+ expires: DateTime.UtcNow.AddHours(24),
+ signingCredentials: credentials
+ );
+
+ return new JwtSecurityTokenHandler().WriteToken(token);
+ }
+}
diff --git a/GermanApp/Presentation/Controllers/AuthController.cs b/GermanApp/Presentation/Controllers/AuthController.cs
new file mode 100644
index 0000000..9633d9c
--- /dev/null
+++ b/GermanApp/Presentation/Controllers/AuthController.cs
@@ -0,0 +1,106 @@
+using GermanApp.Application.DTOs.Auth;
+using GermanApp.Application.Interfaces;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using System.Net;
+
+namespace GermanApp.Presentation.Controllers;
+
+///
+/// Controller for authentication endpoints.
+/// Part of the Presentation layer.
+///
+[ApiController]
+[Route("api/[controller]")]
+public class AuthController : ControllerBase
+{
+ private readonly IAuthService _authService;
+
+ public AuthController(IAuthService authService)
+ {
+ _authService = authService;
+ }
+
+ ///
+ /// Register a new user.
+ ///
+ /// Registration data
+ /// Authentication response with JWT token
+ [HttpPost("register")]
+ [ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(string), (int)HttpStatusCode.BadRequest)]
+ public async Task Register([FromBody] RegisterDto registerDto)
+ {
+ try
+ {
+ var result = await _authService.RegisterAsync(registerDto);
+ return Ok(result);
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(ex.Message);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode((int)HttpStatusCode.InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Login an existing user.
+ ///
+ /// Login data
+ /// Authentication response with JWT token
+ [HttpPost("login")]
+ [ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
+ [ProducesResponseType(typeof(string), (int)HttpStatusCode.Unauthorized)]
+ public async Task Login([FromBody] LoginDto loginDto)
+ {
+ try
+ {
+ var result = await _authService.LoginAsync(loginDto);
+ return Ok(result);
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ return Unauthorized(ex.Message);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode((int)HttpStatusCode.InternalServerError, ex.Message);
+ }
+ }
+
+ ///
+ /// Get current authenticated user information.
+ ///
+ /// Current user information
+ [HttpGet("me")]
+ [Authorize]
+ [ProducesResponseType(typeof(AuthResponse), (int)HttpStatusCode.OK)]
+ [ProducesResponseType((int)HttpStatusCode.Unauthorized)]
+ public async Task GetCurrentUser()
+ {
+ try
+ {
+ var userId = int.Parse(User.FindFirst("nameid")?.Value ?? "0");
+ if (userId == 0)
+ return Unauthorized();
+
+ var user = await _authService.GetCurrentUserAsync(userId);
+ if (user == null)
+ return Unauthorized();
+
+ return Ok(new AuthResponse
+ {
+ UserId = user.Id,
+ Username = user.Username,
+ Email = user.Email
+ });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode((int)HttpStatusCode.InternalServerError, ex.Message);
+ }
+ }
+}
diff --git a/GermanApp/Program.cs b/GermanApp/Program.cs
index 014fb9c..b1df8d5 100644
--- a/GermanApp/Program.cs
+++ b/GermanApp/Program.cs
@@ -1,13 +1,21 @@
using GermanApp.Application.DTOs;
+using GermanApp.Application.Interfaces;
using GermanApp.Application.UseCases.Commands;
+using GermanApp.Domain.Entities;
using GermanApp.Domain.Interfaces;
using GermanApp.Infrastructure.Data.DbContext;
using GermanApp.Infrastructure.Data.Repositories;
using GermanApp.Infrastructure.Data.SeedData;
+using GermanApp.Infrastructure.Services;
+using GermanApp.Presentation.Controllers;
using GermanApp.Presentation.Endpoints;
using GermanApp.Shared.Middleware;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
+using Microsoft.IdentityModel.Tokens;
using Serilog;
+using System.Text;
// Configure Serilog
Log.Logger = new LoggerConfiguration()
@@ -36,6 +44,41 @@ try
builder.Services.AddHealthChecks()
.AddDbContextCheck();
+ // Add Password Hasher for custom User entity
+ builder.Services.AddScoped, PasswordHasher>();
+
+ // Configure JWT Authentication
+ var jwtKey = builder.Configuration["Jwt:Key"] ?? "super-secret-key-at-least-32-characters";
+ var jwtIssuer = builder.Configuration["Jwt:Issuer"] ?? "DeutschLernen";
+ var jwtAudience = builder.Configuration["Jwt:Audience"] ?? "DeutschLernen";
+
+ builder.Services.AddAuthentication(options =>
+ {
+ options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
+ options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
+ })
+ .AddJwtBearer(options =>
+ {
+ options.TokenValidationParameters = new TokenValidationParameters
+ {
+ ValidateIssuer = true,
+ ValidateAudience = true,
+ ValidateLifetime = true,
+ ValidateIssuerSigningKey = true,
+ ValidIssuer = jwtIssuer,
+ ValidAudience = jwtAudience,
+ IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtKey)),
+ ClockSkew = TimeSpan.Zero
+ };
+ });
+
+ // Add Authorization
+ builder.Services.AddAuthorization();
+
+ // Register AuthService
+ builder.Services.AddScoped();
+
// Configure CORS
builder.Services.AddCors(options =>
{
@@ -94,12 +137,21 @@ try
app.UseSwaggerUI();
}
+ // Use Authentication & Authorization
+ app.UseAuthentication();
+ app.UseAuthorization();
+
// Use CORS
app.UseCors("AllowAll");
// Use Health Checks
app.MapHealthChecks("/health");
+ // Map Auth endpoints
+ app.MapControllerRoute(
+ name: "api",
+ pattern: "api/{controller}/{action}/{id?}" );
+
// Seed database with initial data
app.SeedDatabase();
diff --git a/GermanApp/appsettings.json b/GermanApp/appsettings.json
index 7b19f94..41ed9f9 100644
--- a/GermanApp/appsettings.json
+++ b/GermanApp/appsettings.json
@@ -8,5 +8,11 @@
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=localhost;Port=5432;Database=DeutschLernen;Username=postgres;Password=postgres"
+ },
+ "Jwt": {
+ "Key": "your-super-secret-key-at-least-32-characters-long",
+ "Issuer": "DeutschLernen",
+ "Audience": "DeutschLernen",
+ "ExpireHours": 24
}
}