Authentication is the act of verifying who a user is. When a user successfully authenticates (for example, by entering the correct credentials or token), the application establishes the user’s identity (often represented by a ClaimsPrincipal
in ASP.NET Core) (Introduction to authorization in ASP.NET Core | Microsoft Learn). In other words, authentication answers “Who are you?” and results in an identity that the app can use. By contrast, Authorization is about what the authenticated user is allowed to do or access. It answers “What are you allowed to do?” (Introduction to authorization in ASP.NET Core | Microsoft Learn). For example, after logging in (authentication), an admin user might be authorized to access a management page that a regular user cannot. Authorization is distinct from authentication but depends on it – you must know who the user is before deciding what they can do (Introduction to authorization in ASP.NET Core | Microsoft Learn).
Identity in this context can refer to both the user’s identity information and the ASP.NET Core Identity system. The user’s identity encompasses the user’s attributes, credentials, and other identifying claims (like name, email, user ID). ASP.NET Core’s built-in Identity framework is a membership system that provides user registration, login, role management, hashing of passwords, etc. It helps manage the user’s identity data (typically in a database) and integrates with authentication. For instance, ASP.NET Core Identity can handle authenticating a user by verifying their password and then creating the identity (ClaimsPrincipal) used for authorization in the app. In summary: authentication establishes identity, authorization uses identity to enforce permissions, and Identity (the framework) is a tool to manage and persist user identities (accounts, passwords, roles) in ASP.NET Core apps.
Modern authentication often relies on OAuth 2.0 and OpenID Connect (OIDC), which are industry-standard protocols. It’s crucial to understand the roles defined by OAuth 2.0 and how various flows work, as well as how OIDC builds on OAuth2 to add identity information.
OAuth 2.0 Roles: OAuth defines four primary roles in an authorization exchange (OAuth 2.0 and OpenID Connect protocols - Microsoft identity platform | Microsoft Learn) (OAuth 2.0 and OpenID Connect protocols - Microsoft identity platform | Microsoft Learn):
- Resource Owner: Usually the end-user who owns the data or resource. For example, in a photo-sharing app, the user is the resource owner of their photos.
- Client: The application requesting access to the resource on behalf of the user. This could be your ASP.NET Core application (a web app, SPA, mobile app, etc.) that needs to act on the user’s behalf (OAuth 2.0 and OpenID Connect protocols - Microsoft identity platform | Microsoft Learn). The client is often called the “relying party” in OIDC or just “the app.”
- Authorization Server (Identity Provider): The server that authenticates the user and issues tokens. It securely handles user credentials and consent. In OAuth terms it’s the authorization server, and in OIDC it’s also called the OpenID Provider (OAuth 2.0 and OpenID Connect protocols - Microsoft identity platform | Microsoft Learn) (OAuth 2.0 and OpenID Connect overview | Okta Developer). Examples include Microsoft Entra ID, Auth0, or IdentityServer acting as the authorization server. This server issues access tokens (and ID tokens in OIDC) that clients use.
- Resource Server: The API or server hosting the protected resources (data) that the client wants to access (OAuth 2.0 and OpenID Connect protocols - Microsoft identity platform | Microsoft Learn). This server validates tokens issued by the authorization server to decide if the request should be allowed. For example, a protected ASP.NET Core Web API is a resource server – it receives a bearer token from the client and must verify it before serving the data.
(OAuth 2.0 Roles) Illustration of OAuth 2.0 roles: the Resource Owner (user) grants access to a Client application, which obtains tokens from the Authorization Server (IdP) and uses them to access the Resource Server (API) on the user’s behalf.
OAuth 2.0 vs OpenID Connect: OAuth 2.0 is an authorization framework – it’s primarily about delegated access (allowing a client to act on behalf of a user to access an API). By itself, OAuth 2.0 issues access tokens (and optionally refresh tokens) that don’t inherently convey the user’s identity to the client; they just grant access to resources. OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0. OIDC issues an ID Token (in JWT format) that contains information about the user (the “identity claims”), in addition to the OAuth2 access token (OAuth 2.0 and OpenID Connect overview | Okta Developer) (OAuth 2.0 and OpenID Connect overview | Okta Developer). In practical terms, the flows for OAuth2 and OIDC are similar, but an OIDC authentication flow gives the client an ID token so the client can reliably identify the user who logged in. As one source puts it: “The high-level flow looks the same for both OpenID Connect and regular OAuth 2.0 flows. The primary difference is that an OpenID Connect flow results in an ID token, in addition to any access or refresh tokens.” (OAuth 2.0 and OpenID Connect overview | Okta Developer).
OAuth2 Authorization Flows: OAuth 2.0 defines several flows (grant types) to accommodate different application scenarios. Some common flows and when they’re used in ASP.NET Core applications are:
- Authorization Code Flow: The most common flow for server-side web applications and now recommended for single-page apps as well (with PKCE). The client (your ASP.NET Core web app or SPA) redirects the user to the authorization server’s login page. After the user authenticates and consents, the authorization server redirects back with an authorization code. The client app then exchanges this code for tokens (access token and an ID token if using OIDC). This flow keeps sensitive tokens off the URL and requires a back-channel exchange (enhanced security). In ASP.NET Core, if you use the OIDC middleware (AddOpenIdConnect), it by default uses the authorization code flow (with PKCE for public clients).
- Implicit Flow: An older flow where tokens (ID token or access token) are returned directly in the redirect URL. Implicit flow has no back-channel token exchange. It was historically used for pure client-side apps (like SPAs) due to not requiring a client secret. However, implicit flow is no longer recommended because it exposes tokens in the browser address bar and has other security limitations (modern SPAs should use Authorization Code with PKCE instead).
- Client Credentials Flow: Used for server-to-server or service accounts (no user involved). The client (which is typically an ASP.NET Core service or daemon) authenticates with the authorization server using its own credentials (e.g., a client secret or certificate) and gets an access token. This is useful for background services or APIs that need to call other APIs. In ASP.NET Core, you might use this flow to acquire tokens using a library like MSAL, or you might configure an API to accept client-credentials tokens by validating the client identity in the token.
- Resource Owner Password Credentials (ROPC): A legacy flow where the user’s credentials (username/password) are collected by the client and exchanged directly for a token. This flow is highly discouraged (and not supported at all by some IdPs) because the client gets to see the user’s password, breaking the purpose of OAuth’s delegation. Instead, interactive users should use the Authorization Code flow.
- Device Code Flow: Designed for devices with no browser or limited input (e.g., a smart TV). The device asks the user to visit a link on another device to authenticate. Meanwhile, the device polls the auth server for a token. ASP.NET Core might not directly implement this flow, but you could use libraries or the Microsoft Identity client to support it for tools or CLIs.
Roles and Token Types in OIDC: In OIDC terminology, the authorization server is also called the OpenID Provider (OP), and the client is called the Relying Party (RP) (OAuth 2.0 and OpenID Connect overview | Okta Developer). The tokens involved include:
- ID Token: a JWT that contains user identity claims (like name, email, user ID). It’s meant for the client and is signed by the IdP. In ASP.NET Core, after an OIDC login, the middleware will validate the ID token and use it to sign the user into the application (often creating a session cookie with the user’s claims).
- Access Token: usually a JWT or opaque token that authorizes access to APIs (the resource server). The client will include this token in requests to the API (typically in the
Authorization: Bearer <token>
header). The API (resource server) validates the token to check the client’s permissions (scopes, roles, etc.). - Refresh Token: a long-lived token that the client can use to get new access tokens after the current one expires, without user interaction. Refresh tokens should be stored securely (usually only on trusted backend clients or in secure browser storage if used in SPAs) since they can be used to obtain new access tokens “forever” until revoked.
Mapping OAuth2/OIDC to ASP.NET Core: In practical ASP.NET Core development, your app can play different roles:
- If you are building an ASP.NET Core server-side web app that allows users to sign in via an external IdP (like Azure AD, Google, Okta, etc.), then your app is the OAuth Client (OIDC Relying Party). You would use the OpenID Connect authentication handler (via
AddOpenIdConnect
) along with Cookie authentication. The OIDC middleware handles redirecting to the IdP, receiving the authorization code, and redeeming it for tokens. It will then create an application login (usually a cookie) for the user containing the identity (claims from the ID token). In this scenario, the external IdP is the Authorization Server/OP. Your app might not itself issue access tokens, but it may receive an access token from the IdP if it needs to call APIs on the user’s behalf (for example, if your web app calls a downstream API like Microsoft Graph, the OIDC middleware can be configured to obtain an access token for that API). - If you are building an ASP.NET Core Web API that needs to be secured, your API is a Resource Server. You wouldn’t use the OIDC handler here. Instead, you’d configure JWT Bearer authentication (
AddJwtBearer
) so that the API can accept and validate access tokens issued by an IdP or security token service. You’ll set options likeAuthority
(the issuer’s URL) and the expected audience or scope. The API then uses[Authorize]
attributes to enforce that only requests with a valid token are allowed. The heavy lifting of token validation (signatures, expiration, issuer) is done by the JWT middleware. For example, if using Azure AD (Entra ID) to secure the API, the access tokens are JWTs that the API will validate using Azure’s public keys and the configured issuer. - In more advanced scenarios, you might build your own authorization server in ASP.NET Core – for example, using Duende IdentityServer or another library to issue tokens to other clients. In that case, your ASP.NET Core app itself acts as the IdP/Authorization Server, implementing OAuth2 and OIDC endpoints. (Microsoft’s templates historically included IdentityServer for SPAs, and in .NET 8, new Identity API endpoints allow issuing JWTs for authentication – more on that later.) But if your goal is to integrate with existing identity providers like Azure AD, Auth0, etc., your app usually won’t function as an OAuth server but rather as the client or resource.
In summary, OAuth2/OIDC provide a framework for delegated auth that ASP.NET Core apps participate in either by outsourcing authentication to an external IdP (and consuming tokens), or by accepting tokens in APIs. These protocols enable scenarios like single sign-on, calling APIs on behalf of users, and decoupling authentication from your application logic.
Modern authentication uses tokens to carry authentication information. In ASP.NET Core, you will encounter different token formats: JWTs (JSON Web Tokens), reference (opaque) tokens, and the traditional cookie tokens used for web session authentication. Understanding each is key to designing auth in your app.
-
JWT (JSON Web Token): A JWT is a self-contained, stateless token format that is JSON data (claims) signed (and optionally encrypted) into a compact string. A JWT access token contains claims about the user or client and an expiration time, and is digitally signed by the issuer (using an asymmetric key or secret). Because it’s self-contained, any resource server that has the issuer’s public key can validate the token and read the claims without a database lookup. This makes JWTs very convenient for distributed systems and microservices – you don’t need a central session store; the token itself proves the identity and permissions. ASP.NET Core’s JWT Bearer authentication middleware will parse and validate a JWT (verifying the signature and claims) on each request (OAuth reference token - Auth0 Community) (OAuth reference token - Auth0 Community). One downside of JWTs being self-contained is revocation: if you need to revoke a JWT early (before it expires), it’s hard to do because the token is valid until its expiry unless you maintain a revocation list. As the IdentityServer documentation notes, “Once an API has learned about the key material, it can validate self-contained tokens without needing to communicate with the issuer. This makes JWTs hard to revoke. They will stay valid until they expire.” (OAuth reference token - Auth0 Community). Best practices to mitigate this include using short lifetimes for JWTs and using refresh tokens for longevity. In ASP.NET Core, JWTs are commonly used as bearer tokens for API authentication (e.g., an SPA or mobile app passes the JWT in the
Authorization
header). They can also be used as the format for id_tokens in OIDC, and even for authentication cookies in certain cases (though typically cookies use a different format as described below). -
Reference (Opaque) Tokens: A reference token (also called an opaque token) is essentially a token ID rather than a token containing data. When an authorization server issues a reference token, it stores the token’s data (claims, expiry, etc.) in a database on the server side, and gives the client a random opaque string as the token. When the client calls an API with this opaque token, the API can’t validate it by itself (since the token is just an identifier); the API must make a back-channel call to the authorization server (for example, an introspection endpoint defined by OAuth2) to look up the token’s details and validity (OAuth reference token - Auth0 Community). This adds an extra step (communication with the auth server on each request or on cache misses), but it allows the server to centrally revoke or update tokens at any time (since the source of truth is the database). IdentityServer (a popular .NET OAuth/OIDC server) supports reference tokens and uses the OAuth2 token introspection standard for this purpose (OAuth reference token - Auth0 Community). The Microsoft Entra ID (Azure AD) and most major IdPs default to JWTs for access tokens, but some enterprise systems or older protocols use opaque tokens. In ASP.NET Core, there isn’t a built-in “AddReferenceToken” middleware, but you can handle opaque tokens by calling the introspection endpoint in a custom auth handler or by using IdentityModel libraries (for example,
AddOAuth2Introspection
from IdentityModel can be used to integrate with IdentityServer’s reference tokens). In summary, a JWT is a self-contained token (the token itself carries info) whereas a reference token is a pointer to data on the server. “When using reference tokens – IdentityServer will store the contents of the token in a data store and will only issue a unique identifier for this token back to the client. The API receiving this reference token must then open a back-channel communication to IdentityServer to validate the token.” (OAuth reference token - Auth0 Community). -
Cookie Tokens (Authentication Cookies): In traditional web apps, after a user authenticates, the server creates an authentication cookie that is sent to the user’s browser and then included with each subsequent request. The cookie serves as the token that represents the user’s authenticated session. In ASP.NET Core, the cookie typically contains a serialized, encrypted Authentication Ticket – which includes the user’s identity (claims principal) and some metadata (issue time, etc.). This is handled by the Cookie Authentication Handler (
AddCookie
). The cookie is usually encrypted and signed using the ASP.NET Core Data Protection system (often referred to as the “machine key” in earlier frameworks). This means the cookie is effectively an opaque blob to the client but the server can decrypt/verify it to retrieve the user’s claims (How does ASP.NET Core know that cookie is valid in cookie authentication? - Stack Overflow) (How does ASP.NET Core know that cookie is valid in cookie authentication? - Stack Overflow). By default, cookie authentication in ASP.NET Core is stateless on the server (aside from key management): the server doesn’t need to store session data because the cookie itself carries the claims (hence, it’s somewhat analogous to a JWT but used in a browser cookie). A Stack Overflow explanation summarizes it: the cookie “contains the full serialized, encrypted ClaimsPrincipal and AuthenticationProperties” (How does ASP.NET Core know that cookie is valid in cookie authentication? - Stack Overflow). Only the server (or servers in a cluster, if they share the Data Protection keys) can read or validate it (How does ASP.NET Core know that cookie is valid in cookie authentication? - Stack Overflow). This makes cookie auth highly efficient for web apps – no database lookup on each request – and secure (if cookies are configured with Secure and HttpOnly flags). However, unlike JWTs, cookies are automatically attached by the browser to requests to the same domain, and they are subject to CSRF concerns if not protected (which ASP.NET mitigates with anti-forgery tokens and SameSite cookie settings). Also, cookies are domain-specific; they don’t easily work across APIs on different domains without forwarding or using the same top-level domain.– Cookie size considerations: Because a cookie stores data on the client, very large identities (with many claims or roles) can bloat the cookie. If the size grows too large, you might hit browser limits. In those cases, one can switch the cookie handler to “reference mode” where the cookie contains just a session ID and the server keeps the actual claims in a server-side cache or store (How does ASP.NET Core know that cookie is valid in cookie authentication? - Stack Overflow). This is analogous to a reference token approach but for cookies (this is not commonly needed for most apps unless you have dozens of roles/claims per user).
– Cookies vs JWT for web apps: A common question is whether to use cookies or JWTs for a web app. In ASP.NET Core server-rendered apps, the built-in approach is cookies. If you are building an API and a JavaScript front-end, JWTs are more common. Each has pros/cons: cookies are convenient (automatic, http-only, etc.) but tied to the same origin; JWTs are stateless and more flexible for cross-domain, but you must handle storing them securely on the client. We’ll compare these approaches more in the architecture section.
-
Other Token Types: While JWTs are the prevalent self-contained token, there are other formats like SAML tokens (XML-based, used in some enterprise SSO scenarios) or custom proprietary tokens. In ASP.NET Core, JWTs and cookies are the primary formats you’ll deal with. ASP.NET Core Identity in .NET 8 also introduced a new built-in token format for its API authentication mode (not JWT, a custom encrypted token format) (.NET 8: What's New for Authentication and Authorization), but under the hood these behave similarly to reference tokens (the server validates them via its own key material). The key takeaway is understanding whether a token is self-contained or not, and how it’s validated.
An Identity Provider (IdP) is a service that authenticates users and provides identity information (usually via tokens) to applications. In ASP.NET Core, you can integrate a variety of external IdPs to offload authentication. Here are some common IdPs and how they fit into the picture:
-
Microsoft Entra ID (formerly Azure Active Directory): This is Microsoft’s cloud identity platform used by organizations for enterprise logins, and by developers to secure apps and APIs. Entra ID supports OAuth2 and OpenID Connect and is often used to enable sign-in with Microsoft work/school accounts or personal Microsoft accounts. In ASP.NET Core, you might use the
AddOpenIdConnect
handler or the Microsoft.Identity.Web library to configure authentication against Entra ID. For example, you can protect a web app or API by registering it in Azure AD and configuring the ASP.NET Core authentication options with the tenant info and client ID. Azure AD issues JWT tokens for access and id tokens. (Note: Azure AD was renamed to Microsoft Entra ID in 2023 (New name for Azure Active Directory - Microsoft Entra | Microsoft Learn) (New name for Azure Active Directory - Microsoft Entra | Microsoft Learn), but the underlying technology and protocols remain the same). Entra ID can serve as both an authentication provider (signing in users to your app) and an authorization server for issuing tokens to call APIs (like Microsoft Graph or your own APIs). -
Auth0 and Okta: These are popular commercial Identity-as-a-Service platforms. Auth0 (now part of Okta) and Okta provide a cloud-hosted IdP with support for OIDC, OAuth2, social login integration, user management, etc. Developers often use them to avoid building their own identity management. In ASP.NET Core, integrating with Auth0/Okta typically means using OIDC: you configure the OIDC middleware with the authorization server URL, client ID/secret, and the scopes you need. The app then redirects users to Auth0/Okta for login, and Auth0/Okta sends back tokens. Both Auth0 and Okta issue JWTs by default for access tokens (and an ID token for OIDC). They also have their own SDKs, but you can stick to the standard ASP.NET Core authentication libraries.
-
Keycloak: Keycloak is a robust open-source identity provider (originally by Red Hat/JBoss). It can be self-hosted on your servers. Keycloak supports OIDC and SAML and is often used in self-managed scenarios where a company wants full control over the identity server. If your ASP.NET Core app needs to use Keycloak, you again use OIDC middleware. Keycloak will act as the authorization server/IdP and your app as the client. You just plug in Keycloak’s endpoints (issuer, client id/secret). Keycloak can issue JWT tokens and manage user federations, social logins, etc. It’s a good choice if you want an on-premises IdP and don’t want to roll your own.
-
Social Logins (Google, Facebook, etc.): These providers (Google, Facebook, Microsoft account, Twitter, GitHub, etc.) allow users to sign in with their social media or existing accounts. Under the hood, these also use OAuth2/OIDC (Google fully supports OpenID Connect; Facebook has an OAuth2-based system that returns an access token to fetch user info). ASP.NET Core makes integration easy by providing specific authentication handlers (e.g.,
AddGoogle
,AddFacebook
, etc. from theMicrosoft.AspNetCore.Authentication.*
packages). These are essentially preconfigured OAuth/OIDC clients for those providers. For example, when you useAddGoogle
, ASP.NET Core knows Google’s authorization and token endpoint URIs and handles the OAuth flow for you. After the external provider returns an identity (usually as claims like name, email), you typically sign the user into your app with a local cookie. Many apps combine ASP.NET Core Identity with social logins — i.e., the app has its own user database but allows linking a Google account login to a local user account. -
Custom/Self-Hosted Providers: Instead of using third-party IdPs, some scenarios require a custom identity solution:
- ASP.NET Core Identity (combined with cookie auth) can itself be seen as a mini-identity provider for your single application. It manages users, passwords, and roles. However, on its own it doesn’t issue OAuth2 tokens for other apps to consume; it’s primarily for traditional login to the same app. In .NET 8, Identity introduced API endpoints that can return tokens, but historically if you wanted your Identity-based app to serve tokens, you’d integrate IdentityServer or another OAuth server on top of it.
- IdentityServer (Duende): This is a framework that turns your ASP.NET Core application into a full OAuth2/OIDC Provider. IdentityServer was used in earlier ASP.NET Core templates for SPA authentication. If you host IdentityServer (v5+ from Duende), you can tailor the authentication flows and token issuance to your needs (including support for custom grants, federation, etc.). It’s commonly used for self-hosted scenarios in enterprise where using a cloud IdP is not possible. IdentityServer can use ASP.NET Core Identity as its user store, effectively bridging your local identities to OAuth2/OIDC.
- Other Custom Implementations: While it’s possible to write your own OAuth2/OIDC token issuing logic from scratch, it is not recommended – the specs are complex and security pitfalls are plenty. Instead, use a library like IdentityServer or OpenIdDict if you need a custom IdP. The best practice is to rely on proven components or services for identity.
When integrating an external IdP in ASP.NET Core, the typical pattern is: you register the appropriate authentication handler (AddOpenIdConnect
, AddGoogle
, etc.) with the client credentials (like client ID/secret obtained from the IdP) and necessary settings (callback URL, scopes). You also register cookie auth for maintaining the local session. The authentication middleware takes care of the redirects, token processing, and sign-in. Once the user is logged in, you have a ClaimsPrincipal
that may contain information from the IdP (for example, Google returns name and email claims). You can use these claims for authorization decisions in your app.
It’s also worth noting that many IdPs provide additional features like multifactor authentication, user profile management, password recovery, etc., which you get out-of-the-box by using them. This can significantly reduce the burden of implementing those features yourself.
ASP.NET Core supports various application architectures, and the approach to authentication/authorization can differ for each. Let’s break down common app types and how auth is typically handled in each:
These are traditional web apps where all logic runs on the server and HTML is rendered on the server (Razor views or pages). For these apps, the dominant pattern is cookie-based authentication with optional external login providers:
- Local Authentication & Identity: Many server-rendered apps use ASP.NET Core Identity for managing users. A typical scenario: a user registers or logs in through a form on your site (over HTTPS). ASP.NET Core Identity checks the credentials (perhaps comparing a hashed password in the database) and, if valid, signs the user in by creating an auth cookie. This cookie is then sent with each request, so on subsequent page loads the user is recognized. You secure parts of your app using the
[Authorize]
attribute on controllers or pages, optionally specifying roles or policies. The framework will automatically redirect an unauthenticated user to the login page if they try to access a protected page. - External Logins: If using an external IdP (say, Azure AD for an intranet app, or Google for a consumer app), the app will typically still end up using a cookie internally. For example, with OpenID Connect, when the user signs in via Azure AD, your app receives an ID token and perhaps an access token. At that point, the OIDC handler will sign the user in locally (again via a cookie containing the user’s claims). The difference is that the initial authentication was done by Azure AD. From the app’s perspective, after the OIDC callback, it just has an authenticated user principal (with claims from the external provider) and issues its cookie.
- Session Management: Server-side apps can rely on the cookie’s lifetime or server-side session logic to manage how long a user stays logged in. ASP.NET Core Identity provides features like security stamp validation (to detect if a user’s account info changed and re-authenticate if needed) and easy ways to sign out (invalidate the cookie).
- Calling APIs: If a server-side app needs to call external APIs on behalf of the user (e.g., call GitHub’s API after GitHub login, or call a company’s REST API after Azure AD login), it will typically have an access token from the external provider. The app might store this token (often encrypted in the authentication cookie or in session) and use it to call the external API from the server side. Care should be taken to refresh or handle expiration of these tokens. The ASP.NET Core OIDC handler can be configured to save tokens (via
SaveTokens = true
) for this purpose.
Summary for Web Apps: Use cookie auth for the actual session. Use ASP.NET Core Identity if you want to manage user accounts in-app (with hashed passwords, email confirmation, etc.). Or use OIDC/OAuth to outsource auth to an external IdP. Either way, once authenticated, you have a claims principal in HttpContext.User
and you enforce authorization with attributes or policy checks. This pattern leverages the fact that the browser will send the cookie with each request, and the server can trust that cookie (since it’s signed/encrypted) to identify the user.
Blazor Server shares a lot in common with traditional server-side apps in terms of security. A Blazor Server application runs .NET code on the server and uses a persistent SignalR connection to communicate with the browser. Authentication in Blazor Server is essentially the same as in an MVC/Razor Pages app:
- You can use cookie authentication. When the user navigates to the Blazor Server app, the HTTP connection establishing the Blazor Circuit can carry the auth cookie. If the user isn’t authenticated, you can redirect them to a login page (Blazor Server can integrate with Identity or external providers similarly to an MVC app). Once authenticated (cookie set), all Blazor interactions (which happen via SignalR/websocket) operate under that user’s context. The
AuthenticationStateProvider
in Blazor Server will read theHttpContext.User
(so it knows who the user is). - You can use
[Authorize]
attributes on Blazor components (razor components) to restrict access, just like on MVC controllers. The framework will prevent the UI from rendering for unauthorized users and can redirect to login. - One important note: Because Blazor Server is executing code on the server, authorization checks on the server are reliable and enforceable. You should still guard any sensitive server-side operations with policies or checks (never trust anything coming from the client). But you don’t have to worry about users manipulating the Blazor UI to bypass auth; if they’re not authorized, the server simply won’t execute the action or will refuse it.
- Blazor Server apps often use ASP.NET Core Identity for handling user accounts if they require login, since it integrates well (you might scaffold Identity pages for login/logout, or use an external provider and then create a local identity for that user). Alternatively, they can use Azure AD authentication (there are templates for Blazor Server that hook up to Entra ID, for example).
- In terms of tokens: In Blazor Server, you typically wouldn’t issue JWTs to the browser, because the client isn’t making direct calls to APIs — the server code is doing that. If the Blazor Server app needs to call an API (say a REST API) on behalf of the user, it can use the user’s credentials or tokens (similar to any server-side app). You might use Azure AD’s client libraries to acquire an access token using the user’s session (especially if the user logged in via Azure AD). The server can then use that token to call the API. Essentially, treat Blazor Server as a connected web app.
Blazor WebAssembly apps run entirely in the browser (the .NET code is downloaded and executed in WebAssembly). This scenario is more akin to a Single-Page Application (SPA) from a security perspective. The Blazor WASM app itself is static files; it typically communicates with one or more backend APIs. Key points for Blazor WASM auth:
- Authentication flows: Because the code runs on the client, you can’t use server-side cookie auth in the same way (unless you adopt a special BFF pattern – more on that later). The common approach is to use OIDC/OAuth2 implicit or code flows directly from the Blazor WASM app to an IdP. For example, you might configure Blazor WASM to use Azure AD or Auth0 using OAuth2 Authorization Code flow with PKCE (which is the secure way for public clients). Blazor WASM has support for this via the
Microsoft.Authentication.WebAssembly.Msal
package or by using the OIDC client JavaScript under the covers. The user will be redirected to the IdP’s login page, then back to a special redirect page in the Blazor app, and the Blazor app will get the tokens. - Token storage: Once the Blazor WASM app obtains tokens (an ID token and access token), it needs to store them to make API calls. This is usually done in memory or browser storage. The Microsoft Blazor WASM auth library (using MSAL) will typically keep tokens in browser localStorage or sessionStorage by default to persist the auth state. Developers need to be aware of XSS risks – anything stored in localStorage is accessible to malicious scripts if your app has an XSS bug. As a best practice, ensure you protect your Blazor app from XSS and consider token lifetime/refresh strategies accordingly. (Alternatively, one can store tokens in browser cookies with HttpOnly, but JavaScript/WASM code wouldn’t be able to directly read those; that pattern leans into the BFF approach).
- Calling APIs: Blazor WASM will call your backend APIs (or third-party APIs) by attaching the access token in the
Authorization: Bearer
header. Typically, you configure anHttpClient
in the Blazor app that automatically adds the token. The backend API must be configured with JWT Bearer authentication and know to trust the issuer (e.g., your Azure AD tenant or Auth0 domain). - Authorization in Blazor WASM UI: Blazor WASM provides an
AuthorizeView
component and similar features to show/hide UI based on the user’s authentication state or roles/claims. This is purely client-side logic – it can improve user experience (e.g., not showing an “Admin” button to a non-admin user), but it cannot actually secure the data. Any critical checks must happen on the server API. As with any SPA, a user could manipulate the client-side code or calls, so the server must enforce authorization on API endpoints. - ASP.NET Core Identity with Blazor WASM: One variant is the Hosted Blazor WASM model where you have three projects: the WASM client, an ASP.NET Core API, and optionally an IdentityServer or Identity backend. For example, Microsoft’s template for Blazor WASM with Identity (pre-.NET 8) would configure IdentityServer on the server to issue JWTs to the Blazor client. The Blazor app would log in via that IdentityServer (which in turn might use ASP.NET Core Identity for user storage), and then use the obtained JWT to call the hosted API. Essentially, the server was both the IdP and the API resource server. In .NET 8, since the IdentityServer is no longer included by default, a similar setup can be achieved using the new Identity API endpoints (which issue tokens) – or by integrating an external IdP. The main idea remains: Blazor WASM acts as an OAuth client obtaining tokens, and uses them to authenticate to APIs.
In summary, Blazor WebAssembly is like any single-page application: use OAuth/OIDC to authenticate the user and get tokens, store those tokens safely, and secure the back-end APIs with JWT validation. The UI can reflect the user’s auth state (e.g., show logged-in user’s name, or only render certain components if User.IsInRole("Admin")
via AuthorizeView
). But always enforce the real rules on the server side (the API).
If you are building a Web API (or a set of microservice APIs) with ASP.NET Core, you typically won’t have server-side HTML or cookies at all. Instead, these APIs are meant to be consumed by clients (SPA, mobile app, other servers). The common pattern is token-based security:
- JWT Bearer Authentication: You configure JWT bearer middleware (usually in
Program.cs
something likebuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => {...})
). You set up the options to validate tokens issued by your IdP. For example, if using Azure AD, you might setoptions.Authority = "https://login.microsoftonline.com/<TenantID>/v2.0"
andoptions.Audience = "<ClientID>"
or useoptions.TokenValidationParameters.ValidAudience = "api://...id"
depending on Azure AD setup. The JWT handler will download the signing keys (via OpenID Connect metadata) and automatically validate the token’s signature, issuer, audience, and expiry. - Authorize Attributes and Policies: On your controllers or endpoints, you use
[Authorize]
to require a valid token. You can further require specific scopes or roles by using policy-based authorization. For instance, if you want to ensure that only tokens with the scope “Files.ReadWrite” can access a certain endpoint, you might define a policy:options.AddPolicy("FilesScope", p => p.RequireClaim("scp", "Files.ReadWrite"));
and then use[Authorize(Policy = "FilesScope")]
on the controller action. Similarly, if the token carries roles (say your JWT has a “roles” claim or an “admin” claim), you can require those. Many JWTs from IdPs put roles in a claim type like “roles” or “role” or even as part of “groups”. You might need to configure the JWT handler’sRoleClaimType
to ensureUser.IsInRole()
works with that. By default, if the token has a claimhttp://schemas.microsoft.com/ws/2008/06/identity/claims/role
, ASP.NET will map that to roles. - Stateless nature: Each API call is stateless – the API doesn’t remember anything about past calls. It trusts the token on each request. This means you don’t have server-side sessions, which is good for scalability (each server can independently validate tokens).
- Protecting sensitive data: For highly sensitive operations, beyond checking scopes/roles, you might implement additional checks (like verifying the user’s organization or other claims). This can all be done in an authorization handler or in the controller logic if needed.
- Authentication vs API keys: Note that some APIs use API keys or other methods. But when we talk about OAuth2/OIDC tokens, we’re focusing on user or service tokens. If you issue API keys, you might treat them similarly to an opaque token – e.g., check a database for the key. But that’s outside OAuth’s scope (often custom).
- CORS: If your API is consumed by a web front-end on a different origin (e.g., a React app hosted on
https://frontend.com
calling an API onhttps://api.yourapp.com
), you must enable CORS in ASP.NET Core for the JWT-protected endpoints, or else the browser will block the requests. CORS doesn’t directly relate to auth, but it’s a necessary configuration to allow the token to be sent via AJAX from a different origin.
Development tip: Secure your API methods early. It’s easy during development to leave [AllowAnonymous]
and forget – which would be a big hole. A best practice is to globally require authorization (via .RequireAuthorization()
on endpoints or a global filter), and then explicitly allow anonymous only where needed (like a health check or an open endpoint). This way, you don’t accidentally expose an API without auth.
This is a very common scenario: you have a JavaScript front-end (React, Angular, Vue, etc.) and a separate ASP.NET Core API. Essentially, this is similar to Blazor WASM + API, but with potentially different tech on front-end. Key aspects:
- User Authentication: The SPA cannot securely store a client secret, so it will use an OAuth2 flow designed for public clients – typically the Authorization Code flow with PKCE (Proof Key for Code Exchange). This is the modern standard for SPAs. Your SPA will redirect the user to an IdP (say Auth0, Okta, or Azure AD B2C) with PKCE, the user logs in, and the IdP redirects back to a redirect URI in your SPA with an authorization code. The SPA (via code in the browser) exchanges that code for tokens. Many SPAs use libraries like Auth0.js, Okta Auth JS, or MSAL.js to handle this process, which includes the token exchange via XHR/Fetch (since doing the exchange requires sending the PKCE verifier and getting tokens, which usually happens via a silent XHR or a dedicated redirect flow). After this, the SPA has an ID token and access token (and possibly a refresh token if using a secure context or Auth0’s rotating refresh tokens for SPAs).
- Storage and Security: Once the SPA has the access token, it needs to store it to include in API calls. As mentioned, storing in
localStorage
is common but poses XSS risk; storing in an HTTP-only cookie is another approach (but then you need to handle CSRF). A growing pattern is the Backend for Frontend (BFF) approach, where the SPA doesn’t directly get the token at all – instead, the token is stored in a secure server-side session (e.g., the ASP.NET Core app acts as a proxy). In a pure SPA approach without BFF, you accept the trade-offs and must harden the app accordingly (CSP, XSS prevention). - API calls: The SPA will call the ASP.NET Core Web API by including the access token in the header (
Authorization: Bearer ...
). The ASP.NET Core API validates it (just like the previous section on APIs). If the token is expired or invalid, the API returns 401, and the SPA should handle that (perhaps triggering a refresh token flow or redirect to login). - Cookie vs Token for SPAs: It’s worth contrasting: Some developers attempt to use cookie authentication for SPAs (for example, serve the React app from the ASP.NET Core app and use the same domain cookie). While this can work (the React app would do a form-post login or use an XHR to a login endpoint and get a cookie), it often runs into issues with CORS and third-party cookies if the IdP is separate. A hybrid approach is the BFF model: the SPA talks to its own backend (same origin) which uses cookies, and that backend in turn does token stuff with IdP. This can be very secure (no tokens in the browser at all) and avoids CORS, but it introduces more complexity and network calls. The official docs and many community solutions are increasingly advocating BFF for high-security SPAs. For documentation purposes, know both patterns. The classic pattern is “SPA gets a token and calls API,” which we’ve described. The BFF pattern is “SPA calls its own server (maybe through relatively dumb endpoints), server calls API or IdP as needed and returns data, using a secure cookie for the SPA->BFF leg”.
- Logout in SPA: Logging out often means both clearing the SPA’s tokens (and any client state) and revoking tokens or ending the session at the IdP. With OIDC, there’s often a logout endpoint you can redirect the user to (e.g., a Microsoft Entra ID logout URL, or Auth0 logout, etc.) which will clear the server-side session at IdP and optionally redirect back. You’d also drop the token from storage or clear the cookie. This ensures the user is fully signed out.
In summary for hybrid SPA+API: Use OAuth/OIDC flows to authenticate the user in the SPA, use JWT bearer on the API, and ensure you handle the tokens carefully. The separation of client and server means things like CORS and CSRF need to be considered. It’s a bit more complex than a server-rendered app, but it provides great flexibility and scalability (your API can serve multiple clients, etc.).
The ASP.NET Core security model is claims-based, meaning an authenticated user is represented by a set of claims. Authorization decisions then use those claims, often in the form of roles, scopes, or custom policies. Let’s clarify these terms and how to configure them:
-
Claims: A claim is simply a piece of information about the user (or entity) – a key-value (with optional metadata like issuer). For example, a claim might be “name = Alice”, “email = [email protected]”, or “EmployeeId = 12345”. In ASP.NET, the
ClaimsPrincipal
holds a collection of claims (accessible viaUser.Claims
in a controller). Each claim has a type (a string, often a URI or a well-known name) and a value. Claims can come from various sources: if you use ASP.NET Core Identity, by default it issues claims like the user’s unique ID, their username, and their roles. If you authenticate via OIDC, the external IdP might supply claims like given name, surname, email, and perhaps group memberships. Claims are the currency of identity – they allow for a flexible representation beyond just “username”. You could have a claim for almost anything (age, security clearance, preferred language, etc.). In authorization, you might check for the presence or value of certain claims. For example, you could require that a user has an “EmailVerified=true” claim to allow posting in a forum, etc. Think of claims as attributes of the user. (“A claim is a piece of information about the user, which is, in essence, a key-value pair.” (Getting to Know the Identity of .NET 4.5 - CODE Magazine)). In ASP.NET Core, you rarely create the claims principal manually (except in cookie auth during login), but you often consume it. -
Roles: Roles are a classic way to assign permissions by grouping users (role-based access control). In ASP.NET Core, roles are often used through the Identity system: a user can belong to one or more roles (stored in the Identity DB). Roles in implementation are actually just claims with a special type (typically
ClaimTypes.Role
or"role"
). When a user in Identity has roles, the Identity system will create a claim for each role upon sign-in. So you might see claims like “role = Administrator”, “role = User”. The[Authorize(Roles="Admin")]
attribute simply checks if the user has a claim with type role and value “Admin”. However, conceptually roles differ from arbitrary claims because they are intended to represent broad categories of user membership (and often each role implies a set of permissions). One important distinction made in documentation: “While roles are claims, not all claims are roles. Claims are meant to be information about an individual user. Using roles to add claims to a user can confuse the boundary between the user and their individual claims.” (Role-based authorization in ASP.NET Core | Microsoft Learn). This means roles ideally shouldn’t be overloaded to store attributes (that’s what claims are for). Roles describe a set of users (e.g., “Administrators” group), whereas claims describe properties of a single user (c# - Should it be a claim, a role or a policy? - Software Engineering Stack Exchange). You might use roles like “Admin”, “Manager”, “Subscriber”, etc., to gate certain actions. Roles are easy to check and manage if the number of roles is relatively small and static. ASP.NET Core Identity has a RoleManager to create roles and assign users to roles.- Roles in tokens: If using external IdPs, not all will use the same concept of roles. Azure AD has “groups” or application roles that can be emitted in tokens, which you can map to roles in your app. Okta/Auth0 might also include roles or you might treat certain claims as roles. The ASP.NET JWT bearer auth can be told what claim type to treat as a role (via
TokenValidationParameters.RoleClaimType
). So you might map, say, an Azure AD “groups” claim GUID to a role name in your app, but that might require a lookup unless the token contains human-readable roles. - When to use roles vs claims: If your authorization logic is mostly about grouping users (like “only users in the Admin role can do X”), roles are convenient. If it’s more dynamic or attribute-based (like “only users with Department = Finance and Clearance = High can do X”), claims (and policy) are more straightforward. Roles become unwieldy if you have too many distinct permissions – that’s when you consider a claims or policy-based approach.
- Roles in tokens: If using external IdPs, not all will use the same concept of roles. Azure AD has “groups” or application roles that can be emitted in tokens, which you can map to roles in your app. Okta/Auth0 might also include roles or you might treat certain claims as roles. The ASP.NET JWT bearer auth can be told what claim type to treat as a role (via
-
Scopes: In the context of OAuth2/OIDC, a scope is a string that represents a permission or set of permissions that a client is requesting on behalf of a user. For example,
openid
,profile
,email
are OIDC scopes for basic profile info. Or an API might define scopes likeread:messages
orwrite:files
. Scopes end up as claims in the access token (often a claim namedscp
orscope
containing a space-delimited list of scopes). In an ASP.NET Core Web API, you’ll commonly check the token’s scopes to authorize certain endpoints. For instance, if you use Azure AD to protect an API, you might define App Roles or Delegated Permissions in Azure, which appear as roles or scopes in the token. You can then write a policy:options.AddPolicy("Files.Write", p => p.RequireClaim("scp", "Files.Write"))
. This requires that the token presented includes the “Files.Write” scope. If using the ASP.NET Core JWT middleware with Azure AD, there’s also an extension method (in Microsoft.Identity.Web)AddAuthorization(options => { options.Initializer.AddAzureADBearer(...).AddPolicy("ScopePolicy", p=>p.RequireScope("Files.Write")); })
that simplifies this. In summary, scopes are like coarse-grained permissions delegated to the client from the user. They are not user-specific attributes, but rather rights the user (resource owner) has consented to give to the client. In terms of implementation: treat them as claims and useRequireClaim
or the Microsoft provided helpers to enforce. -
Policy-based Authorization: Policies in ASP.NET Core are a flexible way to express authorization rules in code. A policy has a name and one or more requirements, and you can register them in
AddAuthorization
. For example, you might create a policy named "EmployeeOnly" that requires the claim "EmployeeID" to exist. Or a policy "AtLeast21" that requires a claim "DateOfBirth" such that the user is 21 years or older. Policies are evaluated by authorization handlers which you can write if the logic is complex (like checking multiple claims or external resources).- Roles and Claims via Policy: Roles and scopes as mentioned can actually be enforced via policies internally (and they often are, under the covers).
[Authorize(Roles="Admin")]
and[Authorize(Policy="AdminPolicy")]
could do similar things. Microsoft encourages developers to use policy-based auth for complex scenarios. For simple role checks, the[Authorize(Roles=...)]
is fine. For anything else, define a policy. Example:oroptions.AddPolicy("AdminOrManager", policy => policy.RequireRole("Administrator","Manager"));
Thenoptions.AddPolicy("HasBadge", policy => policy.RequireClaim("EmployeeBadge", "True"));
[Authorize(Policy="HasBadge")]
on a controller enforces that requirement. - Resource-based policies: ASP.NET Core also allows you to pass the resource being accessed to the policy. This is more advanced, used when e.g. you want to check if the user has access to a specific entity (like checking if a user is the owner of a record). This involves imperative authorization (
AuthorizationService.AuthorizeAsync(user, resource, "PolicyName")
) and writing a handler that examines the resource. That might be relevant in documentation for fine-grained auth (like permissions at a data level).
- Roles and Claims via Policy: Roles and scopes as mentioned can actually be enforced via policies internally (and they often are, under the covers).
-
Permissions: The term "permissions" can mean different things, but generally it refers to fine-grained actions a user can perform (sometimes overlapping with scopes or claims or roles). For example, you might define application-specific permissions like “CanPublishArticles”, “CanDeleteComments”. How do you implement that? There’s no built-in “Permission” object in ASP.NET Core, but you can implement it via claims or roles:
- One approach: Use a claim for each permission. e.g., a user might have claims
perm=CanPublishArticles
,perm=CanDeleteComments
. You can then have a policy for each permission:policy.RequireClaim("perm", "CanPublishArticles")
. This is easy to check but might lead to a lot of claims if a user has many permissions. - Another: Use roles to group permissions (if roles fit). e.g., “Editor” role implies publish and edit rights; “Moderator” role implies delete comments, etc. Then check roles.
- Another: Just define a policy with custom logic. Perhaps your user has some “PermissionLevel” claim or you maintain a lookup somewhere; your handler can examine
User
and external data to decide if the permission is granted. - The key difference is that roles are meant to be coarse and tied to identity, whereas permissions can be very granular and numerous. For maintainability, sometimes a combination is used (role grants a bunch of permissions, plus some individual overrides as claims).
- In official docs, roles and claims cover 95% of use cases. The concept of “permissions” as a separate entity is up to the developer’s design.
- One approach: Use a claim for each permission. e.g., a user might have claims
How to configure these in ASP.NET Core:
- When using ASP.NET Core Identity, you call
AddDefaultIdentity
or similar which by default sets up authentication and authorization to use Identity’s user store and roles if enabled. Identity will populate the user’s claims (including roles) when they sign in. If you want to add custom claims for a user from the Identity database (like user’s department), you might use a claims transformer or include it in the cookie on sign-in (there are events you can hook, or you store those in the user’s profile and query them as needed). - Without Identity, if using only external IdP, the claims come from the tokens. The JWT handler will populate
ClaimsPrincipal
with claims from the JWT (by default it even maps some standard JWT claims to .NET claim types, e.g.,sub
->ClaimTypes.NameIdentifier
). You can disable or adjust the mapping if needed (JwtBearerOptions.MapInboundClaims = false
if you want raw claim types). - You add authorization policies in the
Services.AddAuthorization
call inProgram.cs
or startup. Also, you can use attributes like[Authorize(Roles="X")]
or[Authorize(Policy="Y")]
on controllers, Razor pages, or Blazor components. - If using Roles with Identity, ensure you called
AddRoles<IdentityRole>()
when configuring Identity (this adds the role manager and tells Identity to include role claims) (Role-based authorization in ASP.NET Core | Microsoft Learn) (Role-based authorization in ASP.NET Core | Microsoft Learn). Then Identity’s user manager can assign roles to users which are persisted in the database AspNetUserRoles table. - If you have a JWT from an external provider that includes roles (say Azure AD can include roles or groups), you might not need Identity at all – you can rely on those claims directly in the token for authZ. For example, Azure AD allows defining app roles and then you can do
[Authorize(Roles="SomeRole")]
if you configure the JWT token validation to map the role claims correctly. - Claims Transformation: If you need to augment or transform claims once the user is authenticated, ASP.NET Core provides
IClaimsTransformation
. For instance, you could automatically add a “permission” claim based on a user’s role, or fetch extra user data from a DB and add it to claims after login. This runs on every request for that user (or you can cache it). This is useful for things like dynamic permissions or legacy role mapping.
In essence, claims are the foundation, roles and scopes are just particular claims that have special meaning by convention, and policies are how you evaluate those claims (and other conditions) to make authorization decisions. The system is very flexible – you can express simple role checks or complex logic with the policy model. The official recommendation is to prefer policy-based checks in new applications, as they scale better for complex requirements and testing.
Implementing authentication and authorization comes with many security considerations. Mistakes can lead to serious vulnerabilities. Here are common pitfalls to avoid and best practices to follow when securing ASP.NET Core applications:
-
Always Use HTTPS: Never transmit authentication credentials or tokens over unencrypted connections. An obvious but critical rule: enable HTTPS on your site and API. In fact, ASP.NET Core templates enforce HTTPS by default. Using plain HTTP for auth is a common mistake to avoid (Authentication and Authorization Best Practices), as an attacker could sniff tokens or cookies in transit. Ensure that cookies have the
Secure
flag so they’re not sent over HTTP. For APIs, simply do not host them on HTTP in production. (During development, use https://localhost). -
Protect Secrets and Keys: Never embed sensitive secrets (client secrets, API keys, encryption keys) in frontend code or commit them to source control (Authentication and Authorization Best Practices). Use tools like user-secrets for development and secure environment variables or vaults for production. In authentication, this applies to things like JWT signing keys (which should be properly safeguarded if you manage them) and third-party IdP secrets. If you accidentally leak a secret (like push it to GitHub), assume it’s compromised and rotate it (generate a new one) (Authentication and Authorization Best Practices).
-
Don’t Roll Your Own Crypto or Protocol: Use the built-in frameworks and standard protocols. ASP.NET Core Identity, the Microsoft authentication libraries, or well-known libraries like IdentityServer implement security best practices. Building a custom auth scheme or crypto algorithm is error-prone. A common mistake is implementing custom auth without necessity (Authentication and Authorization Best Practices) – e.g., inventing a new token scheme or writing your own password hashing. Instead, rely on Identity (which by default uses proven password hashing algorithms and user lockout, etc.) or use OAuth/OIDC libraries. Only write custom auth handlers if you absolutely must (and even then, consider a security review).
-
Validate All Tokens Properly: If you use JWTs or other tokens, make sure to validate all aspects of them – signature, expiration, audience, issuer, etc. The JWT middleware by default does this, but if you ever manually parse tokens, do not just decode the JWT and trust its contents without validation. A dangerous anti-pattern: some might use
JwtSecurityTokenHandler().ReadToken()
or decode the base64 payload and not verify the signature – this can be exploited easily (an attacker could forge a token). Always let the framework validate the token using the known signing keys. Also ensure you configure the expected issuer and audience: for example, if your API only expects tokens from your tenant or your Auth0 domain, set those. The JWT middleware will then reject tokens from a different issuer or wrong audience. -
Use Strong Identity Store Practices: If using local accounts, store passwords securely (ASP.NET Identity does this by default using hashed+salted passwords with the latest Go-based KDF or PBKDF2). Do not store passwords in plaintext or using weak hashes (MD5, SHA1 are inadequate). Also consider enabling additional protections: account lockout on multiple failed attempts (Identity has this feature to prevent brute force), 2FA/MFA for sensitive accounts, password strength policies, etc. Not enforcing strong passwords or lockout is a pitfall – it makes brute-force or credential stuffing easier (Authentication and Authorization Best Practices).
-
Be Cautious with Persistent Logins: If you allow “Remember Me” (long-lived sessions), understand the risk that if a token or cookie is stolen, it could be used for a long time. Mitigate by using sliding expiration and absolute expiration for cookies appropriately. ASP.NET Core Identity’s cookies by default use a 14-day persistent cookie if “Remember Me” is checked. If security is a bigger concern than convenience, you might shorten that. Also, consider implementing refresh token rotation if you have long-lived refresh tokens on SPAs or mobile (some IdPs handle this for you, ensuring a stolen refresh token can be detected/invalidated).
-
Secure Cookies Properly: If using cookie authentication, set the cookie options to secure defaults:
HttpOnly = true
(so JavaScript cannot read the cookie and steal it – this helps mitigate XSS impact).SameSite
attribute appropriately: Lax or Strict for most apps to prevent CSRF in cross-site contexts. If your app needs cross-site POST (rare for same-site cookies beyond OAuth login flows), then you may allow SameSite=None with Secure. The default in ASP.NET Core was adjusted to Lax to accommodate OAuth logins (Use cookie authentication without ASP.NET Core Identity | Microsoft Learn), but you should evaluate based on your scenario.Cookie.SecurePolicy = Always
to force secure (HTTPS).- If the app is multi-subdomain and you need the cookie across them, carefully set the domain and consider implications.
- Use the Cookie Policy Middleware to help apply things globally (and to log or adjust cookie parameters centrally) (Use cookie authentication without ASP.NET Core Identity | Microsoft Learn).
- Anti-Forgery (CSRF) Tokens: For cookie-authenticated web apps (MVC/Razor), always use the anti-forgery token on state-changing POST forms (the template does this). If you build custom AJAX that modifies server state, include an anti-CSRF header/token (or ensure SameSite cookies protect it). This prevents malicious websites from triggering actions using a user’s cookie (CSRF attack). ASP.NET Core’s
[ValidateAntiForgeryToken]
and related attributes are your friend.
-
Watch for Open Redirects: If you use the
[Authorize]
attribute, unauthenticated users get redirected to a login page. Make sure you validate or restrict theReturnUrl
parameter to avoid open redirect attacks (where a user could be tricked into logging in and then sent to a malicious URL). The Identity system’s login page logic usually handles this by checkingUrl.IsLocalUrl(ReturnUrl)
before redirecting. If you implement custom login endpoints, do the same. -
Limit Exposure of User Data in JWTs: If you design custom tokens, avoid putting sensitive personal data in them. Remember JWTs can be decoded by anyone who intercepts them (if not encrypted). They are only base64 encoded and signed, not encrypted. So don’t include things like plain-text passwords (never), or overly detailed profile info. Include only what you need for authZ decisions. If you have lots of user data needed by APIs, consider that the API might fetch details from a database after auth rather than bloating the token.
-
Use Scopes/Least Privilege: When requesting tokens (from an IdP), request the minimal scopes needed. Don’t ask for broad scopes like
admin
orwrite
for everything if the app only needs read access, for example. On the API side, check for specific scopes/roles to ensure the token presented has been issued with the right privileges. This prevents a token meant for one thing from accessing another. Also, segment your APIs by audience if needed – e.g., issue separate tokens for different microservices to reduce the blast radius if a token is misused. -
Implement Logging and Monitoring: It’s best practice to log authentication events (without sensitive info). For example, log failures (with reasons), and obviously log sensitive admin activities. Use ASP.NET Core’s built-in logging to catch unexpected authZ failures or token validation exceptions; these could indicate someone probing your system. Also consider enabling Azure AD or your IdP’s sign-in risk detection if available. From the app side, monitor for unusual patterns (e.g., many 401s could mean someone is trying a token replay or guessing).
-
Keep Authentication Middleware Updated: Stay up to date with the latest ASP.NET Core security updates. The frameworks do get patches for vulnerabilities. Also, if you roll in libraries like IdentityServer or others, keep them updated. Using outdated crypto (like an old IdentityServer with known issues, or an old ASP.NET Core where a vulnerability was patched) is a risk.
-
Disable Legacy Protocols If Not Needed: For example, if you integrate Azure AD, you likely use OIDC (which is OAuth2/OpenID Connect). Azure AD also supports WS-Fed and SAML endpoints for compatibility. If your app doesn’t need those, don’t use or enable them. Similarly, if using IdentityServer, you might choose to not enable the legacy resource owner password flow. Reducing attack surface is always good.
-
Test your authentication flows: Use tools or just manual testing to ensure that unauthorized access is truly blocked. For example, after implementing an API, try calling it without a token or with an expired token – does it correctly return 401? Try calling an admin endpoint with a normal user’s token – is it forbidden? Run through the OAuth flows and see if anything can be tampered with. If you discover you can, say, change the
scope
parameter in the URL during login to get more privilege than intended, fix your IdP settings to prevent that. -
Beware of JSON Deserialization (if you accept raw JSON in APIs): While not directly auth, injection attacks through model binding can be a risk if you’re not careful with what you accept. Use model validation and don’t blindly trust any data coming in just because the user is authenticated.
-
Logout and Session Invalidation: Provide a way for users to log out which clears their cookie or token. If using JWTs without server state, you can’t force invalidate a JWT (until it expires), but if you have a refresh token or session list, you could revoke those. For cookie auth, calling
SignOut
will clear the cookie. One pitfall is not clearing the authentication cookie properly (make sure path and name match when expiring it). Another is when using multiple auth schemes (like cookie + JWT in the same app) – ensure you sign out of all if needed (like if an IdP session also should be logged out via OIDC signout, etc.).- Also, consider scenarios like password changes: If a user changes their password or an admin disables an account, you might want to invalidate active sessions. In Identity, updating the SecurityStamp and configuring the cookie validation interval can force users to re-login when such changes occur.
-
Cross-Site Scripting (XSS): This isn’t directly authZ, but an XSS vulnerability in your app can be catastrophic for security, especially for SPAs that store tokens. An XSS could allow an attacker to run JS in the user’s browser, potentially stealing tokens (if in JS accessible storage) or making requests on behalf of the user (which if you rely solely on client-side checks, could be bad). So follow best practices to avoid XSS (encode output, avoid eval, consider a CSP). For Blazor WASM and other SPAs, a well-configured Content Security Policy and using frameworks’ built-in protections go a long way.
Following these best practices will help ensure your authentication and authorization implementation is robust. Security is a broad topic, but focusing on protecting credentials/tokens, using proven libraries, and enforcing least privilege will address many of the common pitfalls.
Stepping back, it’s useful to consider the architecture patterns for handling auth in modern applications. The two fundamental patterns we’ve touched on are session-based (cookie) authentication and token-based authentication. Many applications use a combination of both, depending on their components. Let’s compare these patterns and variations (with a visual aid):
(Cookies vs. Tokens: The Definitive Guide) Traditional cookie-based session auth (left) vs modern token-based auth (right). In cookie auth, the server creates and stores a session (or session token in a cookie) after login, and the browser automatically sends the cookie with each request. In token-based auth, the client receives a token (e.g., JWT) and must explicitly send it in an Authorization header for each API request. The right-side also often involves separate domains for SPA and API.
In this pattern, the server and client maintain a session using cookies. The steps typically are:
- Login: User provides credentials (or goes through an external login flow). Server validates and creates a session – often by serializing the user’s identity into an encrypted cookie stored on the client (How does ASP.NET Core know that cookie is valid in cookie authentication? - Stack Overflow) (How does ASP.NET Core know that cookie is valid in cookie authentication? - Stack Overflow). The server may also keep a session record (e.g., in a database or in-memory store) if not using self-contained cookies.
- Requests: The browser automatically attaches the cookie to every request to the server’s domain. The server, via middleware, reads the cookie, deserializes the identity (checks signature to ensure it’s not tampered), and establishes
HttpContext.User
. This happens for each request without extra effort from the client or server (just the initial overhead of decrypting the cookie). - AuthZ: The server can check
User
and enforce [Authorize] attributes as needed, knowing that ifUser
is set, the user is authenticated. - Logout: The session is terminated by clearing the cookie (and possibly clearing server-side session if applicable). The browser no longer sends the cookie (or it’s invalid), so the user is signed out.
Pros: Simplicity for the developer (the browser and server handle passing the token via cookie, you don’t manually attach headers in your code). Cookies can be HttpOnly, adding protection. It’s a good fit for web apps where the client and server are the same origin. Also, because the cookie can be considered one big token, it can contain a lot of claims (within size limits) and the server can trust it without database calls (if using self-contained cookies).
Cons: Doesn’t work well across domains (if your API is separate domain from front-end, the cookie won’t be sent to it unless you allow cross-site cookies, which raises CSRF issues). Also, cookies are subject to CSRF since the browser will send them automatically – you must implement anti-forgery measures for state-changing operations. Another con is scaling if using server-stored sessions (but ASP.NET Core’s default cookie auth is stateless, so that’s not a big issue unless you opt into server storage). Additionally, some modern architectures (mobile apps, SPAs from separate origins) can’t use cookies easily – mobile apps don’t automatically store and send cookies unless managed, and cross-origin SPAs face SameSite restrictions.
Use cases: Traditional multi-page web applications (Razor Pages/MVC). Internal enterprise apps where users access via a web browser and SSO with corporate IdP (the app can use OIDC and then maintain its own cookie). Anytime you have a web UI served by ASP.NET Core, cookie auth is typically the go-to method.
In this pattern, there is no implicit session via cookies. Instead, after authentication, the client is given a token (like a JWT). The client is responsible for storing and presenting that token on each request to the server (usually in the HTTP Authorization header).
Using the diagram’s right side as reference:
- Login/Token issuance: The user authenticates (e.g., by sending credentials to an auth endpoint or via an OAuth redirect flow). The server (or IdP) responds with a JSON containing tokens (access token, etc.) instead of setting a cookie. For example, a response might be
{ "token": "<JWT here>", "refresh_token": "<GUID>" }
. The client (browser JS or mobile app) then stores these tokens (in memory, localStorage, or a secure storage area). - Requests: For subsequent API calls, the client must manually include the access token, typically by adding an
Authorization: Bearer <token>
header. The browser will not attach tokens automatically – the app’s code does it, often usingfetch
orHttpClient
with an authorization header. The server’s JWT middleware sees the header, takes the token, validates it, and if valid populatesHttpContext.User
. - AuthZ: From here, the authorization is similar – the server uses
[Authorize]
and policies to check the claims in the token. - Token Refresh: Because tokens have expiry, if a refresh token was provided, the client may at some point call a token refresh endpoint to get a new access token when the old is about to expire. This is an extra moving part compared to cookie-based sessions (where sliding expiration can happen transparently on the server). The refresh flow must be handled carefully (store refresh token securely, rotate it if possible to avoid use of stolen refresh tokens).
- Logout: Clearing a token on the client simply means removing it from storage so it’s not sent anymore. There’s no server-side state to clear (unless you maintain a token revocation list). You might call an IdP’s revoke endpoint to invalidate the refresh token.
Pros: It’s stateless on the server – any server node with the public key can validate the JWT, no session persistence needed. It’s also inherently suited for APIs and microservices, where clients other than browsers might call (mobile apps, other servers). For cross-origin scenarios, tokens are not automatically sent, which means you avoid CSRF by design (an attacker’s website can’t force the user’s browser to attach a token in an Authorization
header – they don’t have access to it if stored properly). Also, multiple different APIs on different domains can all accept the same token (if they share the issuer trust), which is great for distributed systems.
Cons: Puts more responsibility on the client-side: developers must implement storage and transmission of tokens. If done incorrectly, it can introduce vulnerabilities (e.g., storing JWT in plain localStorage is susceptible to XSS theft; or not using HTTPS would leak tokens). Also, revocation of stateless tokens is tricky – you either accept the time window until expiry or implement an infrastructure (like an revoke list or short expiration + refresh). Another con is performance: parsing and validating a JWT is very fast and usually negligible, but it is more data being sent each request compared to a small session cookie ID. And if the token is large (many claims), that’s extra bandwidth each call. Usually fine, but it’s a consideration in some high-throughput cases.
Use cases: SPAs and mobile apps are the biggest uses. Also any scenario where your frontend is decoupled from the backend and/or you want a unified auth across many services. If you have say a React app, a mobile app, and maybe allow third-party integrations, issuing JWTs from a central IdP that all your APIs trust means any of those clients can call any service with the token. Another use case is machine-to-machine (client credentials) – there’s no browser, so a token is the natural choice (e.g., a nightly job gets a JWT to call an API).
Many real-world solutions use both patterns in tandem:
-
Cookie + JWT: An ASP.NET Core server-rendered app might use a cookie for its own session, but also obtain a JWT to call a downstream API. Or it might set a JWT as a cookie (for legacy reasons) but typically it would just keep the JWT in memory or a server-side cache if needed.
-
BFF (Backend for Frontend): This is a variant where the front-end (browser JS) does not directly get a token. Instead, when the user logs in, the token is stored in a secure http-only cookie by the server (the BFF). The React/Angular app then communicates with the BFF (on the same domain, so cookie is automatically sent) for any API data, and the BFF internally adds the token when calling the actual API. In effect, the SPA uses cookie auth with its BFF, and the BFF uses token auth with other APIs. This pattern tries to get the best of both worlds: it avoids exposing tokens to the browser JavaScript (mitigating XSS risk), and it leverages the ease of cookies for the browser-BFF communication, while still being stateless or token-based for the BFF-backend communication. ASP.NET Core can serve as a BFF: for example, use
AddCookie
for the front-end, and also useAddOpenIdConnect
or a manual OAuth flow to obtain tokens from an IdP, storing them server-side (perhaps in the cookie or in a server cache keyed by session). The BFF endpoints would require the cookie (ensuring the user is logged in) and then useHttpClient
with the bearer token to fetch data from other APIs. The Duende BFF library (by the IdentityServer folks) provides an out-of-the-box implementation of this concept for .NET. This is a relatively new but increasingly popular architecture for SPAs that need high security. -
Microservices with a Gateway: You might have many microservice APIs and one gateway (perhaps using YARP or Ocelot in .NET). The gateway could handle authentication (validate JWT or handle cookie -> token translation) and then forward the request to downstream services, perhaps with a stripped-down token or using an internal token. For docs purposes, just know that architecture exists. In such cases, each microservice might trust a token issued by the gateway or the original IdP.
-
SignalR authentication: If you use SignalR (or any persistent connection) along with token auth (like for a SPA), you can send the JWT as part of the connection negotiation. ASP.NET Core allows JWT authentication to apply to WebSockets/SignalR as well. Or if using cookies, it just uses the cookie. This is just a note that real-time hubs integrate into the same auth framework.
High-Level Trade-offs: A quote from an Okta developer article encapsulates a key difference: “The main difference between cookies and tokens is their nature: tokens are stateless while cookies are stateful.” (A Comparison of Cookies and Tokens for Secure Authentication | Okta Developer). Cookie auth is considered stateful because typically the server maintains a session or at least depends on the cookie for state; token auth is stateless as each request is self-contained with auth info. Neither is universally better – it depends on your app type and needs.
For a new ASP.NET Core app, you might use:
- Cookie auth if it’s a standard web app that doesn’t need to call a lot of separate APIs and you want the simplicity of built-in login pages and Identity.
- Token auth (with maybe a IdP like IdentityServer, Auth0, etc.) if you are building an API for use by SPAs or mobile.
- Or even combine: e.g., an app that has a web UI for admin (cookie auth) and also exposes APIs for clients (JWT auth). ASP.NET Core’s flexibility with authentication schemes makes it possible to support multiple at once (just be careful to specify default schemes and such so they don’t conflict).
Best of Both: If you can, use the approach that leverages the platform security features. For example, if building a pure web app (Razor Pages), use cookie auth – it’s battle-tested and straightforward. If building a SPA, strongly consider using an existing IdP (like Azure AD B2C, Auth0, etc.) and their libraries, or the BFF pattern, instead of manually handling too much token logic.
Finally, always design with the assumption that networks are hostile. Validate everything, principle of least privilege for tokens, and compartmentalize your application into distinct components (IdP vs API vs UI) with clear trust boundaries. With ASP.NET Core’s rich identity and security ecosystem, you have the tools to implement these patterns securely – and this foundational understanding of concepts and terminology will guide you as you overhaul the official docs for authentication, authorization, and identity in ASP.NET Core.