Single Sign-On in .NET: The Complete Implementation Guide
Single Sign-On (SSO) lets users authenticate once and access multiple applications without re-entering credentials. In ASP.NET Core, you implement SSO using OpenID Connect (OIDC), the protocol built on top of OAuth 2.0. This guide walks through working code, protocol mechanics, and production considerations.
What Is Single Sign-On?
SSO is an authentication pattern where one login session grants access to multiple independent applications. The user authenticates with a central identity provider (IdP), which issues tokens that each application accepts as proof of identity.
SSO solves three problems:
- User friction: one password, one login, access to everything without having to re-login all the time
- Security centralization: enforce MFA, password policies, and session management in one place
- Operational simplicity: onboard and offboard users from a single directory
How SSO Works in ASP.NET Core
ASP.NET Core implements SSO through its authentication middleware. The flow:
sequenceDiagram
participant User
participant AppA as App A
participant IdP
participant AppB as App B
User->>AppA: Visit /dashboard
AppA-->>User: 302 Redirect to IdP
User->>IdP: Login (credentials + MFA)
IdP-->>IdP: Create session cookie
IdP-->>User: 302 Redirect with auth code
User->>AppA: POST /signin-oidc (auth code)
AppA->>IdP: Exchange code for tokens
IdP-->>AppA: ID token + access token
AppA-->>User: Set cookie, show dashboard
Note over User,IdP: Later, user visits App B
User->>AppB: Visit /profile
AppB-->>User: 302 Redirect to IdP
User->>IdP: (session cookie sent)
IdP-->>User: 302 Redirect with auth code (no login prompt)
User->>AppB: POST /signin-oidc (auth code)
AppB->>IdP: Exchange code for tokens
IdP-->>AppB: ID token + access token
AppB-->>User: Set cookie, show profile- User visits Application A (not authenticated)
- Application A redirects to the identity provider
- User authenticates at the IdP (enters credentials, completes MFA)
- IdP redirects back to Application A with an authorization code
- Application A exchanges the code for tokens (ID token + access token)
- User visits Application B (not authenticated)
- Application B redirects to the same IdP
- IdP recognizes the existing session and redirects back immediately, no login prompt
- Application B receives tokens. SSO complete.
The IdP maintains a session cookie. When Application B redirects there, the IdP already knows who the user is and skips the login form. That cookie is the mechanism behind SSO.
What Protocol Should You Use?
| Protocol | Use Case | Recommendation |
|---|---|---|
| OpenID Connect | Web apps, SPAs, mobile apps, APIs | Default choice for new projects |
| SAML 2.0 | Enterprise federation with legacy IdPs | Use when a partner requires it |
| WS-Federation | Legacy .NET Framework apps, ADFS | Migrate to OIDC when possible |
OpenID Connect is the right choice for new .NET applications. It supports all application types, has first-class ASP.NET Core middleware, and every major IdP supports it.
Implementing SSO with OpenID Connect
Step 1: Configure the Identity Provider
Your IdP needs a client registration for each application participating in SSO. Using Duende IdentityServer as an example:
Csharp
new Client
{
ClientId = "webapp-a",
ClientName = "Web Application A",
AllowedGrantTypes = GrantTypes.Code,
RequirePkce = true,
ClientSecrets = { new Secret("secret-a".Sha256()) },
RedirectUris = { "https://app-a.example.com/signin-oidc" },
PostLogoutRedirectUris = { "https://app-a.example.com/signout-callback-oidc" },
AllowedScopes = { "openid", "profile", "email" }
}Register a second client for Application B with the same scopes. Both applications point to the same IdP authority, which enables SSO between them.
Step 2: Configure ASP.NET Core Authentication
Each application configures the OpenID Connect middleware to point at the same authority:
Csharp
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://idp.example.com";
options.ClientId = "webapp-a";
options.ClientSecret = "secret-a";
options.ResponseType = "code";
options.UsePkce = true;
options.Scope.Add("profile");
options.Scope.Add("email");
options.SaveTokens = true;
options.MapInboundClaims = false;
}); The Authority property matters most here. Every application that shares the same authority participates in the same SSO session.
Step 3: Protect Routes
Apply authorization to your endpoints:
Csharp
app.MapGet("/dashboard", (ClaimsPrincipal user) =>
{
var name = user.FindFirst("name")?.Value;
return Results.Ok($"Welcome, {name}");
}).RequireAuthorization();Or for controllers:
Csharp
[Authorize]
public class DashboardController : Controller
{
public IActionResult Index()
{
return View();
}
}When an unauthenticated user hits a protected route, the middleware redirects to the IdP. If they already have a session there from another app, they return with tokens and no login prompt. That is SSO.
SSO Session Management
Session Lifetime
The IdP session lifetime determines how long SSO works without re-authentication. Configure it on your identity provider:
Csharp
builder.Services.AddIdentityServer(options =>
{
options.Authentication.CookieLifetime = TimeSpan.FromHours(8);
options.Authentication.CookieSlidingExpiration = true;
});Each application also has its own cookie lifetime. The application session and IdP session are independent. A user can be signed out of one application while their IdP session stays active.
Single Logout
When a user logs out, you likely want to end sessions across all applications. OpenID Connect supports this through front-channel logout and back-channel logout:
Csharp
// In your application's logout endpoint
public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
await HttpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
return new SignOutResult();
}The OIDC middleware redirects to the IdP's end session endpoint, which notifies other applications to clear their sessions.
SSO with Different Identity Providers
The middleware configuration is nearly identical across IdPs. Swap the authority URL and credentials, and everything works.
Microsoft Entra ID (Azure AD)
Csharp
builder.Services.AddAuthentication()
.AddOpenIdConnect("EntraID", options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.ClientId = "your-client-id";
options.ClientSecret = "your-client-secret";
options.ResponseType = "code";
options.UsePkce = true;
options.Scope.Add("profile");
});Okta
Csharp
builder.Services.AddAuthentication()
.AddOpenIdConnect("Okta", options =>
{
options.Authority = "https://your-org.okta.com";
options.ClientId = "your-client-id";
options.ClientSecret = "your-client-secret";
options.ResponseType = "code";
options.UsePkce = true;
});Duende IdentityServer (Self-Hosted)
Csharp
builder.Services.AddAuthentication()
.AddOpenIdConnect("IdentityServer", options =>
{
options.Authority = "https://idp.yourcompany.com";
options.ClientId = "your-client-id";
options.ClientSecret = "your-client-secret";
options.ResponseType = "code";
options.UsePkce = true;
});That sameness is the point of standards-based protocols. Your application code does not change when you switch providers.
SSO for APIs: Token-Based Authentication
APIs do not participate in cookie-based SSO directly. Your web applications obtain access tokens during the SSO flow and pass them to APIs:
Csharp
// API project: validate tokens from the IdP
builder.Services.AddAuthentication()
.AddJwtBearer(options =>
{
options.Authority = "https://idp.example.com";
options.Audience = "api";
options.MapInboundClaims = false;
});The web application includes the access token in API calls:
Csharp
// In your web application
var token = await HttpContext.GetTokenAsync("access_token");
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);For production applications, use Duende.AccessTokenManagement to handle token refresh and caching.
SSO for SPAs: The BFF Pattern
Single-page applications cannot securely store client secrets or refresh tokens in the browser. The Backend for Frontend (BFF) pattern keeps token management server-side:
Csharp
builder.Services.AddBff();
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(options =>
{
options.Authority = "https://idp.example.com";
options.ClientId = "spa-bff";
options.ClientSecret = "spa-secret";
options.ResponseType = "code";
options.UsePkce = true;
});
app.UseBff();
app.MapBffManagementEndpoints();
// Proxy API calls through the BFF
app.MapRemoteBffApiEndpoint("/api", "https://api.example.com")
.RequireAccessToken();The SPA communicates with its BFF using cookies (same-site, HttpOnly). The BFF handles OIDC, stores tokens server-side, and forwards API calls with the access token attached. SSO works because the BFF redirects to the same IdP as your other applications.
Production Checklist
Before deploying SSO to production:
- HTTPS everywhere - tokens travel in redirects; HTTP exposes them
- PKCE enabled - prevents authorization code interception
- Short token lifetimes - access tokens under 60 minutes, use refresh tokens
- Validate issuer and audience - reject tokens from unexpected sources
- Secure cookie settings -
SameSite=Lax,Secure=true,HttpOnly=true - Session monitoring - track active sessions, support admin revocation
- Clock synchronization - token validation fails if server clocks drift
- Logout implementation - end sessions across all participating applications
Common SSO Problems and Fixes
"Infinite redirect loop"
The application redirects to the IdP, which redirects back, but the cookie is not persisted. Common causes: missing SameSite cookie configuration, HTTP instead of HTTPS (the browser rejects Secure cookies), or a cookie domain mismatch in load-balanced environments.
"Correlation failed" error
The OIDC middleware stores a nonce in a cookie before redirecting. If that cookie is gone when the user returns, correlation fails. This happens when the user takes too long to authenticate, a load balancer routes the callback to a different server (fix with distributed data protection), or browser cookie policies block the correlation cookie.
"SSO not working between applications"
Both applications must point to the same authority URL, exact match, including trailing slash. The IdP session cookie must be accessible to both redirect requests, which means both apps must redirect to the same IdP domain.
SAML 2.0: When You Need It
Some enterprise partners require SAML. Duende IdentityServer can act as a SAML identity provider, so your .NET applications use OIDC internally while federating with SAML partners externally. Modern protocols for your apps, backward compatibility for partners.
Choosing an Identity Provider
| Consideration | Self-Hosted (Duende IdentityServer) | Cloud (Entra ID, Okta) |
|---|---|---|
| Control | Full control over data, flows, UI | Vendor-managed |
| Customization | Unlimited, it is your code | Limited to provider options |
| Compliance | Data stays in your infrastructure | Depends on vendor certifications |
| Operations | You manage uptime, updates | Vendor manages infrastructure |
| Cost model | License-based, predictable | Per-user/per-auth, can scale up fast |
Many organizations use both: Duende IdentityServer as the central IdP with federation to external providers (Entra ID, Google Workspace, Okta) for specific user populations.
Next Steps
- OpenID Connect documentation - configure login flows
- Duende IdentityServer quickstarts - get running in minutes
- BFF Security Framework - secure your SPAs