Client Credentials Grant 適用於機器對機器 (M2M) 場景:Client 直接以自身身分向 Authorization Server 申請 Access Token,無需使用者介入。本文基於 DotNetOpenAuth 完整示範 Server/Client/Resource Server 三端實作。


public async Task<ActionResult> GetSiteHomePosts()
{
var clientId = "m.cnblogs.com";
var clientSecret = "20140213";
// Basic Auth: base64(client_id:client_secret)
var credentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{clientId}:{clientSecret}"));
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Basic", credentials);
var form = new Dictionary<string, string> { { "grant_type", "client_credentials" } };
var response = await http.PostAsync("https://authserver.example.com/oauth/token",
new FormUrlEncodedContent(form));
var json = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
var token = JObject.Parse(json)["access_token"].ToString();
return Content($"AccessToken: {token}");
}
return Content(json);
}
}
public class Client : IClientDescription
{
public string Id { get; set; }
public string Secret { get; set; }
public Uri DefaultCallback => throw new NotImplementedException();
public ClientType ClientType { get; set; }
public bool HasNonEmptySecret => !string.IsNullOrEmpty(Secret);
public bool IsCallbackAllowed(Uri callback) => throw new NotImplementedException();
// 核心驗證邏輯
public bool IsValidClientSecret(string secret) => Secret == secret;
}
public class AuthorizationServerHost : IAuthorizationServerHost
{
public static readonly ICryptoKeyStore CryptoKeyStore = new HardCodedKeyCryptoKeyStore("base64-key==");
public IClientDescription GetClient(string clientIdentifier)
{
// 從資料庫/快取查詢 Client
return ServiceLocator.GetService<IClientService>().GetClient(clientIdentifier);
}
public AccessTokenResult CreateAccessToken(IAccessTokenRequest request)
{
var token = new AuthorizationServerAccessToken
{
Lifetime = TimeSpan.FromHours(10),
SymmetricKeyStore = CryptoKeyStore
};
return new AccessTokenResult(token);
}
// Client Credentials Grant 特定驗證
public AutomatedAuthorizationCheckResponse CheckAuthorizeClientCredentialsGrant(IAccessTokenRequest request)
{
var client = GetClient(request.ClientIdentifier);
if (client == null)
return new AutomatedAuthorizationCheckResponse(false, "Client not found");
if (!client.IsValidClientSecret(request.ClientSecret))
return new AutomatedAuthorizationCheckResponse(false, "Invalid secret");
return new AutomatedAuthorizationCheckResponse(true);
}
// 其他介面成員略...
public INonceStore NonceStore => null;
public bool IsAuthorizationValid(IAuthorizationDescription auth) => true;
}
public async Task<ActionResult> CallApi(string accessToken)
{
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", accessToken);
var response = await http.GetAsync("https://api.example.com/blog/posts/sitehome");
return Content(await response.Content.ReadAsStringAsync());
}
}
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MessageHandlers.Add(new BearerTokenHandler());
}
}
public class BearerTokenHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken ct)
{
if (request.Headers.Authorization?.Scheme == "Bearer")
{
var analyzer = new StandardAccessTokenAnalyzer(AuthorizationServerHost.CryptoKeyStore);
var rs = new ResourceServer(analyzer);
var principal = await rs.GetPrincipalAsync(request, ct);
HttpContext.Current.User = principal;
Thread.CurrentPrincipal = principal;
}
return await base.SendAsync(request, ct);
}
}
[RoutePrefix("blog/posts")]
public class PostsController : ApiController
{
[Route("sitehome")]
public IEnumerable<string> GetSiteHome()
{
// User.Identity.Name 來自驗證後的 Principal
return new[] { $"Client: {User.Identity.Name}" };
}
}
呼叫 `/blog/posts/sitehome` 回傳:
["Client: m.cnblogs.com"]
實作過程中最大坑點:`CheckAuthorizeClientCredentialsGrant` 回傳型別為 `AutomatedAuthorizationCheckResponse`,非單純 bool,需正確建構回傳物件。