932 字
5 分鐘
瀏覽次數
ASP.NET Core + OpenIddict Implementation Notes: Password Flow and Refresh Token
2025-07-25
2026-04-13

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.

Terminal window
# Visual Studio Package Manager Console
Add-Migration InitialCreate
Update-Database
# DotNet CLI
dotnet ef migrations add InitialCreate
dotnet ef database update
# 產生 SQL 腳本
dotnet ef migrations script InitialCreate --context DefaultDbContext

2. 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 DEBUG
CreateCientAndUser(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 add WEBSITE_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 Flow is 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
Terminal window
# 安裝後執行 Migration
Add-Migration InitialCreate
Update-Database
dotnet ef migrations add InitialCreate
dotnet ef database update
dotnet ef migrations script InitialCreate --context DefaultDbContext
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=password
client_id=default-client
client_secret=your-client-secret
username=admin
password=your-password
scope=offline_access profile email roles
POST /connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
client_id=default-client
client_secret=your-client-secret
refresh_token=your-refresh-token

Wrapping 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#

ASP.NET Core + OpenIddict Implementation Notes: Password Flow and Refresh Token
https://joyceowo.github.io/posts/en/23ba78ea09fa81dcb655f1a2f1693b47/
作者
JoyceOwO
發佈於
2025-07-25
許可協議
CC BY-NC-SA 4.0