基於 DotNetOpenAuth 5.0 實作 OAuth 2.0 Authorization Server 與 Client,示範 Authorization Code Flow 核心流程:授權碼換 Access Token。

本文聚焦 (D) Client 以 Authorization Code 向 Token Endpoint 請求 Access Token、(E) Server 驗證核發 Token。
public class OAuthController : Controller
{
public async Task<ActionResult> Token()
{
var authServer = new AuthorizationServer(new AuthorizationServerHost());
var response = await authServer.HandleTokenRequestAsync(Request);
return response.AsActionResult();
}
}
public class AuthorizationServerHost : IAuthorizationServerHost
{
// 驗證授權碼有效性
public bool IsAuthorizationValid(IAuthorizationDescription authorization)
{
// 例:檢查 ClientId、RedirectUri、過期時間、Scope
return authorization.ClientIdentifier == "webclientdemo"
&& authorization.Scope.Contains("basic");
}
// 核發 Access Token
public AccessTokenResult CreateAccessToken(IAccessTokenRequest request)
{
var token = new AuthorizationServerAccessToken
{
Lifetime = TimeSpan.FromHours(10), // 10 小時有效
SymmetricKeyStore = this.CryptoKeyStore // 簽章金鑰
};
return new AccessTokenResult(token);
}
public ICryptoKeyStore CryptoKeyStore
=> new HardCodedKeyCryptoKeyStore("your-256-bit-base64-key==");
}
public class OAuthController : Controller
{
private const string ClientId = "webclientdemo";
private const string ClientSecret = "webclientdemosecret";
private const string TokenEndpoint = "https://authserver.example.com/oauth/token";
public async Task<ActionResult> Callback(string code)
{
using (var http = new HttpClient())
{
var form = new Dictionary<string, string>
{
{ "grant_type", "authorization_code" },
{ "code", code },
{ "redirect_uri", Request.Url.GetLeftPart(UriPartial.Path) },
{ "client_id", ClientId },
{ "client_secret", ClientSecret }
};
var response = await http.PostAsync(TokenEndpoint, new FormUrlEncodedContent(form));
string json = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var tokenObj = JObject.Parse(json);
string accessToken = tokenObj["access_token"].ToString();
// 儲存 Token 至 Session/資料庫,導向受保護資源
return Redirect("/Home/ProtectedResource");
}
return Content($"Token 申請失敗: {json}");
}
}
}
未使用 `DotNetOpenAuth.OAuth2.WebServerClient` 主因:跨平台相容性考量,純 HTTP 呼叫更透明、易除錯。
| 環節 | 注意事項 |
|---|---|
| Authorization Code | 單次使用、短效期 (建議 10 分鐘)、綁定 ClientId + RedirectUri |
| Client Secret | 機密應用才需驗證,公共應用 (SPA/行動App) 採用 PKCE 延伸 |
| Token 儲存 | Access Token 加密儲存,Refresh Token 更嚴格保護 |
| 錯誤處理 | 依 RFC 6749 回傳標準錯誤碼 (`invalid_grant`, `unauthorized_client` 等) |
public class BearerTokenHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
if (request.Headers.Authorization?.Scheme == "Bearer")
{
var analyzer = new StandardAccessTokenAnalyzer(AuthorizationServerHost.CryptoKeyStore);
var resourceServer = new ResourceServer(analyzer);
var principal = await resourceServer.GetPrincipalAsync(request, cancellationToken);
HttpContext.Current.User = principal;
Thread.CurrentPrincipal = principal;
}
return await base.SendAsync(request, cancellationToken);
}
}
// WebApiConfig.cs
config.MessageHandlers.Add(new BearerTokenHandler());
DotNetOpenAuth 文件較少,建議搭配原始碼閱讀 `AuthorizationServer`、`ResourceServer`、`StandardAccessTokenAnalyzer` 理解驗證流程。