Imported from samdop/convertonode (
.github/skills/auth-and-security-migration/SKILL.md). Install upstream withnpx skills add samdop/convertonode --skill auth-and-security-migration. Copyright stays with the author.
Auth and security migration
Use this skill to migrate ASP.NET MVC authentication and authorization into a Node.js API plus React frontend. Default to Microsoft Entra ID, OIDC, and JWT for workforce users. Prefer Entra External ID for external customers. Mention Auth0 or Amazon Cognito only when a customer already standardizes on those providers.
This skill is framework-agnostic. It provides the identity decision, React MSAL
pattern, token validation core, Passport option, authorization helpers, cookie
session guidance, and migration references. Use generated
nodejs-backend-<framework>-mapping skills for Express, Fastify, NestJS, Hono,
or other framework wiring.
When to use
Use this skill when the task mentions any of these source patterns:
- Windows Authentication, IIS Integrated Authentication,
WindowsIdentity,WindowsPrincipal, NTLM, Kerberos,User.Identity.Name, or domain users. - Active Directory, AD groups, nested groups, SID checks,
IsInRole, or role logic tied toDOMAIN\GroupName. - Forms Authentication,
.ASPXAUTH, login cookies,FormsAuthenticationTicket,Session,TempDataauth flows, or custom cookie login pages. - ASP.NET Identity, Membership,
AspNetUsers,AspNetRoles,AspNetUserRoles,AspNetUserClaims, PBKDF2 password hashes, or legacy SQL membership providers. - MVC
[Authorize],[Authorize(Roles = "...")],[Authorize(Policy = "...")],[AllowAnonymous], or controller-level auth attributes. - AntiForgeryToken, CSRF, SameSite cookies, httpOnly cookies, BFF sessions, or client-side token storage concerns.
- Entra ID, Azure AD, OIDC, OAuth 2.0, JWT, MSAL, Passport, JWKS, claims, roles, app roles, delegated scopes, or Microsoft Graph authorization checks.
Do not use this skill for folder layout only; use react-project-structure.
Do not use it for axios cache and mutation design except for auth interceptors;
use state-and-data-fetching. Do not use it for Azure hosting and Bicep details;
use azure-fullstack-deployment.
Migration goal
Replace server-rendered MVC authentication coupling with explicit browser and API responsibilities:
- The React web app starts sign-in and obtains tokens through MSAL.
- The browser calls
/api/*with an access token in theAuthorizationheader. - The Node API validates the token signature, issuer, audience, and expiration.
- The API authorizes with roles, app roles, group claims, or custom policies.
- Secrets live in Key Vault or managed identity, not source code or plain env.
- Legacy cookie and session flows are retained only when a BFF pattern requires them.
Decision matrix -- source auth to target pattern
| Source | Target | Notes |
|---|---|---|
| Windows Auth (IIS + AD) | Entra ID (workforce) + OIDC + JWT | Users' AD identities sync to Entra with Entra Connect; no user impact if already synced. |
| Forms Auth (cookie) | JWT (Bearer) + short-lived access + refresh flow | Cookie option: httpOnly + Secure + SameSite=Lax; call out BFF pattern for cookie approach. |
| ASP.NET Identity / Membership | Entra External ID or self-managed with bcrypt + JWT |
For external customers, prefer Entra External ID over rolling your own. |
| Basic Auth | Do not migrate as-is; move to OIDC | Basic Auth leaks credentials to every request and has poor MFA posture. |
| Custom SSO (SAML) | Entra ID Enterprise app with SAML to OIDC bridge or SAML support | Case-by-case; preserve IdP requirements while modernizing the app edge. |
| Anonymous | No auth on public endpoints; explicit AllowAnonymous-equivalent middleware |
Public routes must be intentionally listed and tested. |
Recommended target architecture
sequenceDiagram
participant Browser
participant Web as Web (Vite/React)
participant Entra as Entra ID
participant Api as Node API
participant Jwks as Entra JWKS
Browser->>Web: Load app
Web->>Entra: [1] Redirect to Entra ID
Entra-->>Web: id_token + access_token
Web->>Web: Store token in memory
Web->>Entra: Silent request when token expires
Web->>Api: GET /api/* Authorization: Bearer <accessToken>
Api->>Jwks: Download signing keys
Jwks-->>Api: Public keys
Api->>Api: Validate iss, aud, exp, signature
Api->>Api: Authorize roles/groups claims
Api-->>Web: Domain response
Keep tokens out of URLs. Keep access tokens short-lived. Use MSAL silent token acquisition before API calls. Store browser tokens in memory where possible; use MSAL cache settings deliberately if product requirements force persistence.
AD groups to Entra groups
Follow this workflow when MVC authorizes with AD groups:
- Verify the organization syncs on-premises AD to Entra with Entra Connect.
- Confirm each required on-prem AD security group is in sync scope.
- Find the synced Entra security group Object ID for each source group.
- Map group Object IDs to role names in API configuration or a database table.
- Configure the API app registration to emit the
groupsclaim in tokens. - Validate the API access token contains expected group Object IDs.
- Make backend authorization middleware check the token
groupsclaim. - If tokens exceed group overage limits, call Microsoft Graph to resolve group membership after validating the token.
- Prefer Entra App Roles for portable app logic when you can change the model.
- Declare app roles in the API app registration, assign users or groups, and
read the token
rolesclaim in the API.
Use group Object IDs, not display names. Display names are mutable and not unique. Store mappings in env vars for small apps or in a database table for admin-managed products.
ROLE_ADMIN_GROUP_ID=00000000-0000-0000-0000-000000000001
ROLE_MANAGER_GROUP_ID=00000000-0000-0000-0000-000000000002
App Roles are cleaner than group IDs for portable role logic because code checks
Admin instead of tenant-specific group Object IDs. Use groups when the customer
requires existing AD group governance. See references/ad-groups-to-entra.md
for concrete examples.
Frontend integration -- MSAL
Install MSAL in apps/web:
pnpm --filter web add @azure/msal-browser @azure/msal-react
Create the MSAL configuration from templates/web-msal-config.ts. Pull the
client ID, tenant ID, redirect URI, API scope, and API base URL from Vite env
vars. Keep runtime names prefixed with VITE_ only for values safe to expose in
the browser. Do not put secrets in Vite env vars.
Wrap React with MsalProvider from templates/web-msal-provider.tsx. The
provider owns the PublicClientApplication, handles redirect promises, and
renders either authenticated app content or a sign-in button. Use
AuthenticatedTemplate and UnauthenticatedTemplate for simple shells.
For custom flows, create a useAuth() hook around useMsal() and active account
state. Expose:
accountisAuthenticatedlogin()logout()getAccessToken()hasRole(role)hasAnyRole(roles)
Use acquireTokenSilent for API calls. If interaction is required, fall through
to acquireTokenRedirect, not a hand-built login URL.
const result = await msalInstance.acquireTokenSilent({
account,
scopes: [import.meta.env.VITE_API_SCOPE],
});
return result.accessToken;
Attach access tokens to axios with an interceptor. Put the API client in
apps/web/src/lib/api-client.ts. The interceptor calls
msalInstance.acquireTokenSilent(scopes).then(r => r.accessToken) and attaches
Authorization: Bearer <token>. Cross-reference state-and-data-fetching for
query hooks, invalidation, retries, and server-state boundaries.
Use RequireAuth from templates/web-require-auth.tsx to protect route
subtrees. Use RequireRole from templates/web-require-role.tsx for UI-level
role gates. Treat frontend role checks as UX only; always enforce authorization
again on the API.
Backend integration -- Node.js
Choose one backend token validation pattern.
Pattern A -- Passport with passport-azure-ad BearerStrategy
Use this when the project already uses Passport, when NestJS uses
@nestjs/passport, or when the team wants a conventional middleware strategy.
Use templates/api-passport-entra.ts as the core. It configures a bearer
strategy with issuer, tenant ID, audience, and logging settings.
Install dependencies in the API workspace:
pnpm --filter api add passport passport-azure-ad
pnpm --filter api add -D @types/passport
Wrap the Passport authenticate call with the chosen framework's middleware
adapter. For Express, mount it on /api. For NestJS, convert it into a Guard via
@nestjs/passport. For Fastify or Hono, prefer Pattern B unless Passport is a
hard requirement.
Pattern B -- Lightweight jose plus JWKS
Use this when the backend is Fastify, Hono, Express without Passport, serverless functions, or any project that wants framework-neutral token validation. This is the recommended default for generated v4.0 apps because it is explicit and portable.
Install dependencies:
pnpm --filter api add jose
Use templates/api-auth-middleware.ts. It validates the token signature against
Entra's JWKS endpoint, enforces issuer and audience, extracts claims, and exposes
framework-neutral wrappers. Adapt the wrapper to Express middleware, Fastify
preHandler, Hono middleware, or NestJS Guards.
Framework-specific wiring belongs in the generated
nodejs-backend-<framework>-mapping skill. This skill provides core auth logic
and a Passport option for consistency.
Authorization middleware
Use three primitives everywhere:
requireAuth(handler)
requireRole('Admin')(handler)
requireAnyRole(['Admin', 'Manager'])(handler)
The api-auth-middleware.ts template implements the framework-agnostic core.
Generated framework skills should wrap these primitives without changing their
security behavior. Keep authorization checks close to routes and repeat critical
checks inside domain services when data-level authorization matters.
Normalize claims before checking them:
roles: Entra App Roles, recommended for portable app roles.groups: Entra security group Object IDs, useful for AD group migration.scp: delegated OAuth scopes, useful for API permission checks.oid: stable user Object ID.tid: tenant ID.preferred_username: display/login hint only; do not use as a primary key.
[Authorize] attribute mapping
| MVC pattern | Node + React target |
|---|---|
[Authorize] on controller |
Global requireAuth middleware on route group. |
[Authorize(Roles = "Admin")] |
requireRole('Admin') middleware. |
[Authorize(Roles = "Admin,Manager")] |
requireAnyRole(['Admin','Manager']) middleware. |
[AllowAnonymous] |
Do not apply requireAuth on that route; document it as public. |
[Authorize(Policy = "...")] |
Custom middleware or requirePolicy(...) factory. |
IPrincipal.IsInRole(...) in controller code |
Decode token and check roles server-side; on frontend use useAuth().hasRole(...). |
Map controller-level attributes to route groups first. Then map action-level
overrides. Preserve [AllowAnonymous] intentionally, because accidental public
endpoints are common during MVC migration.
Cookie and session migration
Default to bearer tokens. Retain cookie sessions only when the product chooses a BFF pattern, when legacy browser constraints demand server-side sessions, or when tokens must never be visible to frontend JavaScript.
For Express BFF sessions:
import session from 'express-session';
import RedisStore from 'connect-redis';
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET!,
resave: false,
saveUninitialized: false,
cookie: { httpOnly: true, secure: true, sameSite: 'lax' },
}));
For Fastify, use @fastify/session with a Redis-backed store. Always set
httpOnly, Secure, and SameSite=Lax unless a cross-site embedding scenario
forces SameSite=None; Secure.
CSRF rules:
- Bearer tokens in the
Authorizationheader do not need MVCAntiForgeryTokenprotection. - Cookie sessions do need CSRF protection because browsers attach cookies automatically.
csurfis deprecated but still workable for legacy Express projects.- Prefer a double-submit cookie with
crypto.randomBytesplus custom middleware for new BFF work. - Move the MVC server/client
AntiForgeryTokenhandshake to a double-submit CSRF token endpoint and header.
ASP.NET Identity migration
See references/aspnet-identity-migration.md before changing user data. Legacy
ASP.NET Identity apps usually contain:
AspNetUsersAspNetRolesAspNetUserRolesAspNetUserClaims- PBKDF2 password hashes in v2 or v3 formats
Prefer one of two paths:
- Bulk import users into Entra External ID and force password reset by email.
- Keep self-managed auth temporarily, verify legacy PBKDF2 on login, and rehash
to
bcrypton the first successful login during a migration window.
Migrate AspNetRoles to Entra App Roles when moving to Entra. Assign users or
synced groups to app roles. Use the resulting roles claim in the API and React
UX gates.
Entra app registration bootstrap
Use templates/entra-app-registration.ps1 to create two app registrations:
- Web app registration for the React SPA.
- API app registration that exposes
access_as_user.
The script uses az ad app create, configures the API scope, grants delegated
permission from the web app to the API, and adds GitHub Actions OIDC federated
credentials with az ad app federated-credential create. Run it only after
choosing tenant, redirect URI, repository, branch or environment, and API app ID
URI conventions.
.\.github\skills\auth-and-security-migration\templates\entra-app-registration.ps1 `
-TenantId "<tenant-id>" `
-WebDisplayName "myapp-web" `
-ApiDisplayName "myapp-api" `
-RedirectUri "https://web.example.com/auth/callback" `
-GitHubOrg "my-org" `
-GitHubRepo "my-repo" `
-GitHubBranch "main"
Secrets management
Never put client secrets in source code. Avoid client secrets in environment variables. Prefer managed identity and workload identity federation.
For Azure Container Apps, use Key Vault references in env vars:
env: [
{
name: 'JWT_AUDIENCE'
value: apiAppId
}
{
name: 'SESSION_SECRET'
secretRef: 'session-secret'
}
]
Use Container Apps managed identity to read Key Vault secrets. Use GitHub Actions OIDC federated credentials for deployment. Use federated credentials on the API's managed identity to acquire downstream tokens without a client secret whenever possible.
Security hardening checklist
- Enforce HTTPS-only traffic; redirect HTTP to HTTPS.
- Enable HSTS on the API if TLS terminates there.
- Send access tokens in the
Authorizationheader, never in URLs. - Use MSAL silent requests for refresh; never store refresh tokens in
localStorage. - Keep browser token cache in memory unless requirements justify persistence.
- Add CSP headers on the web host; see
azure-fullstack-deploymentfor nginx or Static Web Apps configuration. - Limit CORS to known web origins.
- Set
credentials: trueonly when using cookies. - Rate-limit login and public endpoints; use APIM policy for public APIs.
- Log authentication success, authentication failure, token validation failure, role check deny, and policy deny to Application Insights.
- Do not log raw tokens, cookies, authorization headers, or password values.
- Validate token
iss,aud,exp, and signature on every API request. - Reject tokens from the wrong tenant unless the app is explicitly multi-tenant.
- Prefer App Roles over hard-coded tenant group IDs for product roles.
- Document every public endpoint that maps from
[AllowAnonymous].
Common migration pitfalls
- Token audience mismatch: the React app requested Microsoft Graph instead of the API scope.
- Missing admin consent: users authenticate but API scope is not granted.
- Wrong tenant authority: tokens validate locally but fail in production.
- Group overage: large users do not receive full
groupsclaims. - Frontend-only authorization: UI hides buttons but API still allows the action.
- LocalStorage tokens: XSS becomes account takeover.
- Reusing MVC AntiForgeryToken with bearer auth: unnecessary complexity.
- Assuming AD display names are stable: use Object IDs or App Roles.
- Treating
preferred_usernameas immutable identity: useoidplustid.
Migration checklist
- Inventory MVC auth filters, login controllers, cookie config, and session use.
- Classify source auth with the decision matrix.
- Choose Entra workforce, Entra External ID, BFF, or temporary self-managed JWT.
- Register web and API apps in Entra.
- Configure API scope and optional App Roles.
- Configure groups claim only when group-based migration is required.
- Implement React MSAL provider and protected route components.
- Implement API token validation with
joseor Passport. - Map
[Authorize]and[AllowAnonymous]route-by-route. - Replace
IsInRolechecks with role, group, or policy middleware. - Remove AntiForgeryToken from bearer-token forms.
- Add CSRF only for cookie session/BFF flows.
- Move secrets to Key Vault and managed identity.
- Add auth audit logging and denial telemetry.
- Test with normal user, admin user, unauthorized user, expired token, wrong audience token, and anonymous request.
Cross-references
azure-fullstack-deployment: Entra provisioning via Bicep, managed identity, Key Vault references, HTTPS, CORS, and CSP headers.state-and-data-fetching: axios interceptor, TanStack Query integration, and auth-aware API client placement.react-project-structure: folder placement forapps/web,apps/api, andpackages/shared.nodejs-backend-<framework>-mapping: Express, Fastify, NestJS, Hono, or other route wiring around the framework-agnostic middleware.