基於OWIN WebAPI 使用OAuth受權服務【客戶端模式(Client Credentials Grant)】

適應範圍

採用Client Credentials方式,即應用公鑰、密鑰方式獲取Access Token,適用於任何類型應用,但經過它所獲取的Access Token只能用於訪問與用戶無關的Open API,而且須要開發者提早向開放平臺申請,成功對接後方能使用。認證服務器不提供像用戶數據這樣的重要資源,僅僅是有限的只讀資源或者一些開放的 API。例如使用了第三方的靜態文件服務,如Google Storage或Amazon S3。這樣,你的應用須要經過外部API調用並以應用自己而不是單個用戶的身份來讀取或修改這些資源。這樣的場景就很適合使用客戶端證書受權,經過此受權方式獲取Access Token僅可訪問平臺受權類的接口。 php

好比獲取App首頁最新聞列表,因爲這個數據與用戶無關,因此不涉及用戶登陸與受權,但又不想任何人均可以調用這個WebAPI,這樣場景就適用[例:好比微信公衆平臺受權]。html

Client Credentials Grant:http://tools.ietf.org/html/rfc6749#section-4.4
     +---------+                                  +---------------+
     |         |                                  |               |
     |         |>--(A)- Client Authentication --->| Authorization |
     | Client  |                                  |     Server    |
     |         |<--(B)---- Access Token ---------<|               |
     |         |                                  |               |
     +---------+                                  +---------------+

                     Figure 6: Client Credentials Flow

基本流程

 

Client Credentials Grant

A.客戶端提供用戶名和密碼交換令牌
B.認證服務器驗證經過,發放令牌,後面根據這個令牌獲取資源便可
web

服務實現

使用WEBAPI基於Microsoft.Owin.Security.OAuth實現,源碼:http://katanaproject.codeplex.com/SourceControl/latest#src ;新建一個不啓用身份驗證空的WEBAPI項目
安裝包
json

Install-Package Microsoft.AspNet.Identity.Owin
Install-Package Microsoft.Owin.Security.OAuth
Install-Package Microsoft.AspNet.WebApi.Owin
Install-Package Microsoft.AspNet.WebApi.WebHost
Install-Package Microsoft.Owin.Host.SystemWeb
api

OWIN WEBAPI 服務器

[assembly: OwinStartup(typeof(Startup))]
namespace OAuth2.App_Start
{
    public partial class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            ConfigureAuth(app);
        }
    }
}
    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder app)
        {
            /*
              app.UseOAuthAuthorizationServer(new OAuthAuthorizationServerOptions
             {
                 TokenEndpointPath = new PathString("/token"),
                 Provider = new ApplicationOAuthProvider(),
                 AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
                 AuthenticationMode = AuthenticationMode.Active,
                 //HTTPS is allowed only AllowInsecureHttp = false
                 AllowInsecureHttp = true
                 //ApplicationCanDisplayErrors = false
             });
             app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
             */
            app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
            {
                TokenEndpointPath = new PathString("/token"),
                Provider = new ApplicationOAuthProvider(),
                //RefreshTokenProvider = new ApplicationRefreshTokenProvider(),
                AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
                AuthenticationMode = AuthenticationMode.Active,
                //HTTPS is allowed only AllowInsecureHttp = false
                AllowInsecureHttp = true
                //ApplicationCanDisplayErrors = false
            });
        }
    }
    public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
    {
        /*
         private OAuth2ClientService _oauthClientService;
         public ApplicationOAuthProvider()
         {
             this.OAuth2ClientService = new OAuth2ClientService();
         }
         */

        /// <summary>
        /// 驗證客戶[client_id與client_secret驗證]
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            //http://localhost:48339/token
            //grant_type=client_credentials&client_id=irving&client_secret=123456
            string client_id;
            string client_secret;
            context.TryGetFormCredentials(out client_id, out client_secret);
            if (client_id == "irving" && client_secret == "123456")
            {
                context.Validated(client_id);
            }
            else
            {
                //context.Response.StatusCode = Convert.ToInt32(HttpStatusCode.OK);
                context.SetError("invalid_client", "client is not valid");
            }
            return base.ValidateClientAuthentication(context);
        }

        /// <summary>
        /// 客戶端受權[生成access token]
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
        {
            /*
                 var client = _oauthClientService.GetClient(context.ClientId);
                 oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, client.ClientName));
             */


            var oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
            oAuthIdentity.AddClaim(new Claim(ClaimTypes.Name, "iphone"));
            var ticket = new AuthenticationTicket(oAuthIdentity, new AuthenticationProperties() { AllowRefresh = true });
            context.Validated(ticket);
            return base.GrantClientCredentials(context);
        }

        /// <summary>
        /// 刷新Token[刷新refresh_token]
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
        {
            //enforce client binding of refresh token
            if (context.Ticket == null || context.Ticket.Identity == null || !context.Ticket.Identity.IsAuthenticated)
            {
                context.SetError("invalid_grant", "Refresh token is not valid");
            }
            else
            {
                //Additional claim is needed to separate access token updating from authentication 
                //requests in RefreshTokenProvider.CreateAsync() method
            }
            return base.GrantRefreshToken(context);
        }

        public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
        {
            if (context.ClientId == "irving")
            {
                var expectedRootUri = new Uri(context.Request.Uri, "/");
                if (expectedRootUri.AbsoluteUri == context.RedirectUri)
                {
                    context.Validated();
                }
            }
            return Task.FromResult<object>(null);
        }
    }

