Skip to content

Auth0 Integration for .NET and Angular

Developer/Agent Implementation Plan: Auth0 Integration for ASP.NET Core API and Angular UI

Section titled “Developer/Agent Implementation Plan: Auth0 Integration for ASP.NET Core API and Angular UI”

This plan is written for developers or coding agents implementing Auth0 in a .NET API plus Angular frontend.

Target stack:

Frontend: Angular
Backend: ASP.NET Core Minimal API
Auth provider: Auth0
Login UX: Auth0 Universal Login
Identity providers: Google first, Facebook later
Authorization: Auth0 scopes/permissions + app database ownership checks
Database: app-owned Users table keyed by Auth0 subject claim

The API side is implemented for the current Obaki architecture with these adaptations:

[x] JWT bearer package and central package version added
[x] Auth0 options added under `Auth0`
[x] Auth0 validation is disabled by default with `Auth0:Enabled=false`
[x] When enabled, API validates Auth0 issuer, audience, signing key, and lifetime
[x] CORS options added under `Cors`
[x] Existing API header security allows browser Bearer-token requests only on configured auth paths and allows CORS preflight
[x] `/api/me` creates or refreshes a local app user from Auth0 `sub`
[x] `/api/me/token` is available only when development Scalar endpoints are enabled
[x] `users` table migration added with unique `auth0_user_id`
[x] Authenticated, own-notification, and admin policy names exist
[ ] Angular integration is still a separate UI slice
[x] Account-owned notification subscription endpoints require authenticated users

Current Obaki deviation from this generic plan:

The `/api/notification-subscriptions` endpoints now require authenticated users.
Ownership is enforced by the authenticated subject-derived user key, not by trusting the public subscription id alone.

Primary references used while preparing this plan:


[ ] Angular can sign in through Auth0 Universal Login
[ ] Angular can sign out
[ ] Angular can read authenticated UI state
[ ] Angular sends Auth0 access token to API
[ ] API validates issuer, audience, signature, expiry
[ ] API exposes protected endpoints
[ ] API can identify current user from `sub`
[ ] API creates local user row on first authenticated request
[ ] Notification-rule endpoints are protected
[ ] Admin-only endpoints are separated from normal user endpoints
[ ] Google login enabled
[ ] Facebook login can be enabled without code changes
[ ] CORS locked to Angular origin
[ ] Secrets are not committed
[ ] Production and local Auth0 settings are separated
[ ] Basic permission policy exists
[ ] Tests cover 401, 403, and authenticated success paths
[ ] Do not build custom password login
[ ] Do not store Auth0 client secret in Angular
[ ] Do not use email as primary user identity
[ ] Do not trust frontend-only authorization
[ ] Do not log raw JWTs
[ ] Do not accept tokens without validating audience
[ ] Do not send ID token to API as the API credential

Angular
Auth0 Angular SDK
AuthGuard
HTTP interceptor
Access token with API audience
|
v
ASP.NET Core API
JWT Bearer middleware
Auth0 issuer + audience validation
Authorization policies
CurrentUser service
User bootstrap/sync service
|
v
Database
Users
NotificationRules
Admin/domain tables

Auth0 owns authentication.

The application owns authorization rules that are specific to the community tool.

Examples:

Auth0 says who the caller is.
Your API decides whether that caller can create a notification rule.
Your database decides whether that notification rule belongs to that caller.

Before coding, these Auth0 values must exist.

Application name: Obaki Web
Application type: Single Page Application
Allowed Callback URLs:
http://localhost:4200
https://YOUR_FRONTEND_DOMAIN
Allowed Logout URLs:
http://localhost:4200
https://YOUR_FRONTEND_DOMAIN
Allowed Web Origins:
http://localhost:4200
https://YOUR_FRONTEND_DOMAIN

Required frontend values:

AUTH0_DOMAIN
AUTH0_CLIENT_ID
AUTH0_AUDIENCE
API name: Obaki API
Identifier/Audience: https://api.jjosh.dev
Signing algorithm: RS256

Required backend values:

Auth0:Domain
Auth0:Audience

Start with:

read:me
manage:own_notifications
admin:manage_notifications

