migrate to identity for authentication
All checks were successful
Build and Deploy API / build-and-deploy (push) Successful in 12s

This commit is contained in:
2026-03-21 15:38:05 -05:00
parent ef4f0c0159
commit 3dd0460209
10 changed files with 397 additions and 29 deletions

View File

@@ -0,0 +1,84 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace agologum_api.Migrations
{
[DbContext(typeof(AppDbContext))]
[Migration("20260321203739_UseIdentityPlatform")]
partial class UseIdentityPlatform
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.5")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("agologumApi.Models.User", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer");
b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property<string>("Email")
.HasColumnType("text");
b.Property<bool>("EmailConfirmed")
.HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasColumnType("text");
b.Property<string>("NormalizedUserName")
.HasColumnType("text");
b.Property<string>("PasswordHash")
.HasColumnType("text");
b.Property<string>("PhoneNumber")
.HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasColumnType("text");
b.HasKey("Id");
b.ToTable("Users");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,221 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace agologum_api.Migrations
{
/// <inheritdoc />
public partial class UseIdentityPlatform : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Name",
table: "Users");
migrationBuilder.DropColumn(
name: "Role",
table: "Users");
migrationBuilder.AlterColumn<string>(
name: "PasswordHash",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Email",
table: "Users",
type: "text",
nullable: true,
oldClrType: typeof(string),
oldType: "text");
migrationBuilder.AlterColumn<string>(
name: "Id",
table: "Users",
type: "text",
nullable: false,
oldClrType: typeof(int),
oldType: "integer")
.OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
migrationBuilder.AddColumn<int>(
name: "AccessFailedCount",
table: "Users",
type: "integer",
nullable: false,
defaultValue: 0);
migrationBuilder.AddColumn<string>(
name: "ConcurrencyStamp",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "EmailConfirmed",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<bool>(
name: "LockoutEnabled",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<DateTimeOffset>(
name: "LockoutEnd",
table: "Users",
type: "timestamp with time zone",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "NormalizedEmail",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "NormalizedUserName",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "PhoneNumber",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "PhoneNumberConfirmed",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "SecurityStamp",
table: "Users",
type: "text",
nullable: true);
migrationBuilder.AddColumn<bool>(
name: "TwoFactorEnabled",
table: "Users",
type: "boolean",
nullable: false,
defaultValue: false);
migrationBuilder.AddColumn<string>(
name: "UserName",
table: "Users",
type: "text",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AccessFailedCount",
table: "Users");
migrationBuilder.DropColumn(
name: "ConcurrencyStamp",
table: "Users");
migrationBuilder.DropColumn(
name: "EmailConfirmed",
table: "Users");
migrationBuilder.DropColumn(
name: "LockoutEnabled",
table: "Users");
migrationBuilder.DropColumn(
name: "LockoutEnd",
table: "Users");
migrationBuilder.DropColumn(
name: "NormalizedEmail",
table: "Users");
migrationBuilder.DropColumn(
name: "NormalizedUserName",
table: "Users");
migrationBuilder.DropColumn(
name: "PhoneNumber",
table: "Users");
migrationBuilder.DropColumn(
name: "PhoneNumberConfirmed",
table: "Users");
migrationBuilder.DropColumn(
name: "SecurityStamp",
table: "Users");
migrationBuilder.DropColumn(
name: "TwoFactorEnabled",
table: "Users");
migrationBuilder.DropColumn(
name: "UserName",
table: "Users");
migrationBuilder.AlterColumn<string>(
name: "PasswordHash",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "Email",
table: "Users",
type: "text",
nullable: false,
defaultValue: "",
oldClrType: typeof(string),
oldType: "text",
oldNullable: true);
migrationBuilder.AlterColumn<int>(
name: "Id",
table: "Users",
type: "integer",
nullable: false,
oldClrType: typeof(string),
oldType: "text")
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
migrationBuilder.AddColumn<string>(
name: "Name",
table: "Users",
type: "text",
nullable: false,
defaultValue: "");
migrationBuilder.AddColumn<string>(
name: "Role",
table: "Users",
type: "text",
nullable: false,
defaultValue: "");
}
}
}

