Inject roles on token validation

This commit is contained in:
lumijiez
2025-06-08 00:17:01 +03:00
parent 87c4f27de5
commit ab9b80b74f
12 changed files with 137 additions and 38 deletions

View File

@@ -9,7 +9,7 @@ services:
- ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT} - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT}
- ConnectionStrings__DefaultConnection=Server=${SQL_SERVER};Database=${SQL_DATABASE};User Id=${SQL_USER_ID};Password=${SQL_PASSWORD};Encrypt=false;TrustServerCertificate=true;MultipleActiveResultSets=true; - ConnectionStrings__DefaultConnection=Server=${SQL_SERVER};Database=${SQL_DATABASE};User Id=${SQL_USER_ID};Password=${SQL_PASSWORD};Encrypt=false;TrustServerCertificate=true;MultipleActiveResultSets=true;
- ASPNETCORE_URLS=${ASPNETCORE_URLS} - ASPNETCORE_URLS=${ASPNETCORE_URLS}
- Auth0__Authority=${AUTH0_DOMAIN} - Auth0__Authority=${AUTH0_AUTHORITY}
- Auth0__Audience=${AUTH0_AUDIENCE} - Auth0__Audience=${AUTH0_AUDIENCE}
- Logging__LogLevel__Default=${ASPNETCORE_LOGGING_LEVEL_DEFAULT} - Logging__LogLevel__Default=${ASPNETCORE_LOGGING_LEVEL_DEFAULT}
- Logging__LogLevel__Microsoft.AspNetCore=${ASPNETCORE_LOGGING_LEVEL} - Logging__LogLevel__Microsoft.AspNetCore=${ASPNETCORE_LOGGING_LEVEL}
@@ -37,6 +37,7 @@ services:
- AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET} - AUTH0_CLIENT_SECRET=${AUTH0_CLIENT_SECRET}
- AUTH0_AUDIENCE=${AUTH0_AUDIENCE} - AUTH0_AUDIENCE=${AUTH0_AUDIENCE}
- AUTH0_SCOPE=${AUTH0_SCOPE} - AUTH0_SCOPE=${AUTH0_SCOPE}
- COOKIE_DOMAIN=${COOKIE_DOMAIN}
- NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL}
- NEXT_PUBLIC_AUTH0_CLIENT_ID=${NEXT_PUBLIC_AUTH0_CLIENT_ID} - NEXT_PUBLIC_AUTH0_CLIENT_ID=${NEXT_PUBLIC_AUTH0_CLIENT_ID}
- NEXT_PUBLIC_AUTH0_DOMAIN=${NEXT_PUBLIC_AUTH0_DOMAIN} - NEXT_PUBLIC_AUTH0_DOMAIN=${NEXT_PUBLIC_AUTH0_DOMAIN}

View File

@@ -21,7 +21,7 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
public DbSet<OrderStatus> OrderStatuses { get; set; } public DbSet<OrderStatus> OrderStatuses { get; set; }
public DbSet<ShippingStatus> ShippingStatuses { get; set; } public DbSet<ShippingStatus> ShippingStatuses { get; set; }
public DbSet<User> Users { get; set; } public DbSet<User> Users { get; set; }
public DbSet<UserRole> UserRoles { get; set; } public DbSet<UserRole> UserRole { get; set; }
public DbSet<Role> Roles { get; set; } public DbSet<Role> Roles { get; set; }
public DbSet<Category> Categories { get; set; } public DbSet<Category> Categories { get; set; }

View File