Do not create a huge permission list early.


4. Backend implementation plan: ASP.NET Core API

Section titled “4. Backend implementation plan: ASP.NET Core API”

Install JWT bearer authentication:

Terminal window
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Optional if the project uses OpenAPI:

Terminal window
dotnet add package Swashbuckle.AspNetCore

appsettings.Development.json:

{
"Auth0": {
"Enabled": true,
"Domain": "YOUR_TENANT.REGION.auth0.com",
"Audience": "https://api.jjosh.dev",
"ClaimNamespace": "https://jjosh.dev/claims"
},
"Cors": {
"AllowedOrigins": [
"http://localhost:4200"
]
}
}

Production environment variables:

Auth0__Enabled=true
Auth0__Domain=YOUR_TENANT.REGION.auth0.com
Auth0__Audience=https://api.jjosh.dev
Auth0__ClaimNamespace=https://jjosh.dev/claims
Cors__AllowedOrigins__0=https://YOUR_FRONTEND_DOMAIN

Do not commit production secrets or provider client secrets.

The API does not need the Auth0 SPA client secret.

public sealed class Auth0Options
{
public required string Domain { get; init; }
public required string Audience { get; init; }
}

Register:

builder.Services.Configure<Auth0Options>(builder.Configuration.GetSection("Auth0"));

Program.cs:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
var auth0Domain = builder.Configuration["Auth0:Domain"]
?? throw new InvalidOperationException("Missing Auth0:Domain.");
var auth0Audience = builder.Configuration["Auth0:Audience"]
?? throw new InvalidOperationException("Missing Auth0:Audience.");
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = $"https://{auth0Domain}/";
options.Audience = auth0Audience;
options.MapInboundClaims = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
NameClaimType = "sub"
};
});
builder.Services.AddAuthorization();

Pipeline order:

var app = builder.Build();
app.UseHttpsRedirection();
app.UseCors("Frontend");
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/health", () => Results.Ok(new { status = "ok" }));
app.Run();

UseAuthentication() must run before UseAuthorization().