View File

@@ -23,29 +23,52 @@ namespace agologum_api.Migrations
modelBuilder.Entity("agologumApi.Models.User", b => modelBuilder.Entity("agologumApi.Models.User", b =>
{ {
b.Property<int>("Id") b.Property<string>("Id")
.ValueGeneratedOnAdd() .HasColumnType("text");
b.Property<int>("AccessFailedCount")
.HasColumnType("integer"); .HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id")); b.Property<string>("ConcurrencyStamp")
.HasColumnType("text");
b.Property<DateTime>("CreatedAt") b.Property<DateTime>("CreatedAt")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("Email") b.Property<string>("Email")
.IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Name") b.Property<bool>("EmailConfirmed")
.IsRequired() .HasColumnType("boolean");
b.Property<bool>("LockoutEnabled")
.HasColumnType("boolean");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("timestamp with time zone");
b.Property<string>("NormalizedEmail")
.HasColumnType("text");
b.Property<string>("NormalizedUserName")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("PasswordHash") b.Property<string>("PasswordHash")
.IsRequired()
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("Role") b.Property<string>("PhoneNumber")
.IsRequired() .HasColumnType("text");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("boolean");
b.Property<string>("SecurityStamp")
.HasColumnType("text");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("boolean");
b.Property<string>("UserName")
.HasColumnType("text"); .HasColumnType("text");
b.HasKey("Id"); b.HasKey("Id");

View File

@@ -3,8 +3,11 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens; using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Text; using System.Text;
using agologumApi.Models;
using agologumApi.Services; using agologumApi.Services;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@@ -22,15 +25,20 @@ builder.Services.AddScoped<UserService>();
builder.Services.AddScoped<JwtService>(); builder.Services.AddScoped<JwtService>();
// configuration for jwt authentication // configuration for jwt authentication
builder.Services.AddIdentity<User, IdentityRole>()
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
builder.Services.AddAuthentication(options => { builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => { }).AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters { options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = false, ValidateIssuer = true,
ValidateAudience = false, ValidateAudience = true,
ValidateLifetime = true, ValidateLifetime = true,
ValidateIssuerSigningKey = true, ValidateIssuerSigningKey = true,
ValidIssuer = "agologum",
ValidAudience = "agologum",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)) IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
}; };
}); });

View File

