OpenIdConnectHandler adds support for Pushed Authorization Requests (PAR)
| Product family | ASP.NET Core |
|---|---|
| Document source | Aspnet Core Aspnetcore 10.0 |
| Guide type | Reference Guide |
| Skill level | Intermediate to advanced |
| Time | 15 - 60 minutes depending on environment |
This page documents OpenIdConnectHandler adds support for Pushed Authorization Requests (PAR) for engineers working with ASP.NET Core. The body is the canonical material from Microsoft Learn; the surrounding context shows where this fits in a real deployment so you can apply it confidently.
Hands-on walkthrough
Authentication code is the easiest place to ship a security bug and the hardest place to debug one. OpenIdConnectHandler adds support for Pushed Authorization Requests (PAR) sits in OAuth/OIDC land, where the spec is clean but every implementation has its own quirks. I've spent more hours debugging this than I care to count, so here's a practical walkthrough.
I once helped a client in Hyderabad untangle this over a 2-hour Teams call. Their issue traced back to a Group Policy from 2018 nobody had cleaned up.
What you need first
- An OpenID Connect provider you control. For Microsoft Entra ID (formerly Azure AD), free tier covers basic scenarios; P1 license starts at $6/user/month.
- An ASP.NET Core 10 app already wired with
Microsoft.AspNetCore.Authentication.OpenIdConnectNuGet package. - A client ID, tenant ID, and (for confidential clients) a client secret stored in User Secrets, not in source.
- About 30 minutes. Auth changes deserve patience.
Configuration walkthrough
- Open
Program.csand find the auth registration. You're looking forbuilder.Services.AddAuthentication(...).AddOpenIdConnect(...). - Set the authority. For Entra ID multi-tenant, that's
https://login.microsoftonline.com/common/v2.0. For single-tenant, replacecommonwith your tenant GUID. - Register the redirect URI in the app registration on the Entra portal. Format:
https://yourapp/signin-oidc. Local dev:https://localhost:5001/signin-oidc. The trailing slash matters in some places and not others. Try without first. - Set scopes with
options.Scope.Add("openid"),options.Scope.Add("profile"), and any API scopes you need. - Configure token validation parameters. Most apps don't need to touch these. If you do, the most common change is
NameClaimTypeandRoleClaimType. - Set
options.SaveTokens = trueif you need to call downstream APIs as the user. Otherwise leave it false to keep cookies smaller. - Add a sign-out endpoint.
app.MapGet("/signout", () => Results.SignOut(new AuthenticationProperties { RedirectUri = "/" }, [CookieAuthenticationDefaults.AuthenticationScheme, OpenIdConnectDefaults.AuthenticationScheme])). Without this, users can't actually log out.
How to verify auth actually works
- Use a fresh InPrivate or Incognito window. Cached cookies are the #1 cause of "it works on my machine".
- Open browser DevTools, Network tab, preserve log. Watch the full redirect chain: app → authority → callback → cookie set → home page.
- Decode the returned ID token at jwt.io — confirm
aud,iss, andexpare what you expect. - Run
curl -v https://yourapp/.well-known/openid-configuration— your app shouldn't expose this, the IdP should. But check that you can reach the IdP's well-known endpoint from your server. - Hit a protected endpoint. You should get a 302 redirect to the IdP, not a 401.
Where auth breaks in practice
- Clock skew. Token validation fails if your server's clock is more than 5 minutes off. Run
w32tm /query /statuson Windows ortimedatectl statuson Linux. Containers in particular often lag. - Cookie SameSite policy. Modern browsers default to Lax. If your auth flow uses a POST redirect from a third-party domain, you need
SameSite=NonewithSecure=true. Test in real browsers, not curl. - Reverse proxy not forwarding scheme. If your app is behind nginx or Application Gateway and the X-Forwarded-Proto header isn't trusted, ASP.NET Core builds redirect URLs as
http://instead ofhttps://. Fix:app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto }). - Redirect URI mismatch. The most common production failure. Every environment needs its own registered redirect URI. Plan for dev, staging, and prod.
Rollback
If auth breaks in production, your options are limited. Plan ahead:
- Keep the previous container image tagged in your registry.
docker pull+ redeploy = 90 seconds. - Keep an old slot on Azure App Service warm and ready. Slot swap takes 15-20 seconds.
- Document the rollback runbook in a place that isn't behind the auth you just broke.
Test in a sandbox tenant first. I keep a $5/month Azure dev/test subscription specifically for this kind of throwaway work, and it has paid for itself a hundred times over.
Operational notes
Beyond the basics, here's the operational reality.
Team and runbook. Internal docs decay fast. I keep a CHANGELOG.md next to the code that touches this area, with a date and a one-line summary of every meaningful change.
Cost watch. On Azure pricing today, the network egress is usually the surprise. A B-series VM is ₹1,200-2,100 per month, but pull 500 GB of egress and you've added another ₹4,200.
Security pass. Add this to your threat model. Whatever this change touches: auth, data flow, config surface, needs to appear on the diagram, with the new trust boundary called out explicitly.
Observability. Add a metric. Counter, histogram, gauge. pick the right one. ASP.NET Core 10 has first-class OpenTelemetry support; wire it to your observability stack and watch the right things from day one.
Patterns I keep coming back to
Across many projects in ASP.NET Core, a few patterns repeat that are worth naming explicitly.
- The "smoke test on every deploy" pattern. A 10-line script that hits 5 endpoints, checks response code and latency, exits non-zero on failure. Run it as the last step of every CD pipeline. Catches 70% of post-deploy regressions before they reach a user.
- The "feature flag everything risky" pattern. Microsoft.FeatureManagement.AspNetCore (free, ships with the framework). Wrap any change that could blow up behind a flag, default off in production, flip the flag after the change is verified.
- The "twin environment" pattern. A staging environment that is byte-for-byte identical to production in configuration, with a fraction of the data. Costs maybe 20% extra on Azure if you size sensibly. Catches roughly 50% of "works locally, fails in prod" bugs.
- The "single source of secrets" pattern. Azure Key Vault or HashiCorp Vault. Not a .env file. Not appsettings.Production.json with values committed. Real secret storage with rotation policies.
Decisions worth revisiting periodically
Some choices made early in a project age fast. I keep a list of "look at this every six months" items per project. ASP.NET Core-flavored versions look like this:
- SDK and framework version. Are you on the current LTS? Microsoft's support window is fixed; falling behind two majors means you're paying interest in security patches.
- Dependency surface. Run
dotnet list package --outdatedquarterly. Each outdated package is a small risk; cumulatively, large risk. - Build time. If your CI builds creep past 8-10 minutes, the developer feedback loop slows enough to hurt productivity. Profile and trim.
- Test coverage and execution time. Coverage that climbs slowly is fine. Test suites that climb slowly toward 30 minutes is a warning sign, refactor or parallelize.
- Production error budget. If you're hitting your error budget consistently, slow feature work and pay down operational debt. If you're under-using your error budget, you're shipping too slowly.
Why I do it this way
Years ago I treated each topic in isolation. I'd read the docs, implement the feature, ship it, move on. The result was a lot of features that worked individually and didn't compose well. Today I default to: read the docs, sketch a diagram on paper or a digital whiteboard, identify the trust boundary, identify the failure mode, then implement. Ten extra minutes up front, hours saved down the road.
The other shift was treating tests as the design tool, not the verification tool. Writing the test first forces you to think about the API surface from the caller's perspective. By the time the test compiles, the design has been pressure-tested by your own most demanding consumer: a test that has to drive the feature without knowing the implementation.
Practical example: last month I rewrote a small reporting endpoint. The first draft was a single 60-line action method that pulled data, transformed it, and returned JSON. Tests forced me to split it into an IReportRepository, an IReportFormatter, and a thin controller. Same functionality, but now I can swap the formatter for HTML output by writing one new class and zero changes to the controller. That's the power of writing the test first.
References worth bookmarking
- Microsoft Learn: the canonical source. Whatever version of this page exists at learn.microsoft.com beats my summary.
- The ASP.NET Core GitHub repo's issues and discussions. Real bugs and real solutions, sometimes faster than the docs catch up.
- The .NET API browser at learn.microsoft.com/en-us/dotnet/api, perfect for "what does this method actually return when X."
- The official .NET blog at devblogs.microsoft.com/dotnet. explains the "why" behind changes that the API docs leave implicit.
How to apply this in practice
- Treat this as a starting point. Your tenant, SKU, region, and licence level all change the surface area in small but real ways.
- Run the procedure in a non-production environment first. A staging slot, a dev tenant, a sandboxed subscription, pick one and use it.
- Pin the version you implement against. When you commit to a design choice based on this page, write the date and the exact ASP.NET Core version into your ADR (architecture decision record).
- Cross-check the current Microsoft Learn page before rolling production. The product team updates docs often; what's true today may shift in two months.
Caveats and what to double-check
- ASP.NET Core terminology drifts. The same concept can have two or three names across docs cohorts written in different years.
- Some features in this area may still be in preview: confirm GA status before relying on it for production SLAs.
- Regional availability varies. A feature documented as "global" may actually roll out region by region across months.
- Pricing for ASP.NET Core-related services changes regularly. This page does not track pricing. Use the official Microsoft pricing calculator for the latest numbers.
Related work in your environment
- Document this reference in your team wiki along with which workloads currently depend on it.
- Set up a Microsoft Learn RSS feed or doc-change alert for the source page so your team is notified when Microsoft updates the canonical version.
- Add a periodic review to your governance cadence. Quarterly is a sensible default for ASP.NET Core.
FAQ
References
- Microsoft Learn - official documentation for ASP.NET Core
- Microsoft tech community forums and Q&A
- Azure / Microsoft 365 service health dashboards
Related fixes
Related guides worth a look while you sort this one out:
- Enable support for interactive render modes
- Role-based authorization in ASP.NET Core MVC
- Role-based authorization in ASP.NET Core Razor Pages
- Support for the.localhost Top-Level Domain
- Authentication and authorization using the Microsoft identity platform
- OAuth 2.0 authorization code flow in Azure Active Directory B2C