var allowedOrigins = builder.Configuration
.GetSection("Cors:AllowedOrigins")
.Get<string[]>() ?? [];
builder.Services.AddCors(options =>
{
options.AddPolicy("Frontend", policy =>
{
policy
.WithOrigins(allowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod();
});
});

Do not use AllowAnyOrigin() with authenticated browser APIs.

using System.Security.Claims;
public interface ICurrentUser
{
bool IsAuthenticated { get; }
string Auth0UserId { get; }
}
public sealed class CurrentUser(IHttpContextAccessor httpContextAccessor) : ICurrentUser
{
public bool IsAuthenticated =>
httpContextAccessor.HttpContext?.User.Identity?.IsAuthenticated == true;
public string Auth0UserId
{
get
{
var user = httpContextAccessor.HttpContext?.User;
var sub = user?.FindFirstValue("sub");
if (string.IsNullOrWhiteSpace(sub))
{
throw new UnauthorizedAccessException("Missing Auth0 subject claim.");
}
return sub;
}
}
}

Register:

builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ICurrentUser, CurrentUser>();

Use sub as the stable external identity.

Do not use email as the identity key.

app.MapGet("/api/me/token", (ClaimsPrincipal user) =>
{
return Results.Ok(new
{
sub = user.FindFirstValue("sub"),
claims = user.Claims.Select(c => new { c.Type, c.Value })
});
})
.RequireAuthorization();

Use this only during integration. Remove or restrict it before production if it exposes too much claim data.

Recommended table:

Users
- Id: Guid / long
- Auth0UserId: string, unique, required
- Email: string?, optional
- DisplayName: string?, optional
- AvatarUrl: string?, optional
- CreatedAtUtc
- LastSeenAtUtc
- IsDisabled

EF Core entity:

public sealed class AppUser
{
public Guid Id { get; set; }
public required string Auth0UserId { get; set; }
public string? Email { get; set; }
public string? DisplayName { get; set; }
public string? AvatarUrl { get; set; }
public DateTimeOffset CreatedAtUtc { get; set; }
public DateTimeOffset LastSeenAtUtc { get; set; }
public bool IsDisabled { get; set; }
}

EF configuration:

builder.Entity<AppUser>(entity =>
{
entity.HasKey(x => x.Id);
entity.Property(x => x.Auth0UserId).HasMaxLength(256).IsRequired();
entity.HasIndex(x => x.Auth0UserId).IsUnique();
entity.Property(x => x.Email).HasMaxLength(320);
entity.Property(x => x.DisplayName).HasMaxLength(200);
entity.Property(x => x.AvatarUrl).HasMaxLength(1000);
});

Create a service that ensures the Auth0 user exists in your database.

public interface IUserBootstrapper
{
Task<AppUser> GetOrCreateAsync(ClaimsPrincipal principal, CancellationToken cancellationToken);
}

Implementation rules:

1. Read sub from access token.
2. Find Users.Auth0UserId == sub.
3. If found, update LastSeenAtUtc.
4. If not found, create row.
5. Do not fail if email/name/picture are missing.
6. If IsDisabled is true, reject the request.

Basic endpoint:

app.MapGet("/api/me", async (
ClaimsPrincipal principal,
IUserBootstrapper bootstrapper,
CancellationToken cancellationToken) =>
{
var user = await bootstrapper.GetOrCreateAsync(principal, cancellationToken);
return Results.Ok(new
{
user.Id,
user.Email,
user.DisplayName,
user.AvatarUrl
});
})
.RequireAuthorization();

4.10 Getting email/name/picture in the API

Section titled “4.10 Getting email/name/picture in the API”

Important: Auth0 access tokens for your API may not automatically contain all profile claims.

Preferred learning path:

Use an Auth0 Post Login Action to add small, namespaced custom claims to the access token.

Example Auth0 Action:

exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://jjosh.dev/claims';
if (event.authorization) {
api.accessToken.setCustomClaim(`${namespace}/email`, event.user.email);
api.accessToken.setCustomClaim(`${namespace}/name`, event.user.name);
api.accessToken.setCustomClaim(`${namespace}/picture`, event.user.picture);
}
};

Then read them in .NET:

private const string ClaimNamespace = "https://jjosh.dev/claims";
var email = principal.FindFirstValue($"{ClaimNamespace}/email");
var name = principal.FindFirstValue($"{ClaimNamespace}/name");
var picture = principal.FindFirstValue($"{ClaimNamespace}/picture");

Keep custom claims small.

Do not add heavy data to tokens.

Auth0 can put scopes in the scope claim as a space-separated string.

Create helper:

public static class AuthorizationPolicyBuilderExtensions
{
public static AuthorizationPolicyBuilder RequireScope(
this AuthorizationPolicyBuilder builder,
string scope)
{
return builder.RequireAssertion(context =>
{
var scopeClaim = context.User.FindFirst("scope")?.Value;
if (string.IsNullOrWhiteSpace(scopeClaim))
{
return false;
}
return scopeClaim.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Contains(scope, StringComparer.Ordinal);
});
}
}

Register policy:

builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ManageOwnNotifications", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireScope("manage:own_notifications");
});
});

Use policy:

app.MapPost("/api/notification-rules", CreateNotificationRule)
.RequireAuthorization("ManageOwnNotifications");

If using Auth0 RBAC with permissions in access token, you may also check the permissions claim/array depending on token format and middleware behavior.

var api = app.MapGroup("/api");
api.MapGet("/public/outages", GetPublicOutages);
var authed = api.MapGroup("").RequireAuthorization();
authed.MapGet("/me", GetMe);
authed.MapGet("/notification-rules", GetMyNotificationRules);
authed.MapPost("/notification-rules", CreateNotificationRule)
.RequireAuthorization("ManageOwnNotifications");
var admin = api.MapGroup("/admin")
.RequireAuthorization("AdminOnly");
admin.MapGet("/users", GetUsers);
admin.MapPost("/notification-rules/{id:guid}/disable", DisableRule);

Always check ownership in the database.

Bad:

User has manage:own_notifications, so allow editing any notification rule ID.

Correct:

User has manage:own_notifications.
Rule.UserId == current local user ID.
Then allow update.

