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.
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.
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.
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
OpenIddict stores clients, scopes, authorizations, and tokens through Entity Framework Core:
C# — Data/ApplicationDbContext.cspublic 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();
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):
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();
AddDevelopmentEncryptionCertificate()
and AddDevelopmentSigningCertificate() generate ephemeral, self-signed certificates
suitable only for local development. In production, register real X.509 certificates via
AddSigningCertificate(...)/AddEncryptionCertificate(...).
Clients (and scopes) are stored records, typically seeded on startup or managed through an
admin UI. Use IOpenIddictApplicationManager to register one:
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"
}
});
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.");
}
}
RequireProofKeyForCodeExchange() in the
server configuration enforces it for every authorization code request.
For service-to-service (machine-to-machine) calls with no user involved, extend the
Exchange action to handle the client credentials grant:
[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.");
}
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);
}
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.
curl https://localhost:5001/.well-known/openid-configuration
A separate resource API (the thing your access tokens actually protect) only needs the validation package — not the full server:
bashdotnet 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);
Also Read: JWT Authentication in ASP.NET Core with SQL Server
AI-generated fashion design is revolutionizing the industry, pushing the boundaries of creativity and innovation in style.
Browse most trending Journalist jobs in South Africa. Apply today
Browse most trending Photographer jobs in South Africa. Apply today
Browse most trending Fashion jobs in United Kingdom. Apply today
AI-Generated Fashion Design
SEP 10, 2026
Streetwear Luxury Collaborations
SEP 10, 2026 5,617 VIEWS
Get the latest creative news from FooBar about art, design and business.
Subscribe