• Home
  • /
  • Articles
  • /
  • Software Development
  • /
  • Build an OpenID Connect & OAuth 2.0 Server in ASP.NET Core
HEADLINES

Build a Standards-Compliant OpenID Connect & OAuth 2.0 Server in ASP.NET Core

A practical guide to building your own OIDC/OAuth 2.0 authorization server with ASP.NET Core and OpenIddict — covering flows, endpoints, token issuance, scopes, and client registration, with runnable code.


  • Techm Studios
  • BY TECHM STUDIOS
  • |
  • Sept 10, 2026
  • |
  • UPDATED: Sept 10, 2026
  • |
  • 0 COMMENTS
Build an OpenID Connect & OAuth 2.0 Server in ASP.NET Core
© Sustainsys / Images

ASP.NET Core · Identity · OAuth 2.0 / OIDC
.NET 8 / .NET 9+ OpenIddict 5.x/6.x ASP.NET Core Identity

01OAuth 2.0 vs. OpenID Connect, and Why Build Your Own Server

OAuth 2.0 (RFC 6749) is an authorization framework: it lets a user grant a client application limited access to their resources without sharing credentials. OpenID Connect (OIDC) is a thin identity layer built on top of OAuth 2.0 that adds authentication — a standardized id_token (a JWT) telling the client who the user is, plus discovery and UserInfo endpoints so clients don't need custom integration per provider.

Building your own authorization server makes sense when you need full control over user accounts, token contents, custom grant types, or multi-tenant client management — instead of delegating identity to an external provider like Auth0, Okta, or Microsoft Entra ID.

Key idea: OAuth 2.0 answers "what is this app allowed to do?" while OpenID Connect answers "who is this user?" A modern authentication server implements both together.

02Choosing a Server Library: OpenIddict

Two well-known options exist in the .NET ecosystem: Duende IdentityServer (commercially licensed for most production use) and OpenIddict, a free, open-source OAuth 2.0/OpenID Connect stack. OpenIddict integrates natively with ASP.NET Core Identity and Entity Framework Core, and supports the authorization code, implicit, hybrid, client credentials, refresh token, and device authorization flows, plus custom grant types.

Unlike a turnkey product, OpenIddict is a framework: you write your own authorization, token, and consent endpoint actions (typically a single controller), which is what makes it flexible enough to be fully standards-compliant while remaining self-hosted and free.

03Project Setup

bash
dotnet new web -o AuthServer
cd AuthServer

dotnet add package OpenIddict.AspNetCore
dotnet add package OpenIddict.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore

04Wiring Up ASP.NET Core Identity + EF Core Stores

OpenIddict stores clients, scopes, authorizations, and tokens through Entity Framework Core:

C# — Data/ApplicationDbContext.cs
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        // Register the OpenIddict entity sets (applications, authorizations,
        // scopes, tokens) into this context.
        builder.UseOpenIddict();
    }
}
C# — Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default"));

    // Register the entity sets needed by OpenIddict.
    options.UseOpenIddict();
});

builder.Services
    .AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders();

05Configuring OpenIddict Core, Server, and Validation

OpenIddict is configured through three building blocks: AddCore (storage), AddServer (the authorization/token/UserInfo endpoints and supported flows), and AddValidation (verifying incoming access tokens, used by resource APIs):

C# — Program.cs
builder.Services.AddOpenIddict()

    // Register the OpenIddict core components.
    .AddCore(options =>
    {
        options.UseEntityFrameworkCore()
               .UseDbContext<ApplicationDbContext>();
    })

    // Register the OpenIddict server components.
    .AddServer(options =>
    {
        // Enable the standard OIDC/OAuth2 endpoints.
        options.SetAuthorizationEndpointUris("/connect/authorize")
               .SetTokenEndpointUris("/connect/token")
               .SetUserInfoEndpointUris("/connect/userinfo")
               .SetEndSessionEndpointUris("/connect/logout");

        // Enable the flows this server supports.
        options.AllowAuthorizationCodeFlow()
               .RequireProofKeyForCodeExchange() // PKCE, mandatory for public clients
               .AllowClientCredentialsFlow()
               .AllowRefreshTokenFlow();

        // Register scopes exposed to clients.
        options.RegisterScopes(
            OpenIddictConstants.Scopes.Email,
            OpenIddictConstants.Scopes.Profile,
            OpenIddictConstants.Scopes.Roles,
            "api1");

        // Encryption and signing certificates.
        // In production, replace with X.509 certificates from a certificate store.
        options.AddDevelopmentEncryptionCertificate()
               .AddDevelopmentSigningCertificate();

        // Register ASP.NET Core host and enable the pass-through mode so custom
        // controller actions can handle the authorization/token requests.
        options.UseAspNetCore()
               .EnableAuthorizationEndpointPassthrough()
               .EnableTokenEndpointPassthrough()
               .EnableUserInfoEndpointPassthrough()
               .EnableStatusCodePagesIntegration();
    })

    // Register the OpenIddict validation components (used by the auth server
    // itself and by any resource API that accepts its tokens).
    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Development vs. production: AddDevelopmentEncryptionCertificate() and AddDevelopmentSigningCertificate() generate ephemeral, self-signed certificates suitable only for local development. In production, register real X.509 certificates via AddSigningCertificate(...)/AddEncryptionCertificate(...).

