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
5 changes: 4 additions & 1 deletion src/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
# dp
appsettings.Development.json
appsettings.Development.json

# dp: EF Core Migrations
SpanishByExample.Api/Migrations/
20 changes: 20 additions & 0 deletions src/SpanishByExample.Api/Authentication/AuthDbContext.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace SpanishByExample.Api.Authentication;

/// <summary>
/// This context is only for:
/// - Users
/// - Roles
/// - Identity tables
/// </summary>
public class AuthDbContext : IdentityDbContext<IdentityUser>
{
public AuthDbContext(DbContextOptions<AuthDbContext> options)
: base(options)
{

}
}
15 changes: 15 additions & 0 deletions src/SpanishByExample.Api/Authentication/Dtos/LoginDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;

namespace SpanishByExample.Api.Authentication.Dtos;

/// <summary>
/// DTO used by login action.
/// </summary>
public class LoginDto
{
[Required]
public string Username { get; set; } = null!;

[Required]
public string Password { get; set; } = null!;
}
18 changes: 18 additions & 0 deletions src/SpanishByExample.Api/Authentication/Dtos/RegisterDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

namespace SpanishByExample.Api.Authentication.Dtos;

/// <summary>
/// DTO used by register action.
/// </summary>
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!;
}
18 changes: 18 additions & 0 deletions src/SpanishByExample.Api/Authentication/JwtOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

/// <summary>
/// JWT Options from the configuration.
/// </summary>
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!;
}
93 changes: 93 additions & 0 deletions src/SpanishByExample.Api/Controllers/AuthController.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for authentication using ASP.NET Core Identity and JWT Bearer Authentication.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public class AuthController(IOptions<JwtOptions> _jwtOptions, UserManager<IdentityUser> _userManager) : ControllerBase
{
#region Actions

/// <summary>
/// Registers a new user.
/// </summary>
/// <param name="registerDto">User data.</param>
/// <returns>Created user incl. ID.</returns>
[HttpPost("register")]
public async Task<IActionResult> 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 });
}

/// <summary>
/// Log-in of user.
/// </summary>
/// <param name="loginDto">User data.</param>
/// <returns>JWT Bearer token.</returns>
[HttpPost("login")]
public async Task<IActionResult> 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
}
65 changes: 63 additions & 2 deletions src/SpanishByExample.Api/Program.cs
Original file line number Diff line number Diff line change
@@ -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);

Expand All @@ -13,6 +21,8 @@
// dp: Added for Swagger support.
builder.Services.AddSwaggerGen();

RegisterAuthentication(builder);

RegisterServices(builder);

var app = builder.Build();
Expand All @@ -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<IExamplesService, ExamplesService>();
builder.Services.AddScoped<IDataAccessService, DataAccessService>();
}
}

static void RegisterAuthentication(WebApplicationBuilder builder)
{
// Register Identity

builder.Services.AddDbContext<AuthDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("SpanishByExampleDb")));

builder.Services.AddIdentity<IdentityUser, IdentityRole>()
.AddEntityFrameworkStores<AuthDbContext>()
.AddDefaultTokenProviders();


// Read JWT Options from config

builder.Services.AddOptions<JwtOptions>()
.Bind(builder.Configuration.GetRequiredSection(JwtOptions.SectionName))
.ValidateDataAnnotations()
.ValidateOnStart();

var jwtOptions = builder.Configuration.GetSection(JwtOptions.SectionName)
.Get<JwtOptions>() ?? 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
14 changes: 14 additions & 0 deletions src/SpanishByExample.Api/SpanishByExample.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,21 @@
</PropertyGroup>

<ItemGroup>
<Compile Remove="Migrations\**" />
<Content Remove="Migrations\**" />
<EmbeddedResource Remove="Migrations\**" />
<None Remove="Migrations\**" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.0" />
</ItemGroup>

Expand Down
10 changes: 9 additions & 1 deletion src/SpanishByExample.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
{
"ConnectionStrings": {
"DefaultConnectionString": ""
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Jwt": {
"Key": "",
"Issuer": "SpanishByExample",
"Audience": "SpanishByExample"
}
}
2 changes: 1 addition & 1 deletion tests/SpanishByExample.Tests/SpanishByExample.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.3" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
Expand Down