diff --git a/api/Migrations/20260321203739_UseIdentityPlatform.Designer.cs b/api/Migrations/20260321203739_UseIdentityPlatform.Designer.cs
new file mode 100644
index 0000000..7c70f29
--- /dev/null
+++ b/api/Migrations/20260321203739_UseIdentityPlatform.Designer.cs
@@ -0,0 +1,84 @@
+//
+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
+ {
+ ///
+ 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("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
+ .HasColumnType("integer");
+
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("text");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("Email")
+ .HasColumnType("text");
+
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("text");
+
+ b.Property("NormalizedUserName")
+ .HasColumnType("text");
+
+ b.Property("PasswordHash")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.ToTable("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/api/Migrations/20260321203739_UseIdentityPlatform.cs b/api/Migrations/20260321203739_UseIdentityPlatform.cs
new file mode 100644
index 0000000..6f022b9
--- /dev/null
+++ b/api/Migrations/20260321203739_UseIdentityPlatform.cs
@@ -0,0 +1,221 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace agologum_api.Migrations
+{
+ ///
+ public partial class UseIdentityPlatform : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Name",
+ table: "Users");
+
+ migrationBuilder.DropColumn(
+ name: "Role",
+ table: "Users");
+
+ migrationBuilder.AlterColumn(
+ name: "PasswordHash",
+ table: "Users",
+ type: "text",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.AlterColumn(
+ name: "Email",
+ table: "Users",
+ type: "text",
+ nullable: true,
+ oldClrType: typeof(string),
+ oldType: "text");
+
+ migrationBuilder.AlterColumn(
+ name: "Id",
+ table: "Users",
+ type: "text",
+ nullable: false,
+ oldClrType: typeof(int),
+ oldType: "integer")
+ .OldAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ migrationBuilder.AddColumn(
+ name: "AccessFailedCount",
+ table: "Users",
+ type: "integer",
+ nullable: false,
+ defaultValue: 0);
+
+ migrationBuilder.AddColumn(
+ name: "ConcurrencyStamp",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "EmailConfirmed",
+ table: "Users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "LockoutEnabled",
+ table: "Users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "LockoutEnd",
+ table: "Users",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "NormalizedEmail",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "NormalizedUserName",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "PhoneNumber",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "PhoneNumberConfirmed",
+ table: "Users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "SecurityStamp",
+ table: "Users",
+ type: "text",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "TwoFactorEnabled",
+ table: "Users",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "UserName",
+ table: "Users",
+ type: "text",
+ nullable: true);
+ }
+
+ ///
+ 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(
+ name: "PasswordHash",
+ table: "Users",
+ type: "text",
+ nullable: false,
+ defaultValue: "",
+ oldClrType: typeof(string),
+ oldType: "text",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "Email",
+ table: "Users",
+ type: "text",
+ nullable: false,
+ defaultValue: "",
+ oldClrType: typeof(string),
+ oldType: "text",
+ oldNullable: true);
+
+ migrationBuilder.AlterColumn(
+ name: "Id",
+ table: "Users",
+ type: "integer",
+ nullable: false,
+ oldClrType: typeof(string),
+ oldType: "text")
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
+
+ migrationBuilder.AddColumn(
+ name: "Name",
+ table: "Users",
+ type: "text",
+ nullable: false,
+ defaultValue: "");
+
+ migrationBuilder.AddColumn(
+ name: "Role",
+ table: "Users",
+ type: "text",
+ nullable: false,
+ defaultValue: "");
+ }
+ }
+}
diff --git a/api/Migrations/AppDbContextModelSnapshot.cs b/api/Migrations/AppDbContextModelSnapshot.cs
index 472f8c1..b84960f 100644
--- a/api/Migrations/AppDbContextModelSnapshot.cs
+++ b/api/Migrations/AppDbContextModelSnapshot.cs
@@ -23,29 +23,52 @@ namespace agologum_api.Migrations
modelBuilder.Entity("agologumApi.Models.User", b =>
{
- b.Property("Id")
- .ValueGeneratedOnAdd()
+ b.Property("Id")
+ .HasColumnType("text");
+
+ b.Property("AccessFailedCount")
.HasColumnType("integer");
- NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+ b.Property("ConcurrencyStamp")
+ .HasColumnType("text");
b.Property("CreatedAt")
.HasColumnType("timestamp with time zone");
b.Property("Email")
- .IsRequired()
.HasColumnType("text");
- b.Property("Name")
- .IsRequired()
+ b.Property("EmailConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("LockoutEnd")
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("NormalizedEmail")
+ .HasColumnType("text");
+
+ b.Property("NormalizedUserName")
.HasColumnType("text");
b.Property("PasswordHash")
- .IsRequired()
.HasColumnType("text");
- b.Property("Role")
- .IsRequired()
+ b.Property("PhoneNumber")
+ .HasColumnType("text");
+
+ b.Property("PhoneNumberConfirmed")
+ .HasColumnType("boolean");
+
+ b.Property("SecurityStamp")
+ .HasColumnType("text");
+
+ b.Property("TwoFactorEnabled")
+ .HasColumnType("boolean");
+
+ b.Property("UserName")
.HasColumnType("text");
b.HasKey("Id");
diff --git a/api/Program.cs b/api/Program.cs
index a05ead5..fbf5bc5 100644
--- a/api/Program.cs
+++ b/api/Program.cs
@@ -3,8 +3,11 @@ using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using System.Text;
+using agologumApi.Models;
using agologumApi.Services;
var builder = WebApplication.CreateBuilder(args);
@@ -22,15 +25,20 @@ builder.Services.AddScoped();
builder.Services.AddScoped();
// configuration for jwt authentication
+builder.Services.AddIdentity()
+ .AddEntityFrameworkStores()
+ .AddDefaultTokenProviders();
builder.Services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.TokenValidationParameters = new TokenValidationParameters {
- ValidateIssuer = false,
- ValidateAudience = false,
+ ValidateIssuer = true,
+ ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
+ ValidIssuer = "agologum",
+ ValidAudience = "agologum",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key))
};
});
diff --git a/api/agologum-api.csproj b/api/agologum-api.csproj
index 82bd216..cfe18b4 100644
--- a/api/agologum-api.csproj
+++ b/api/agologum-api.csproj
@@ -10,6 +10,7 @@
+
diff --git a/api/src/Controllers/AuthController.cs b/api/src/Controllers/AuthController.cs
index 6cb3db8..ba2ae21 100644
--- a/api/src/Controllers/AuthController.cs
+++ b/api/src/Controllers/AuthController.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Identity;
using agologumApi.Models;
using agologumApi.Services;
@@ -9,11 +10,17 @@ using agologumApi.Services;
[Route("api/[controller]")]
public class AuthController : ControllerBase {
+ // identity things
+ private readonly UserManager userManager_;
+ private readonly SignInManager signInManager_;
+
private readonly UserService users_;
private readonly JwtService jwt_;
- public AuthController(UserService users, JwtService jwt)
- {
+ public AuthController(UserManager userManager, SignInManager signInManager, UserService users, JwtService jwt) {
+
+ userManager_ = userManager;
+ signInManager_ = signInManager;
users_ = users;
jwt_ = jwt;
}
@@ -21,10 +28,9 @@ public class AuthController : ControllerBase {
[HttpPost("register")]
public async Task Register(RegisterDto dto) {
var user = new User {
- Name = dto.Username,
+ UserName = dto.UserName,
Email = dto.Email,
PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.Password), // TODO: secondary hashing stage in client
- Role = "user",
CreatedAt = DateTime.UtcNow // yeah why not utc
};
@@ -39,11 +45,13 @@ public class AuthController : ControllerBase {
[HttpPost("login")]
public async Task 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)) {
- return Unauthorized();
- }
+ if (user == null) return Unauthorized();
+
+ var result = await signInManager_.CheckPasswordSignInAsync(user, dto.Password, false);
+
+ if(!result.Succeeded) return Unauthorized();
var token = jwt_.GenerateJwt(user);
diff --git a/api/src/Models/Dto.cs b/api/src/Models/Dto.cs
index 7403c6f..a740896 100644
--- a/api/src/Models/Dto.cs
+++ b/api/src/Models/Dto.cs
@@ -1,7 +1,7 @@
public class RegisterDto {
- public string Name { get; set; } = "";
+ public string UserName { get; set; } = "";
public string Email { get; set; } = "";
public string Password { get; set; } = "";
@@ -9,7 +9,7 @@ public class RegisterDto {
public class LoginDto {
- public string Name { get; set; } = "";
+ public string UserName { get; set; } = "";
public string Password { get; set; } = "";
}
diff --git a/api/src/Models/User.cs b/api/src/Models/User.cs
index ab3618a..2488222 100644
--- a/api/src/Models/User.cs
+++ b/api/src/Models/User.cs
@@ -1,13 +1,32 @@
+using Microsoft.AspNetCore.Identity;
+
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; }
+ // 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
+ */
};
diff --git a/api/src/Services/JwtService.cs b/api/src/Services/JwtService.cs
index 14e0b69..a79188e 100644
--- a/api/src/Services/JwtService.cs
+++ b/api/src/Services/JwtService.cs
@@ -22,13 +22,17 @@ public class JwtService {
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+ if(user.UserName == null) return null;
+
// not too sure
var claims = new[] {
- new Claim(ClaimTypes.Name, user.Name),
+ new Claim(ClaimTypes.Name, user.UserName),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())
};
var token = new JwtSecurityToken(
+ issuer: "agologum",
+ audience: "agologum",
claims: claims,
expires: DateTime.UtcNow.AddHours(2), // will add a refresher later
signingCredentials: creds
diff --git a/api/src/Services/UserService.cs b/api/src/Services/UserService.cs
index 1f9f247..1e6c104 100644
--- a/api/src/Services/UserService.cs
+++ b/api/src/Services/UserService.cs
@@ -22,7 +22,7 @@ public class UserService {
}
public async Task Get(string username) {
- return await db_.Users.FirstOrDefaultAsync(u => u.Name == username);
+ return await db_.Users.FirstOrDefaultAsync(u => u.UserName == username);
}
public async Task Create(User user) {