結論
這篇整理的是 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.SqlServer、Microsoft.EntityFrameworkCore.Tools、OpenIddict.AspNetCore、OpenIddict.EntityFrameworkCore。接著用 Migration 產生 Schema,這一步的重點不是花俏,而是先讓 OpenIddict 需要的資料表真的落地,不然後面 Token 流程再漂亮也只是空中樓閣。實作上可直接跑 EF Migration,若想先看 SQL,也能先輸出 script 再交給 DBA 檢查。
# 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. 在 Startup 註冊 OpenIddict 與 Identity
這一步要做的是把 DbContext、Identity、OpenIddict Core / Server / Validation 串起來。重點有三個:一是 options.UseOpenIddict() 要掛進 DbContext;二是 Server 端要開 /connect/token 並允許 PasswordFlow、RefreshTokenFlow;三是 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 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. 實作 /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
# 安裝後執行 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-token收尾
OpenIddict 真的不難,難的是一開始把資料表、授權設定、Token 流程一次接對;先求最小可用,再慢慢補安全與正式環境配置,這才是正常活路。
參考來源
-
OpenIddict官方文件 https://github.com/openiddict/openiddict-core
-
在 .NET 5 中使用 OpenIddict 設置令牌身份驗證 https://nwb.one/blog/openid-connect-dotnet-5
-
使用 OpenIddict 建立一個支援 Client Credentials Grant 的 Authentication Server 來保護你的 Web API https://wezmag.github.io/posts/protect-api-with-client-credentials-grant-using-openiddict-part-1/
-
使用 OpenIddict 設置授權服務器 - 第 VI 部分 - 刷新令牌 https://dev.to/robinvanderknaap/setting-up-an-authorization-server-with-openiddict-part-vi-refresh-tokens-5669
-
OpenIddict詳細流程 https://www.cnblogs.com/liyouming/p/15772403.html