@@ -12,7 +12,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Imprink.Infrastructure.Migrations namespace Imprink.Infrastructure.Migrations
{ {
[DbContext(typeof(ApplicationDbContext))] [DbContext(typeof(ApplicationDbContext))]
[Migration("20250606173957_InitialSetup")] [Migration("20250607211109_InitialSetup")]
partial class InitialSetup partial class InitialSetup
{ {
/// <inheritdoc /> /// <inheritdoc />
@@ -825,7 +825,7 @@ namespace Imprink.Infrastructure.Migrations
b.HasIndex("UserId") b.HasIndex("UserId")
.HasDatabaseName("IX_UserRole_UserId"); .HasDatabaseName("IX_UserRole_UserId");
b.ToTable("UserRoles"); b.ToTable("UserRole");
}); });
modelBuilder.Entity("Imprink.Domain.Entities.Orders.Order", b => modelBuilder.Entity("Imprink.Domain.Entities.Orders.Order", b =>

View File

@@ -193,7 +193,7 @@ namespace Imprink.Infrastructure.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "UserRoles", name: "UserRole",
columns: table => new columns: table => new
{ {
UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false), UserId = table.Column<string>(type: "nvarchar(450)", maxLength: 450, nullable: false),
@@ -201,15 +201,15 @@ namespace Imprink.Infrastructure.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); table.PrimaryKey("PK_UserRole", x => new { x.UserId, x.RoleId });
table.ForeignKey( table.ForeignKey(
name: "FK_UserRoles_Roles_RoleId", name: "FK_UserRole_Roles_RoleId",
column: x => x.RoleId, column: x => x.RoleId,
principalTable: "Roles", principalTable: "Roles",
principalColumn: "Id", principalColumn: "Id",
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Restrict);
table.ForeignKey( table.ForeignKey(
name: "FK_UserRoles_Users_UserId", name: "FK_UserRole_Users_UserId",
column: x => x.UserId, column: x => x.UserId,
principalTable: "Users", principalTable: "Users",
principalColumn: "Id", principalColumn: "Id",
@@ -630,12 +630,12 @@ namespace Imprink.Infrastructure.Migrations
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_UserRole_RoleId", name: "IX_UserRole_RoleId",
table: "UserRoles", table: "UserRole",
column: "RoleId"); column: "RoleId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_UserRole_UserId", name: "IX_UserRole_UserId",
table: "UserRoles", table: "UserRole",
column: "UserId"); column: "UserId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
@@ -663,7 +663,7 @@ namespace Imprink.Infrastructure.Migrations
name: "OrderItems"); name: "OrderItems");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "UserRoles"); name: "UserRole");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Orders"); name: "Orders");

View File