Local app users need a disable flag even if Auth0 user still exists.

Users.IsDisabled = true

Middleware/service behavior:

If authenticated Auth0 user maps to disabled local user, return 403.

Do not delete users immediately. Soft-disable first.


Terminal window
npm install @auth0/auth0-angular

Auth0’s Angular quickstart states the SDK supports Angular 13 and newer, and the current quickstart covers modern Angular versions.

src/environments/environment.ts:

export const environment = {
production: false,
apiBaseUrl: 'https://localhost:5001',
auth0: {
domain: 'YOUR_TENANT.REGION.auth0.com',
clientId: 'YOUR_AUTH0_SPA_CLIENT_ID',
audience: 'https://api.jjosh.dev',
redirectUri: window.location.origin
}
};

src/environments/environment.prod.ts:

export const environment = {
production: true,
apiBaseUrl: 'https://api.jjosh.dev',
auth0: {
domain: 'YOUR_TENANT.REGION.auth0.com',
clientId: 'YOUR_AUTH0_SPA_CLIENT_ID',
audience: 'https://api.jjosh.dev',
redirectUri: window.location.origin
}
};

The Auth0 SPA client ID is public. It can exist in frontend config.

The Auth0 client secret must not exist in frontend config.

For standalone Angular app config:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { provideAuth0, AuthHttpInterceptor } from '@auth0/auth0-angular';
import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { routes } from './app.routes';
import { environment } from '../environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideHttpClient(withInterceptorsFromDi()),
provideAuth0({
domain: environment.auth0.domain,
clientId: environment.auth0.clientId,
authorizationParams: {
redirect_uri: environment.auth0.redirectUri,
audience: environment.auth0.audience
},
httpInterceptor: {
allowedList: [
`${environment.apiBaseUrl}/api/*`
]
}
}),
{
provide: HTTP_INTERCEPTORS,
useClass: AuthHttpInterceptor,
multi: true
}
]
};

For NgModule-based Angular, configure AuthModule.forRoot(...) instead.

import { Component, inject } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
@Component({
selector: 'app-login-button',
template: `<button type="button" (click)="login()">Sign in</button>`
})
export class LoginButtonComponent {
private readonly auth = inject(AuthService);
login(): void {
this.auth.loginWithRedirect();
}
}
import { Component, inject } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
@Component({
selector: 'app-logout-button',
template: `<button type="button" (click)="logout()">Sign out</button>`
})
export class LogoutButtonComponent {
private readonly auth = inject(AuthService);
logout(): void {
this.auth.logout({
logoutParams: {
returnTo: window.location.origin
}
});
}
}

The returnTo URL must be listed in Auth0 Allowed Logout URLs.

import { Component, inject } from '@angular/core';
import { AsyncPipe, JsonPipe } from '@angular/common';
import { AuthService } from '@auth0/auth0-angular';
@Component({
selector: 'app-auth-status',
standalone: true,
imports: [AsyncPipe, JsonPipe],
template: `
@if (auth.isAuthenticated$ | async) {
<pre>{{ auth.user$ | async | json }}</pre>
} @else {
<p>Not signed in.</p>
}
`
})
export class AuthStatusComponent {
readonly auth = inject(AuthService);
}

Use this for debugging only. Do not expose raw profile data unnecessarily in production UI.

import { Routes } from '@angular/router';
import { AuthGuard } from '@auth0/auth0-angular';
export const routes: Routes = [
{
path: 'notification-rules',
canActivate: [AuthGuard],
loadComponent: () => import('./features/notification-rules/notification-rules.component')
.then(m => m.NotificationRulesComponent)
}
];

Route guards improve UX.

They do not replace API authorization.

import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
@Injectable({ providedIn: 'root' })
export class MeApiClient {
private readonly http = inject(HttpClient);
getMe() {
return this.http.get(`${environment.apiBaseUrl}/api/me`);
}
}

The Auth0 HTTP interceptor attaches the access token to URLs matching allowedList.

After authentication, call:

GET /api/me

This lets the backend create or update the local Users row.

Angular can do this in an app initializer, shell component, or auth-aware user store.