06Registering Client Applications and Scopes

Clients (and scopes) are stored records, typically seeded on startup or managed through an admin UI. Use IOpenIddictApplicationManager to register one:

C#
using var scope = app.Services.CreateScope();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>();

if (await manager.FindByClientIdAsync("web-client") is null)
{
    await manager.CreateAsync(new OpenIddictApplicationDescriptor
    {
        ClientId = "web-client",
        ClientType = OpenIddictConstants.ClientTypes.Public,
        RedirectUris = { new Uri("https://localhost:5001/signin-oidc") },
        PostLogoutRedirectUris = { new Uri("https://localhost:5001/signout-callback-oidc") },
        Permissions =
        {
            OpenIddictConstants.Permissions.Endpoints.Authorization,
            OpenIddictConstants.Permissions.Endpoints.Token,
            OpenIddictConstants.Permissions.Endpoints.EndSession,
            OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
            OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
            OpenIddictConstants.Permissions.ResponseTypes.Code,
            OpenIddictConstants.Permissions.Scopes.Email,
            OpenIddictConstants.Permissions.Scopes.Profile,
            OpenIddictConstants.Permissions.Prefixes.Scope + "api1"
        },
        Requirements = { OpenIddictConstants.Requirements.Features.ProofKeyForCodeExchange }
    });
}

A confidential machine-to-machine client for the client credentials flow looks similar, but keeps a secret:

C#
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
    ClientId = "service-client",
    ClientSecret = "super-secret-value", // hashed automatically by OpenIddict
    ClientType = OpenIddictConstants.ClientTypes.Confidential,
    Permissions =
    {
        OpenIddictConstants.Permissions.Endpoints.Token,
        OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
        OpenIddictConstants.Permissions.Prefixes.Scope + "api1"
    }
});

07Implementing the Authorization Code Flow (with PKCE)

The authorization code flow is the recommended flow for browser and mobile apps that authenticate a real user. You supply a controller action for the authorization endpoint that signs the user in and issues a code, and one for the token endpoint that exchanges the code (or a refresh token) for an access/ID token:

C# — Controllers/AuthorizationController.cs
[ApiController]
public class AuthorizationController : ControllerBase
{
    [HttpGet("~/connect/authorize")]
    [HttpPost("~/connect/authorize")]
    public async Task<IActionResult> Authorize()
    {
        var request = HttpContext.GetOpenIddictServerRequest()!;

        var result = await HttpContext.AuthenticateAsync(
            IdentityConstants.ApplicationScheme);

        if (!result.Succeeded)
        {
            // Not signed in yet: redirect to the login page, then come back here.
            return Challenge(
                authenticationSchemes: IdentityConstants.ApplicationScheme,
                properties: new AuthenticationProperties
                {
                    RedirectUri = Request.PathBase + Request.Path + Request.QueryString
                });
        }

        var identity = new ClaimsIdentity(
            authenticationType: TokenValidationParameters.DefaultAuthenticationType,
            nameType: OpenIddictConstants.Claims.Name,
            roleType: OpenIddictConstants.Claims.Role);

        identity.SetClaim(OpenIddictConstants.Claims.Subject, result.Principal!.FindFirstValue(ClaimTypes.NameIdentifier));
        identity.SetClaim(OpenIddictConstants.Claims.Email, result.Principal.FindFirstValue(ClaimTypes.Email));
        identity.SetClaim(OpenIddictConstants.Claims.Name, result.Principal.Identity!.Name);
        identity.SetScopes(request.GetScopes());

        // Ensure each claim only goes into the tokens/destinations that need it.
        foreach (var claim in identity.Claims)
        {
            claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken, OpenIddictConstants.Destinations.IdentityToken);
        }

        return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