@@ -822,7 +822,7 @@ namespace Imprink.Infrastructure.Migrations
b.HasIndex("UserId") b.HasIndex("UserId")
.HasDatabaseName("IX_UserRole_UserId"); .HasDatabaseName("IX_UserRole_UserId");
b.ToTable("UserRoles"); b.ToTable("UserRole");
}); });
modelBuilder.Entity("Imprink.Domain.Entities.Orders.Order", b => modelBuilder.Entity("Imprink.Domain.Entities.Orders.Order", b =>

View File

@@ -9,7 +9,7 @@ public class UserRoleRepository(ApplicationDbContext context) : IUserRoleReposit
{ {
public async Task<IEnumerable<Role>> GetUserRolesAsync(string userId, CancellationToken cancellationToken = default) public async Task<IEnumerable<Role>> GetUserRolesAsync(string userId, CancellationToken cancellationToken = default)
{ {
return await context.UserRoles return await context.UserRole
.AsNoTracking() .AsNoTracking()
.Where(ur => ur.UserId == userId) .Where(ur => ur.UserId == userId)
.Select(ur => ur.Role) .Select(ur => ur.Role)
@@ -18,7 +18,7 @@ public class UserRoleRepository(ApplicationDbContext context) : IUserRoleReposit
public async Task<IEnumerable<User>> GetUsersInRoleAsync(Guid roleId, CancellationToken cancellationToken = default) public async Task<IEnumerable<User>> GetUsersInRoleAsync(Guid roleId, CancellationToken cancellationToken = default)
{ {
return await context.UserRoles return await context.UserRole
.AsNoTracking() .AsNoTracking()
.Where(ur => ur.RoleId == roleId) .Where(ur => ur.RoleId == roleId)
.Select(ur => ur.User) .Select(ur => ur.User)
@@ -27,32 +27,32 @@ public class UserRoleRepository(ApplicationDbContext context) : IUserRoleReposit
public async Task<bool> IsUserInRoleAsync(string userId, Guid roleId, CancellationToken cancellationToken = default) public async Task<bool> IsUserInRoleAsync(string userId, Guid roleId, CancellationToken cancellationToken = default)
{ {
return await context.UserRoles return await context.UserRole
.AnyAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken); .AnyAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken);
} }
public async Task<UserRole?> GetUserRoleAsync(string userId, Guid roleId, CancellationToken cancellationToken = default) public async Task<UserRole?> GetUserRoleAsync(string userId, Guid roleId, CancellationToken cancellationToken = default)
{ {
return await context.UserRoles return await context.UserRole
.AsNoTracking() .AsNoTracking()
.FirstOrDefaultAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken); .FirstOrDefaultAsync(ur => ur.UserId == userId && ur.RoleId == roleId, cancellationToken);
} }
public async Task AddUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default) public async Task AddUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default)
{ {
context.UserRoles.Add(userRole); context.UserRole.Add(userRole);
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
} }
public async Task RemoveUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default) public async Task RemoveUserRoleAsync(UserRole userRole, CancellationToken cancellationToken = default)
{ {
context.UserRoles.Remove(userRole); context.UserRole.Remove(userRole);
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
} }
public async Task<IEnumerable<UserRole>> GetUserRolesByUserIdAsync(string userId, CancellationToken cancellationToken = default) public async Task<IEnumerable<UserRole>> GetUserRolesByUserIdAsync(string userId, CancellationToken cancellationToken = default)
{ {
return await context.UserRoles return await context.UserRole
.AsNoTracking() .AsNoTracking()
.Where(ur => ur.UserId == userId) .Where(ur => ur.UserId == userId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);

View File

@@ -1,5 +1,3 @@
using System.Security.Claims;
using Imprink.Domain.Common.Models;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@@ -15,16 +13,11 @@ public class UserController : ControllerBase
{ {
var claims = User.Claims; var claims = User.Claims;
var enumerable = claims as Claim[] ?? claims.ToArray(); foreach (var claim in claims)
var user = new Auth0User
{ {
Sub = enumerable.FirstOrDefault(c => c.Type == "sub")?.Value ?? "", Console.WriteLine($"Claim Type: {claim.Type}, Claim Value: {claim.Value}");
Name = enumerable.FirstOrDefault(c => c.Type == "name")?.Value ?? "", }
Nickname = enumerable.FirstOrDefault(c => c.Type == "nickname")?.Value ?? "",
Email = enumerable.FirstOrDefault(c => c.Type == "email")?.Value ?? "",
EmailVerified = enumerable.FirstOrDefault(c => c.Type == "email_verified")?.Value == "true"
};
return Ok(user); return Ok("Claims logged to console.");
} }
} }

View File

@@ -1,3 +1,4 @@
using System.Security.Claims;
using Imprink.Application; using Imprink.Application;
using Imprink.Application.Products.Create; using Imprink.Application.Products.Create;
using Imprink.Domain.Repositories; using Imprink.Domain.Repositories;
@@ -41,6 +42,34 @@ public static class Startup
{ {
options.Authority = builder.Configuration["Auth0:Authority"]; options.Authority = builder.Configuration["Auth0:Authority"];
options.Audience = builder.Configuration["Auth0:Audience"]; options.Audience = builder.Configuration["Auth0:Audience"];
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var token = context.Request.Cookies["access_token"];
if (!string.IsNullOrEmpty(token)) context.Token = token;
return Task.CompletedTask;
},
OnTokenValidated = context =>
{
var dbContext = context.HttpContext.RequestServices.GetService<ApplicationDbContext>();
var userId = context.Principal?.FindFirst(ClaimTypes.NameIdentifier)?.Value
?? context.Principal?.FindFirst("sub")?.Value;
if (string.IsNullOrEmpty(userId)) return Task.CompletedTask;
var identity = context.Principal!.Identity as ClaimsIdentity;
var roles = (from ur in dbContext?.UserRole
join r in dbContext?.Roles on ur.RoleId equals r.Id
where ur.UserId == userId
select r.RoleName).ToList();
foreach (var role in roles) identity!.AddClaim(new Claim(ClaimTypes.Role, role));
return Task.CompletedTask;
}
};
}); });
services.AddAuthorization(); services.AddAuthorization();

View File