Simple approach:

this.auth.isAuthenticated$.subscribe(isAuthenticated => {
if (isAuthenticated) {
this.meApiClient.getMe().subscribe();
}
});

Production code should manage subscriptions cleanly.


Users
- Id
- Auth0UserId unique
- Email nullable
- DisplayName nullable
- AvatarUrl nullable
- CreatedAtUtc
- LastSeenAtUtc
- IsDisabled
NotificationRules
- Id
- UserId
- Keyword
- NormalizedKeyword
- IsEnabled
- CreatedAtUtc
- UpdatedAtUtc

Constraints:

User max active rules: 3-5
Keyword min length: 3
Keyword max length: 80
Unique normalized keyword per user, if desired
UserLoginEvents
- Id
- UserId
- Auth0UserId
- Provider nullable
- OccurredAtUtc
- IpHash nullable
- UserAgentHash nullable

Do not store raw IP forever unless you have a clear privacy reason.


AuthenticatedUser
- requires valid JWT
ManageOwnNotifications
- requires valid JWT
- requires manage:own_notifications scope/permission
AdminOnly
- requires valid JWT
- requires admin:manage_notifications scope/permission or local admin flag
A user can read only their own notification rules.
A user can create only up to the configured max active rules.
A user can update/delete only rules owned by them.
An admin can disable abusive rules.

Keep Auth0 authorization coarse.

Keep resource ownership in your database.


Example backend structure:

Modules/Auth/
CurrentUser.cs
UserBootstrapper.cs
AuthPolicies.cs
ClaimsExtensions.cs
Modules/Users/
AppUser.cs
GetMeEndpoint.cs
Modules/NotificationRules/
NotificationRule.cs
GetMyNotificationRulesEndpoint.cs
CreateNotificationRuleEndpoint.cs
UpdateNotificationRuleEndpoint.cs
DeleteNotificationRuleEndpoint.cs

Example Angular structure:

src/app/core/auth/
auth.config.ts
auth-buttons.component.ts
user-session.store.ts
src/app/core/api/
me-api.client.ts
notification-rules-api.client.ts
src/app/features/notification-rules/
notification-rules.page.ts
notification-rule-form.component.ts

Use fake/test authentication for most API tests.

Do not call Auth0 in normal test runs.

Test cases:

GET /api/me without token returns 401
GET /api/me with valid test principal returns 200
POST /api/notification-rules without token returns 401
POST /api/notification-rules without required scope returns 403
POST /api/notification-rules with required scope creates rule
User cannot update another user's rule
Disabled local user gets 403

Have one manual test that uses a real Auth0 token against local API.

Do not make CI depend on Auth0 unless specifically building an external integration pipeline.

Test:

Login button calls loginWithRedirect
Logout button calls logout
Protected route uses AuthGuard
API client calls correct endpoint
Auth interceptor is configured for API base URL

1. Start API on https://localhost:5001
2. Start Angular on http://localhost:4200
3. Click Sign in
4. Complete Auth0 login
5. Return to Angular
6. Open browser dev tools
7. Call /api/me through UI
8. Confirm Authorization: Bearer token is attached
9. Confirm API returns current app user
10. Create notification rule
11. Refresh page
12. Confirm session persists
13. Sign out
14. Confirm API calls stop being authorized
1. Deploy API
2. Deploy Angular
3. Add production frontend URL to Auth0 Allowed Callback URLs
4. Add production frontend URL to Auth0 Allowed Logout URLs
5. Add production frontend URL to Auth0 Allowed Web Origins
6. Add production frontend origin to API CORS
7. Login with Google
8. Login with Facebook, if enabled
9. Confirm /api/me works
10. Confirm notification-rule endpoints enforce ownership

Likely causes:

Audience mismatch
Issuer/domain mismatch
Angular did not request audience
Token is expired
Using ID token instead of access token
Missing Authorization header
API is running HTTP while frontend expects HTTPS token/cookie behavior

Check:

Auth0 API Identifier == Angular audience == API Audience
API Authority == https://YOUR_AUTH0_DOMAIN/

Likely causes:

