1120 字
6 分鐘
瀏覽次數
ASP.NET Core + OpenIddict 實作筆記:Password Flow 與 Refresh Token
2025-07-25
2026-04-13

結論#

這篇整理的是 OpenIddict 在 ASP.NET Core 專案中的最小可用建置流程,包含套件安裝、資料表建立、/connect/token 實作、Refresh Token 流程,以及 Azure 上常見的憑證權限坑。

適合用在哪裡#

  • 要自己做登入授權中心的 .NET 專案
  • 需要發 access token / refresh token 的 API 系統
  • 已有 ASP.NET Core Identity,想接 OpenIddict
  • 部署到 Azure App Service,遇到憑證或權限問題的人

流程步驟#

1. 先把資料層建起來#

先安裝需要的套件,最少會用到 Microsoft.EntityFrameworkCore.SqlServerMicrosoft.EntityFrameworkCore.ToolsOpenIddict.AspNetCoreOpenIddict.EntityFrameworkCore。接著用 Migration 產生 Schema,這一步的重點不是花俏,而是先讓 OpenIddict 需要的資料表真的落地,不然後面 Token 流程再漂亮也只是空中樓閣。實作上可直接跑 EF Migration,若想先看 SQL,也能先輸出 script 再交給 DBA 檢查。

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. 在 Startup 註冊 OpenIddict 與 Identity#

這一步要做的是把 DbContext、Identity、OpenIddict Core / Server / Validation 串起來。重點有三個:一是 options.UseOpenIddict() 要掛進 DbContext;二是 Server 端要開 /connect/token 並允許 PasswordFlowRefreshTokenFlow;三是 Token 的壽命、Scope、簽章憑證要先定清楚。若你只是先在開發機測通流程,可以先用 AddDevelopmentEncryptionCertificate()AddDevelopmentSigningCertificate(),但正式環境別偷懶,這種東西混過去,日後都會回來找你 (´-ω-`)。

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. 建立測試 Client 與使用者,先把登入鏈路打通#

在開發環境先自動建立一組 Client 與管理者帳號,能大幅減少初始化時的手工操作。這段的目的很單純:確認你的資料表、Identity、OpenIddict 三者真的接起來,而不是每次測試都在懷疑到底是帳號錯、Client 沒建、還是授權配置有洞。這裡會建立 default-client,並產出一個 admin 使用者,密碼先用 PasswordHasher 轉成雜湊後存入。

#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. 實作 /connect/token,把 Password 與 Refresh Token 都接上#

/connect/token 是整個流程的核心入口,收到請求後先判斷 Grant Type,再分流到不同處理方法。Password Grant 的做法是:找到使用者、驗證密碼、建立 ClaimsIdentity、加上角色與必要 Scope,最後回傳 SignIn(...) 讓 OpenIddict 幫你發 Token。Refresh Token 則簡單很多,直接從目前驗證內容取出 ClaimsPrincipal 再重新簽發即可。白話講,前者是「靠帳密換票」,後者是「拿舊票換新票」。

[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);
}

補充#

  • Azure App Service 若遇到 WindowsCryptographicException: Access is denied.,可先補上 WEBSITE_LOAD_USER_PROFILE=1,這通常是在載入開發憑證時踩到的坑。
  • UseReferenceAccessTokens() / UseReferenceRefreshTokens() 代表實際 Token 內容存在資料庫,外部拿到的是參照值,方便控管,但也更依賴資料庫穩定性。
  • Password Flow 雖然實作快,但通常只適合受控系統或內部整合;若是對外產品,授權流程要再多想一步,別只求先能跑。

指令 / 範例整理#

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

收尾#

OpenIddict 真的不難,難的是一開始把資料表、授權設定、Token 流程一次接對;先求最小可用,再慢慢補安全與正式環境配置,這才是正常活路。

參考來源#

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