ASP.NET Web API與Owin OAuth:呼叫與使用者相關的Web API

dudu發表於2015-06-15

在前一篇博文中,我們通過以 OAuth 的 Client Credential Grant 授權方式(只驗證呼叫客戶端,不驗證登入使用者)拿到的 Access Token ,成功呼叫了與使用者無關的 Web API。

在這篇博文中,我們將以 OAuth 的 Resource Owner Password Credentials Grant 的授權方式( grant_type=password )獲取 Access Token,並以這個 Token 呼叫與使用者相關的 Web API。

對應的應用場景是:為自家的網站開發手機 App(非第三方 App),只需使用者在 App 上登入,無需使用者對 App 所能訪問的資料進行授權。

根據 OAuth 規範,客戶端獲取 Access Token 的請求方式如下:

POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=johndoe&password=A3ddj3w

根據上面的請求方式,在 C# 中用 HttpClient 實現一個簡單的客戶端,程式碼如下:

public class OAuthClientTest
{
    private HttpClient _httpClient;

    public OAuthClientTest()
    {
        _httpClient = new HttpClient();
        _httpClient.BaseAddress = new Uri("http://openapi.cnblogs.com");
    } 

    [Fact]
    public async Task Get_Accesss_Token_By_Resource_Owner_Password_Credentials_Grant()
    {
        Console.WriteLine(await GetAccessToken());
    }

    private async Task<string> GetAccessToken()
    {
        var clientId = "1234";
        var clientSecret = "5678";

        var parameters = new Dictionary<string, string>();            
        parameters.Add("grant_type", "password");
        parameters.Add("username", "部落格園團隊");
        parameters.Add("password", "cnblogs.com");

        _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
            "Basic",
            Convert.ToBase64String(Encoding.ASCII.GetBytes(clientId + ":" + clientSecret))
            );

        var response = await _httpClient.PostAsync("/token", new FormUrlEncodedContent(parameters));
        var responseValue = await response.Content.ReadAsStringAsync();
        if (response.StatusCode == System.Net.HttpStatusCode.OK)
        {
            return JObject.Parse(responseValue)["access_token"].Value<string>();
        }
        else
        {
            Console.WriteLine(responseValue);
            return string.Empty;
        }
    }
}

(注:與之前相比,這裡的 client_id/client_secret 改為了 Basic Authorization,以更好的遵循 OAuth 規範)

在服務端,基於 Owin OAuth, 針對 Resource Owner Password Credentials Grant 的授權方式,只需過載 OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials() 方法即可。程式碼如下:

public class CNBlogsAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    //...

    public override async Task GrantResourceOwnerCredentials(
        OAuthGrantResourceOwnerCredentialsContext context)
    {
        //呼叫後臺的登入服務驗證使用者名稱與密碼

        var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
        oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
        var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties());
        context.Validated(ticket);

        await base.GrantResourceOwnerCredentials(context);
    }
}

完整的CNBlogsAuthorizationServerProvider實現程式碼如下(與之前相比,context.TryGetFormCredentials 改為了 context.TryGetBasicCredentials):

public class CNBlogsAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
    public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
    {
        string clientId;
        string clientSecret;
        context.TryGetBasicCredentials(out clientId, out clientSecret);

        if (clientId == "1234"
            && clientSecret == "5678")
        {
            context.Validated(clientId);
        }

        await base.ValidateClientAuthentication(context);
    }

    public override async Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
    {
        var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
        var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties());
        context.Validated(ticket);

        await base.GrantClientCredentials(context);
    }

    public override async Task GrantResourceOwnerCredentials(
        OAuthGrantResourceOwnerCredentialsContext context)
    {
        //呼叫後臺的登入服務驗證使用者名稱與密碼

        var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
        oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
        var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties());
        context.Validated(ticket);

        await base.GrantResourceOwnerCredentials(context);
    }
}
CNBlogsAuthorizationServerProvider

這樣,執行客戶端程式就可以拿到 Access Token 了。

接下來,我們拿著以這種方式獲取的 Access Token,就可以呼叫與使用者相關的 Web API 了。

在服務端我們通過一個簡單的 Web API 測試一下,程式碼如下:

public class UsersController : ApiController
{
    [Authorize]
    public string GetCurrent()
    {
        return User.Identity.Name;
        //這裡可以呼叫後臺使用者服務,獲取使用者相關數所,或者驗證使用者許可權進行相應的操作
    }
}

然後,客戶端用以 grant_type=password 方式拿到的 Access Token 呼叫這個Web API,客戶端增加的程式碼如下:

[Fact]
public async Task Call_WebAPI_By_Resource_Owner_Password_Credentials_Grant()
{
    var token = await GetAccessToken();
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    Console.WriteLine(await (await _httpClient.GetAsync("/api/users/current")).Content.ReadAsStringAsync());
}

客戶端執行結果如下:

"部落格園團隊"

呼叫成功!執行結果正是獲取 Access Token 時所用的 username 。 

結合 ASP.NET 現有的安全機制,藉助 OWIN 的威力,Microsoft.Owin.Security.OAuth 的確讓開發基於 OAuth 的 Web API 變得更簡單。

相關文章