Angular origin missing from API CORS
API CORS middleware order wrong
Auth0 Allowed Web Origins missing Angular origin
Using trailing slash mismatch in some settings

Use exact origins:

http://localhost:4200
https://app.jjosh.dev

Not:

http://localhost:4200/
https://app.jjosh.dev/

Likely causes:

Angular redirect_uri not listed in Auth0 Allowed Callback URLs
Production URL missing
Wrong callback route

Likely cause:

returnTo URL is not listed in Allowed Logout URLs

Likely cause:

Access token does not include profile claims

Fix:

Use Auth0 Action to add small namespaced custom claims, or let Angular show profile from ID token/userinfo while backend only uses sub.

[ ] API validates issuer
[ ] API validates audience
[ ] API validates token lifetime
[ ] API validates signing key
[ ] API requires HTTPS in production
[ ] API CORS allows only known frontend origins
[ ] Auth0 callback URLs are exact
[ ] Auth0 developer keys are not used in production social connections
[ ] Angular does not contain client secret
[ ] Backend logs do not include tokens
[ ] Local user identity uses Auth0 sub, not email
[ ] Admin actions require server-side policy
[ ] User-owned resources check database ownership
[ ] Disabled local users are rejected
[ ] Rate limits exist on write endpoints
[ ] Suspicious community-generated data can be disabled by admin

Use this sequence. Do not skip ahead.

1. Confirm Auth0 domain, client ID, and API audience.
2. Confirm Angular callback/logout/web origins.
3. Confirm API identifier/audience.
4. Confirm Google connection is enabled.

Deliverable:

A short AUTH0_SETUP.md file in the repo with non-secret config values and dashboard checklist.
1. Add JWT bearer package.
2. Add Auth0 config binding.
3. Configure authentication/authorization middleware.
4. Add /api/me/token smoke endpoint.
5. Verify 401 without token.
6. Verify 200 with real Auth0 token.

Deliverable:

Protected API endpoint works with Auth0 access token.
1. Install @auth0/auth0-angular.
2. Add environment config.
3. Register Auth0 provider.
4. Add login/logout buttons.
5. Add protected route.
6. Add HTTP interceptor allowedList.
7. Call /api/me/token from Angular.

Deliverable:

Angular signs in and calls protected API successfully.
1. Add Users table/entity.
2. Add unique index on Auth0UserId.
3. Add CurrentUser service.
4. Add UserBootstrapper.
5. Replace /api/me/token with production /api/me.
6. Create local user on first authenticated request.

Deliverable:

Authenticated users have local app user records.
1. Protect notification rule endpoints.
2. Add ownership checks.
3. Add max active rule limit.
4. Add keyword validation.
5. Add admin disable endpoint.

Deliverable:

Users can manage only their own rules; admin can disable abusive rules.
1. Enable RBAC for Auth0 API if needed.
2. Add API permissions.
3. Create roles.
4. Assign test user role.
5. Validate scopes/permissions in API policies.

Deliverable:

Policy-based authorization works for normal user and admin paths.
1. Create Meta app.
2. Configure Facebook Login.
3. Add Auth0 callback URL to Meta.
4. Add Facebook App ID/Secret to Auth0 connection.
5. Enable Facebook connection for Angular app.
6. Test with Meta test user first.
7. Move to live mode when public requirements are satisfied.

Deliverable:

Facebook sign-in works through the same Angular/Auth0/API path.

The integration is done when:

[ ] A user can login with Google from Angular
[ ] Angular receives authenticated state
[ ] Angular can call /api/me
[ ] API validates JWT using Auth0 issuer/audience
[ ] API creates or updates local user row
[ ] API rejects unauthenticated calls with 401
[ ] API rejects unauthorized calls with 403
[ ] User cannot access another user's rules
[ ] API CORS is locked to frontend origin
[ ] Production Auth0 URLs are configured
[ ] Secrets are not in the repo
[ ] Setup documentation exists in the repo

Keep authentication boring.

Use Auth0 for login.

Use ASP.NET Core authorization policies for API gates.

Use your database for ownership and community-abuse rules.

Do not let OAuth complexity leak into the rest of the application.