@@ -8,7 +8,7 @@
"name": "webui", "name": "webui",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@auth0/nextjs-auth0": "^4.6.0", "@auth0/nextjs-auth0": "^4.6.1",
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"
@@ -46,9 +46,9 @@
} }
}, },
"node_modules/@auth0/nextjs-auth0": { "node_modules/@auth0/nextjs-auth0": {
"version": "4.6.0", "version": "4.6.1",
"resolved": "https://registry.npmjs.org/@auth0/nextjs-auth0/-/nextjs-auth0-4.6.0.tgz", "resolved": "https://registry.npmjs.org/@auth0/nextjs-auth0/-/nextjs-auth0-4.6.1.tgz",
"integrity": "sha512-HK+fcUW6P8/qUDQfOfntftMg6yzeZLtyfTxL/lyeOub1o/xTL9SZ2fF39nH0H6w1loB5SCAbyN1vD8xxBwINqQ==", "integrity": "sha512-eSYLCPBzROheJL0gdI0hHCbV468yqyz/sBcuag7cm3dx6LMhRzzFmComPs8p+Y7OCblzblGfk/Hju8A1BkjZxw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@edge-runtime/cookies": "^5.0.1", "@edge-runtime/cookies": "^5.0.1",

View File

@@ -9,7 +9,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@auth0/nextjs-auth0": "^4.6.0", "@auth0/nextjs-auth0": "^4.6.1",
"next": "15.3.3", "next": "15.3.3",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0"

View File

@@ -5,9 +5,31 @@ import {useEffect, useState} from "react";
export default function Home() { export default function Home() {
const { user, error, isLoading } = useUser(); const { user, error, isLoading } = useUser();
const [accessToken, setAccessToken] = useState(null);
useEffect(() => {
const fetchAccessToken = async () => {
if (user) {
try {
const response = await fetch('/auth/access-token');
const v = await fetch('/token');
if (response.ok) {
const tokenData = await response.text();
setAccessToken(tokenData);
} else {
setAccessToken('Token not available');
}
} catch (error) {
setAccessToken('Error fetching token');
}
}
};
fetchAccessToken().then(r => console.log(r));
}, [user]);
async function checkValidity() { async function checkValidity() {
const check = await fetch('https://impr.ink/auth/sync', {method: 'POST'}); const check = await fetch('https://impr.ink/api/api/User', {method: 'POST'});
} }
if (isLoading) { if (isLoading) {
@@ -39,6 +61,16 @@ export default function Home() {
Sign In Sign In
</span> </span>
</a> </a>
<a
onClick={() => checkValidity()}
className="group relative px-6 py-3 bg-gradient-to-r from-red-500 to-pink-500 rounded-xl font-bold text-white shadow-2xl hover:shadow-red-500/25 transition-all duration-300 hover:scale-105 active:scale-95"
>
<div
className="absolute inset-0 bg-gradient-to-r from-red-600 to-pink-600 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
<span className="relative flex items-center gap-2">
Check
</span>
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -52,7 +84,8 @@ export default function Home() {
{user ? ( {user ? (
<div className="w-full max-w-5xl"> <div className="w-full max-w-5xl">
<div className="text-center mb-6"> <div className="text-center mb-6">
<div className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full mb-3 shadow-2xl"> <div
className="inline-flex items-center justify-center w-16 h-16 bg-gradient-to-r from-purple-500 to-blue-500 rounded-full mb-3 shadow-2xl">
{user.picture ? ( {user.picture ? (
<img <img
src={user.picture} src={user.picture}
@@ -114,6 +147,15 @@ export default function Home() {
</div> </div>
</div> </div>
)} )}
<div>
<label
className="text-purple-300 text-xs font-semibold uppercase tracking-wider">Access
Token</label>
<div
className="text-white/80 text-xs mt-1 p-2 bg-black/30 rounded-lg border border-white/10 font-mono break-all max-h-24 overflow-auto">
{accessToken}
</div>
</div>
</div> </div>
<div> <div>

View File

@@ -0,0 +1,34 @@
import { cookies } from 'next/headers';
import { NextResponse } from 'next/server';
import {auth0} from "@/lib/auth0";
export async function GET() {
try {
const session = await auth0.getSession();
const accessToken = session.tokenSet.accessToken;
if (!accessToken) {
return NextResponse.json({ error: 'No access token found' }, { status: 401 });
}
const response = NextResponse.json({ message: 'Access token set in cookie' });
const cookieDomain = process.env.COOKIE_DOMAIN || undefined;
const cookieStore = await cookies();
cookieStore.set({
name: 'access_token',
value: accessToken,
httpOnly: true,
secure: true,
sameSite: 'strict',
path: '/',
domain: cookieDomain,
maxAge: 3600,
});
return response;
} catch (error) {
console.error('Error in /api/set-token:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}