@@ -10,6 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="BCrypt.Net-Next" Version="4.1.0" /> <PackageReference Include="BCrypt.Net-Next" Version="4.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.5">

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using agologumApi.Models; using agologumApi.Models;
using agologumApi.Services; using agologumApi.Services;
@@ -9,11 +10,17 @@ using agologumApi.Services;
[Route("api/[controller]")] [Route("api/[controller]")]
public class AuthController : ControllerBase { public class AuthController : ControllerBase {
// identity things
private readonly UserManager<User> userManager_;
private readonly SignInManager<User> signInManager_;
private readonly UserService users_; private readonly UserService users_;
private readonly JwtService jwt_; private readonly JwtService jwt_;
public AuthController(UserService users, JwtService jwt) public AuthController(UserManager<User> userManager, SignInManager<User> signInManager, UserService users, JwtService jwt) {
{
userManager_ = userManager;
signInManager_ = signInManager;
users_ = users; users_ = users;
jwt_ = jwt; jwt_ = jwt;
} }
@@ -21,10 +28,9 @@ public class AuthController : ControllerBase {
[HttpPost("register")] [HttpPost("register")]
public async Task<ActionResult> Register(RegisterDto dto) { public async Task<ActionResult> Register(RegisterDto dto) {
var user = new User { var user = new User {
Name = dto.Username, UserName = dto.UserName,
Email = dto.Email, Email = dto.Email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password), // TODO: secondary hashing stage in client PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password), // TODO: secondary hashing stage in client
Role = "user",
CreatedAt = DateTime.UtcNow // yeah why not utc CreatedAt = DateTime.UtcNow // yeah why not utc
}; };
@@ -39,11 +45,13 @@ public class AuthController : ControllerBase {
[HttpPost("login")] [HttpPost("login")]
public async Task<ActionResult> Login(LoginDto dto) public async Task<ActionResult> Login(LoginDto dto)
{ {
var user = await users_.Get(dto.Username); var user = await users_.Get(dto.UserName);
if (user == null || !BCrypt.Net.BCrypt.Verify(dto.Password, user.PasswordHash)) { if (user == null) return Unauthorized();
return Unauthorized();
} var result = await signInManager_.CheckPasswordSignInAsync(user, dto.Password, false);
if(!result.Succeeded) return Unauthorized();
var token = jwt_.GenerateJwt(user); var token = jwt_.GenerateJwt(user);

View File

@@ -1,7 +1,7 @@
public class RegisterDto { public class RegisterDto {
public string Name { get; set; } = ""; public string UserName { get; set; } = "";
public string Email { get; set; } = ""; public string Email { get; set; } = "";
public string Password { get; set; } = ""; public string Password { get; set; } = "";
@@ -9,7 +9,7 @@ public class RegisterDto {
public class LoginDto { public class LoginDto {
public string Name { get; set; } = ""; public string UserName { get; set; } = "";
public string Password { get; set; } = ""; public string Password { get; set; } = "";
} }

View File

@@ -1,13 +1,32 @@
using Microsoft.AspNetCore.Identity;
namespace agologumApi.Models; namespace agologumApi.Models;
public class User { public class User : IdentityUser {
public int Id { get; set; }
public string Name { get; set; } = "";
public string Email { get; set; } = "";
public string PasswordHash { get; set; } = "";
public string Role { get; set; } = "";
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
// properties inherited from IdentityUser:
/*
AccessFailedCount: Gets or sets the number of failed login attempts for the current user.
Claims: Navigation property for the claims this user possesses.
ConcurrencyStamp: A random value that must change whenever a user is persisted to the store
Email: Gets or sets the email address for this user.
EmailConfirmed: Gets or sets a flag indicating if a user has confirmed their email address.
Id: Gets or sets the primary key for this user.
LockoutEnabled: Gets or sets a flag indicating if the user could be locked out.
LockoutEnd: Gets or sets the date and time, in UTC, when any user lockout ends.
Logins: Navigation property for this users login accounts.
NormalizedEmail: Gets or sets the normalized email address for this user.
NormalizedUserName: Gets or sets the normalized user name for this user.
PasswordHash: Gets or sets a salted and hashed representation of the password for this user.
PhoneNumber: Gets or sets a telephone number for the user.
PhoneNumberConfirmed: Gets or sets a flag indicating if a user has confirmed their telephone address.
Roles: Navigation property for the roles this user belongs to.
SecurityStamp: A random value that must change whenever a users credentials change (password changed, login removed)
TwoFactorEnabled: Gets or sets a flag indicating if two factor authentication is enabled for this user.
UserName: Gets or sets the user name for this user.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identityuser?view=aspnetcore-1.1
*/
}; };

View File

@@ -22,13 +22,17 @@ public class JwtService {
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
if(user.UserName == null) return null;
// not too sure // not too sure
var claims = new[] { var claims = new[] {
new Claim(ClaimTypes.Name, user.Name), new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()) new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
}; };
var token = new JwtSecurityToken( var token = new JwtSecurityToken(
issuer: "agologum",
audience: "agologum",
claims: claims, claims: claims,
expires: DateTime.UtcNow.AddHours(2), // will add a refresher later expires: DateTime.UtcNow.AddHours(2), // will add a refresher later
signingCredentials: creds signingCredentials: creds

View File

@@ -22,7 +22,7 @@ public class UserService {
} }
public async Task<User?> Get(string username) { public async Task<User?> Get(string username) {
return await db_.Users.FirstOrDefaultAsync(u => u.Name == username); return await db_.Users.FirstOrDefaultAsync(u => u.UserName == username);
} }
public async Task<User> Create(User user) { public async Task<User> Create(User user) {