Conclusion
This post summarizes the minimum viable build process for OpenIddict in an ASP.NET Core project, including package installation, database table creation, /connect/token implementation, Refresh Token flow, and common certificate permission pitfalls on Azure.
Use Cases
- .NET projects that need to build their own login authorization center
- API systems that need to issue
access token/refresh token - Existing ASP.NET Core Identity projects that want to integrate OpenIddict
- Anyone deploying to Azure App Service and encountering certificate or permission issues
Workflow Steps
1. Set up the Data Layer First
First, install the necessary packages. At a minimum, you’ll need Microsoft.EntityFrameworkCore.SqlServer, Microsoft.EntityFrameworkCore.Tools, OpenIddict.AspNetCore, and OpenIddict.EntityFrameworkCore. Then, use Migration to generate the schema. The key point here isn’t fancy features, but ensuring that the database tables required by OpenIddict are actually created. Otherwise, no matter how elegant your Token flow is, it will just be a castle in the air. In practice, you can directly run EF Migration, or if you want to see the SQL first, you can output a script and hand it over to your DBA for review.
# Visual Studio Package Manager ConsoleAdd-Migration InitialCreateUpdate-Database
# DotNet CLIdotnet ef migrations add InitialCreatedotnet ef database update
# 產生 SQL 腳本dotnet ef migrations script InitialCreate --context DefaultDbContext2. Register OpenIddict and Identity in Startup
This step involves connecting DbContext, Identity, and OpenIddict Core / Server / Validation. There are three key points: first, options.UseOpenIddict() must be hooked into DbContext; second, the Server-side needs to enable /connect/token and allow PasswordFlow and RefreshTokenFlow; third, the Token’s lifetime, Scope, and signing certificate must be clearly defined. If you’re just testing the flow on a development machine, you can use AddDevelopmentEncryptionCertificate() and AddDevelopmentSigningCertificate(). However, don’t be lazy in a production environment; if you gloss over these things, they will come back to haunt you later (´-ω-`).
services.AddDbContext<DefaultDbContext>(options =>{ options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")); options.UseOpenIddict(); // 讓 OpenIddict 使用 EF Core});
services.AddOpenIddict() .AddCore(options => { options.UseEntityFrameworkCore() .UseDbContext<DefaultDbContext>(); }) .AddServer(options => { options.SetTokenEndpointUris("/connect/token"); options.SetUserinfoEndpointUris("/connect/userinfo");
options.AllowPasswordFlow(); options.AllowRefreshTokenFlow(); options.AllowCustomFlow("custom_flow_name");
options.UseReferenceAccessTokens(); options.UseReferenceRefreshTokens();
options.RegisterScopes( OpenIddictConstants.Permissions.Scopes.Email, OpenIddictConstants.Permissions.Scopes.Profile, OpenIddictConstants.Permissions.Scopes.Roles);
options.SetAccessTokenLifetime(TimeSpan.FromMinutes(30)); options.SetRefreshTokenLifetime(TimeSpan.FromDays(7));
options.AddDevelopmentEncryptionCertificate() .AddDevelopmentSigningCertificate();
options.UseAspNetCore() .EnableTokenEndpointPassthrough(); }) .AddValidation(options => { options.UseLocalServer(); options.UseAspNetCore(); });3. Create a Test Client and User to Establish the Login Path
Automatically creating a client and an administrator account in the development environment can significantly reduce manual operations during initialization. The purpose of this section is simple: to confirm that your database tables, Identity, and OpenIddict are truly connected, rather than constantly wondering during testing if it’s an incorrect account, a missing client, or a flaw in the authorization configuration. Here, a default-client will be created, and an admin user will be generated, with the password first hashed using PasswordHasher before being stored.
#if DEBUGCreateCientAndUser(app);#endif
private static void CreateCientAndUser(IApplicationBuilder app){ using var scope = app.ApplicationServices .GetRequiredService<IServiceScopeFactory>() .CreateScope();
var context = scope.ServiceProvider.GetRequiredService<DefaultDbContext>(); context.Database.EnsureCreated();
var manager = scope.ServiceProvider.GetRequiredService<IOpenIddictApplicationManager>(); var existingClientApp = manager.FindByClientIdAsync("default-client").GetAwaiter().GetResult();
if (existingClientApp == null) { manager.CreateAsync(new OpenIddictApplicationDescriptor { ClientId = "default-client", ClientSecret = "your-client-secret", DisplayName = "Default client application", Permissions = { OpenIddictConstants.Permissions.Endpoints.Token, OpenIddictConstants.Permissions.GrantTypes.Password, OpenIddictConstants.Permissions.GrantTypes.RefreshToken } }).GetAwaiter().GetResult(); }}4. Implement /connect/token, Connecting Both Password and Refresh Tokens
/connect/token is the core entry point for the entire flow. Upon receiving a request, it first determines the Grant Type and then dispatches to different handling methods. The approach for Password Grant is: find the user, validate the password, create a ClaimsIdentity, add roles and necessary Scopes, and finally return SignIn(...) to let OpenIddict issue the token for you. Refresh Token is much simpler: just retrieve the ClaimsPrincipal from the current authentication context and re-issue the token. In plain terms, the former is “exchanging credentials for a ticket,” while the latter is “exchanging an old ticket for a new one.”
[HttpPost("~/connect/token")][AllowAnonymous]public async Task<IActionResult> Exchange(){ var oidcRequest = HttpContext.GetOpenIddictServerRequest();
if (oidcRequest.IsPasswordGrantType()) return await TokensForPasswordGrantType(oidcRequest);
if (oidcRequest.IsRefreshTokenGrantType()) return await TokenForRefreshGrantType();
return BadRequest(new OpenIddictResponse { Error = OpenIddictConstants.Errors.UnsupportedGrantType });}
private async Task<IActionResult> TokensForPasswordGrantType(OpenIddictRequest request){ var user = await _userManager.FindByNameAsync(request.Username); if (user == null) return Unauthorized();
var signInResult = await _signInManager.CheckPasswordSignInAsync(user, request.Password, false); if (!signInResult.Succeeded) return Unauthorized();
var identity = new ClaimsIdentity( TokenValidationParameters.DefaultAuthenticationType, OpenIddictConstants.Claims.Name, OpenIddictConstants.Claims.Role);
identity.AddClaim(OpenIddictConstants.Claims.Subject, user.Id.ToString(), OpenIddictConstants.Destinations.AccessToken); identity.AddClaim(OpenIddictConstants.Claims.Username, user.Username, OpenIddictConstants.Destinations.AccessToken);
foreach (var userRole in user.UserRoles) { identity.AddClaim(OpenIddictConstants.Claims.Role, userRole.Role.NormalizedName, OpenIddictConstants.Destinations.AccessToken); }
var principal = new ClaimsPrincipal(identity); principal.SetScopes(new[] { OpenIddictConstants.Scopes.Roles, OpenIddictConstants.Scopes.OfflineAccess, OpenIddictConstants.Scopes.Email, OpenIddictConstants.Scopes.Profile });
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);}
private async Task<IActionResult> TokenForRefreshGrantType(){ var principal = (await HttpContext.AuthenticateAsync( OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);}Additional Notes
- If you encounter
WindowsCryptographicException: Access is denied.on Azure App Service, you can first addWEBSITE_LOAD_USER_PROFILE=1. This is a common pitfall when loading development certificates. UseReferenceAccessTokens()/UseReferenceRefreshTokens()means the actual token content is stored in the database, and external parties receive a reference value. This facilitates management but also increases reliance on database stability.- While
Password Flowis quick to implement, it’s generally only suitable for controlled systems or internal integrations. For external-facing products, the authorization flow requires more thought than just getting it to run.
Commands / Examples Summary
Click to expand
# 安裝後執行 MigrationAdd-Migration InitialCreateUpdate-Database
dotnet ef migrations add InitialCreatedotnet ef database updatedotnet ef migrations script InitialCreate --context DefaultDbContextPOST /connect/tokenContent-Type: application/x-www-form-urlencoded
grant_type=passwordclient_id=default-clientclient_secret=your-client-secretusername=adminpassword=your-passwordscope=offline_access profile email rolesPOST /connect/tokenContent-Type: application/x-www-form-urlencoded
grant_type=refresh_tokenclient_id=default-clientclient_secret=your-client-secretrefresh_token=your-refresh-tokenWrapping Up
OpenIddict isn’t really difficult; the challenge lies in correctly connecting the database tables, authorization settings, and token flow all at once from the beginning. Aim for minimum viability first, then gradually add security and production environment configurations – that’s the normal path forward.
References
-
OpenIddict Official Documentation https://github.com/openiddict/openiddict-core
-
Setting up Token Authentication with OpenIddict in .NET 5 https://nwb.one/blog/openid-connect-dotnet-5
-
Building an Authentication Server with OpenIddict to Protect Your Web API with Client Credentials Grant https://wezmag.github.io/posts/protect-api-with-client-credentials-grant-using-openiddict-part-1/
-
Setting up an Authorization Server with OpenIddict - Part VI - Refresh Tokens https://dev.to/robinvanderknaap/setting-up-an-authorization-server-with-openiddict-part-vi-refresh-tokens-5669
-
Detailed OpenIddict Flow https://www.cnblogs.com/liyouming/p/15772403.html