資源服務 微信

    /// <summary>
    ///客戶端模式【Client Credentials Grant】
    ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api
    /// </summary>
    [RoutePrefix("api/v1/oauth2")]
    public class OAuth2Controller : ApiController
    {
        /// <summary>
        /// 得到資訊
        /// </summary>
        /// <returns></returns>
        [Authorize]
        [Route("news")]
        public async Task<IHttpActionResult> GetNewsAsync()
        {
            var authentication = HttpContext.Current.GetOwinContext().Authentication;
            var ticket = authentication.AuthenticateAsync("Bearer").Result;

            var claimsIdentity = User.Identity as ClaimsIdentity;
            var data = claimsIdentity.Claims.Where(c => c.Type == "urn:oauth:scope").ToList();
            var claims = ((ClaimsIdentity)Thread.CurrentPrincipal.Identity).Claims;
            return Ok(new { IsError = true, Msg = string.Empty, Data = Thread.CurrentPrincipal.Identity.Name + " It's about news !!! token expires: " + ticket.Properties.Dictionary.ToJson() });
        }
    }

啓用受權驗證[WebApiConfig]mvc

在ASP.NET Web API中啓用Token驗證,須要加上[Authorize]標記,而且配置默認啓用驗證不記名受權方式app

            // Web API configuration and services
            // Configure Web API to use only bearer token authentication.
            config.SuppressDefaultHostAuthentication();
            config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));

客戶端

得到票據微信公衆平臺

服務端[/token]獲取token須要三個參數

POST https://domain.com/token HTTP/1.1
Content-type:application/json;charset=UTF-8
grant_type=client_credentials&client_id=irving&client_secret=123456
 
{"access_token":"qghSowAcM9Ap7yIiyZ6i52VOk4NWBpgDDJZ6jf-PdAeP4roFMlGhKUV_Kg_ow0QgTXBKaPzIFBLzdc6evBUPVOaV8Op0wsrUwwKUjRluAPAQmw3MIm8MtmtC0Vfp7ZByuvvMy21NbpRBZcajQxzunJGPIqbdPMYs8e279T5UMmgpBVZJuC4N6d-mxk3DMN2-42cOxz-3k6J-7yXCVYroEh6txjZW03ws155LswIg0yw","token_type":"bearer","expires_in":7199}

image

請求資源

設置HTTP頭 Authorization: Bearer {THE TOKEN}

GET http://localhost:48339/api/v1/oauth2/news HTTP/1.1
Authorization: Bearer qghSowAcM9Ap7yIiyZ6i52VOk4NWBpgDDJZ6jf-PdAeP4roFMlGhKUV_Kg_ow0QgTXBKaPzIFBLzdc6evBUPVOaV8Op0wsrUwwKUjRluAPAQmw3MIm8MtmtC0Vfp7ZByuvvMy21NbpRBZcajQxzunJGPIqbdPMYs8e279T5UMmgpBVZJuC4N6d-mxk3DMN2-42cOxz-3k6J-7yXCVYroEh6txjZW03ws155LswIg0yw
grant_type=client_credentials&client_id=irving&client_secret=123456

{"IsError":true,"Msg":"","Data":"iphone It's about news !!! token expires: {\".refresh\":\"True\",\".issued\":\"Mon, 29 Jun 2015 02:47:12 GMT\",\".expires\":\"Mon, 29 Jun 2015 04:47:12 GMT\"}"}

image

客戶端測試

    /// <summary>
    ///客戶端模式【Client Credentials Grant】
    ///http://www.asp.net/web-api/overview/security/individual-accounts-in-web-api
    /// </summary>
    [RoutePrefix("api/v1/oauth2")]
    public class OAuth2Controller : ApiController
    {
        /// <summary>
        /// 獲取token
        /// </summary>
        /// <returns></returns>
        [Route("token")]
        public async Task<IHttpActionResult> GetTokenAsync()
        {
            //得到token
            var dict = new SortedDictionary<string, string>();
            dict.Add("client_id", "irving");
            dict.Add("client_secret", "123456");
            dict.Add("grant_type", "client_credentials");
            var data = await (@"http://" + Request.RequestUri.Authority + @"/token").PostUrlEncodedAsync(dict).ReceiveJson<Token>();
            //根據token得到諮詢信息 [Authorization: Bearer {THE TOKEN}]
            //var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithHeader("Authorization", "Bearer " + data.access_token).GetAsync().ReceiveString();
            var news = await (@"http://" + Request.RequestUri.Authority + @"/api/v1/oauth2/news").WithOAuthBearerToken(data.access_token).GetAsync().ReceiveString();
            return Ok(new { IsError = true, Msg = data, Data = news });
        }
    }
    public class Token
    {
        public string access_token { get; set; }
        public string token_type { get; set; }
        public string expires_in { get; set; }
    }

REFER:
Secure a Web API with Individual Accounts and Local Login in ASP.NET Web API 2.2
www.asp.net/web-api/overview/security/individual-accounts-in-web-api 
Use OWIN to Self-Host ASP.NET Web API 2
http://www.asp.net/web-api/overview/hosting-aspnet-web-api/use-owin-to-self-host-web-api
OWIN OAuth 2.0 Authorization Server
http://www.asp.net/aspnet/overview/owin-and-katana/owin-oauth-20-authorization-server
ASP.Net MVC: Creating an OAuth client credentials grant type token endpoint
http://www.hackered.co.uk/articles/asp-net-mvc-creating-an-oauth-client-credentials-grant-type-token-endpoint
OwinStartup not Starting … Why?
http://stackoverflow.com/questions/19760545/owinstartup-not-starting-why
[OAuth]基於DotNetOpenAuth實現Client Credentials Grant
http://www.cnblogs.com/dudu/p/oauth-dotnetopenauth-client-credentials-grant.html
一些例子
http://open.koudaitong.com/doc/oauth
http://developer.baidu.com/wiki/index.php?title=docs/oauth/client
https://wakatime.com/developers

相關文章
相關標籤/搜索