    [HttpPost("~/connect/token")]
    public async Task<IActionResult> Exchange()
    {
        var request = HttpContext.GetOpenIddictServerRequest()!;

        if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
        {
            var result = await HttpContext.AuthenticateAsync(
                OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);

            return SignIn(result.Principal!, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
        }

        throw new NotImplementedException("The specified grant type is not implemented.");
    }
}
PKCE (Proof Key for Code Exchange) protects public clients (SPAs, mobile apps) from authorization-code interception. RequireProofKeyForCodeExchange() in the server configuration enforces it for every authorization code request.

08Implementing the Client Credentials Flow

For service-to-service (machine-to-machine) calls with no user involved, extend the Exchange action to handle the client credentials grant:

C#
[HttpPost("~/connect/token")]
public async Task<IActionResult> Exchange()
{
    var request = HttpContext.GetOpenIddictServerRequest()!;

    if (request.IsClientCredentialsGrantType())
    {
        var identity = new ClaimsIdentity(
            TokenValidationParameters.DefaultAuthenticationType,
            OpenIddictConstants.Claims.Name,
            OpenIddictConstants.Claims.Role);

        identity.SetClaim(OpenIddictConstants.Claims.Subject, request.ClientId);
        identity.SetScopes(request.GetScopes());

        foreach (var claim in identity.Claims)
        {
            claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
        }

        return SignIn(new ClaimsPrincipal(identity), OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
    }

    // ... authorization code / refresh token handling from above ...
    throw new NotImplementedException("The specified grant type is not implemented.");
}

09Exposing the UserInfo Endpoint

OpenID Connect clients call the UserInfo endpoint with an access token to retrieve claims about the authenticated user:

C#
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
[HttpGet("~/connect/userinfo")]
public async Task<IActionResult> UserInfo()
{
    var claims = new Dictionary<string, object>
    {
        [OpenIddictConstants.Claims.Subject] = User.GetClaim(OpenIddictConstants.Claims.Subject)!
    };

    if (User.HasScope(OpenIddictConstants.Scopes.Email))
        claims[OpenIddictConstants.Claims.Email] = User.GetClaim(OpenIddictConstants.Claims.Email)!;

    if (User.HasScope(OpenIddictConstants.Scopes.Profile))
        claims[OpenIddictConstants.Claims.Name] = User.GetClaim(OpenIddictConstants.Claims.Name)!;

    return Ok(claims);
}

10Discovery and JWKS Endpoints

OpenIddict automatically exposes the standard OIDC discovery document at /.well-known/openid-configuration and the JSON Web Key Set at /.well-known/jwks, generated from your server configuration and signing certificates — no extra code required. Clients use these to auto-discover endpoints and verify token signatures.

bash
curl https://localhost:5001/.well-known/openid-configuration

11Validating Tokens in a Resource API

A separate resource API (the thing your access tokens actually protect) only needs the validation package — not the full server:

bash
dotnet add package OpenIddict.Validation.AspNetCore
dotnet add package OpenIddict.Validation.SystemNetHttp
C# — Program.cs (resource API)
builder.Services.AddOpenIddict()
    .AddValidation(options =>
    {
        // Point at the authorization server's discovery endpoint.
        options.SetIssuer("https://localhost:5001/");
        options.UseSystemNetHttp();
        options.UseAspNetCore();
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("ApiScope", policy =>
        policy.RequireClaim(OpenIddictConstants.Claims.Scope, "api1"));
});
C#
[Authorize(Policy = "ApiScope")]
[HttpGet("/api/orders")]
public IActionResult GetOrders() => Ok(_orders);

12Security & Standards-Compliance Checklist

  • Always require PKCE for authorization code flow, even for confidential clients — it's now the recommended default across the industry.
  • Never use the implicit flow or the resource owner password flow for new applications; both are discouraged by current OAuth 2.0 Security Best Current Practice guidance in favor of the authorization code flow with PKCE.
  • Use the Backend-for-Frontend (BFF) pattern for browser-based apps with no independent backend, rather than running OIDC directly in client-side JavaScript.
  • Rotate signing keys and use real X.509 certificates in production instead of the ephemeral development certificates.
  • Scope access tokens narrowly; don't put every claim into every token — use claim destinations to control what lands in access vs. identity tokens.
  • Short-lived access tokens + refresh tokens limit the blast radius of a leaked access token.
  • Enforce HTTPS everywhere and validate redirect URIs exactly (no wildcard matching) to prevent open-redirect and code-interception attacks.
Written for ASP.NET Core (.NET 8 / .NET 9+) using OpenIddict. Code samples follow current OpenIddict and Microsoft Learn documentation conventions and are intended as a practical reference, not a verbatim reproduction of any single source. Always review the latest OpenIddict documentation for API changes before deploying to production.

Also Read: JWT Authentication in ASP.NET Core with SQL Server

SPONSORED ADS

COMMENTS
LEAVE A REPLY
SPONSORED ADS
POPULAR
AI-Generated Fashion Design

AI-generated fashion design is revolutionizing the industry, pushing the boundaries of creativity and innovation in style.


SPONSORED JOBS

TECHM JOBS
POPULAR
Journalist Jobs, South Africa

Browse most trending Journalist jobs in South Africa. Apply today

Photographer Jobs, South Africa

Browse most trending Photographer jobs in South Africa. Apply today

Graphical Designer Jobs, United Kingdom

Browse most trending Fashion jobs in United Kingdom. Apply today


TECHM PRODUCTS


TOP POSTS
STAY IN TOUCH
SPONSORED ADS
Subscribe to Updates

Get the latest creative news from FooBar about art, design and business.

Subscribe