We’ve all been there. You spend two days wiring up authentication for a distributed project. It works perfectly on your machine with a mock provider or a local database, but the moment you try to integrate a real enterprise identity provider like Microsoft Entra ID, the wheels come off. Redirect URIs don't match, audience validation fails, and your local environment feels nothing like the production environment you're supposed to be targeting.
When I started building out my latest
The Identity Architecture: The "Three-Registration" Strategy
One of the most common mistakes I see in senior-level designs is trying to use a single Entra App Registration for every component in a distributed system. While it's easier to set up, it’s a security and maintenance nightmare.
In a true Clean Architecture approach, we separate concerns. In this project, I’ve structured the identity flow around three distinct registrations:
- The API (The Resource Server): This is the gatekeeper. It doesn't handle "logins" or UI. Its sole job is to receive a Bearer token, validate the signature against Microsoft’s keys, and check if the
aud(audience) andscp(scopes) claims allow the requested action. - The React App (The Public Client): This is the Next.js frontend using
NextAuth.js. It performs the "heavy lifting" of the OIDC (OpenID Connect) flow. It interacts with the user, handles the redirect back from Microsoft, and securely stores the tokens. - The Scalar UI (The Developer Client): Because we use Scalar for API documentation and testing, it needs its own identity. This allows developers to authenticate directly against the API without needing the React frontend running, which is a massive productivity boost.
repo, you’ll need to create three registrations in the
Hard-Learned Lessons & Trade-offs
1. The Managed Identity Pivot
In local development, we use Client Secrets because they are easy. However, the moment this code moves to Azure via azd up, you should pivot to Managed Identity. .NET Aspire’s Azure components make this easy, but you must ensure your code uses DefaultAzureCredential and avoid keeping secrets in your configuration files.
2. The CORS Nightmare
When your React app (localhost:65499) tries to call your API (localhost:5049), you will hit CORS issues. Aspire helps manage this, but ensure your API's CORS policy explicitly allows the frontend's origin and the headers required for Authorization. In production, azd up typically configures this for you via Container App environment variables, but local dev requires manual attention in the Program.cs of your API.
3. Keycloak as a Fallback
Why did I keep Keycloak in the project? Because Entra ID requires an internet connection and an active Azure subscription. For "offline" development or lightning-fast integration tests, spinning up a Keycloak container via Aspire is a godsend. It ensures that the team can keep working even if Azure is having a bad day.
Path to Production: azd up
One of the primary reasons to use this specific project structure is the deployment story. Because this is a .NET Aspire project, you can run:
azd up
The Azure Developer CLI will look at your AppHost, generate the necessary Bicep files, and provision your Azure Container Apps, Key Vault, and Log Analytics. Because we've separated our App Registrations, we can easily map them to environment variables in the Azure production environment, ensuring a seamless transition from "it works on my machine" to "it works in the cloud."
Conclusion
Identity is often the "final boss" of software architecture. By using .NET Aspire and following the Clean Architecture patterns laid out in this repository, you turn a complex, error-prone manual process into a repeatable, configuration-driven workflow.
**Have you made the jump to .NET Aspire yet?
What’s been your biggest challenge with Entra ID integration?
Let’s discuss in the comments.**
SOCIAL SHARE CARD GENERATOR