Fork me on GitHub

基于OWIN ASP.NET WebAPI 使用OAUTH2授权服务的几点优化

前面在ASP.NET WEBAPI中集成了Client Credentials GrantResource Owner Password Credentials Grant两种OAUTH2模式,今天在调试Client Credentials Grant想到如下几点

  • 建议TryGetBasicCredentials认证 validate client credentials should be stored securely (salted, hashed, iterated),参考PDF设计
  • 增加token额外字段
  • 增加scope授权字段
  • 持久化Token
  • 刷新Token后失效老的Token
  • 自定义验证【重启IIS池Token失效,验证权限】

优化点

1.启用TryGetBasicCredentials认证:Basic Authentication传递clientId与clientSecret,服务端中的TryGetFormCredentials()改为TryGetBasicCredentials()
2.增加token额外字段:需要重写TokenEndpoint方法
http://stackoverflow.com/questions/26357054/return-more-info-to-the-client-using-oauth-bearer-tokens-generation-and-owin-in ,一般无特殊要求,不建议加
3.参数中传soap字段,以空格分隔的权限列表,若不传递此参数,代表请求用户的默认权限
4.重写AccessTokenProvider中CreateAsync方法,生成Token值持久化相关信息到DB
5.重写AccessTokenProvider中ReceiveAsync方法,验证Token是否有效

服务实现

配置Startup

/// <summary>
        /// IOS App OAuth2 Credential Grant Password Service
        /// </summary>
        /// <param name="app"></param>
        public void ConfigureAuth(IAppBuilder app)
        {
            //ClientApplicationOAuthProvider
            app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
            {
                //AuthorizeEndpointPath = new PathString("/authorize")
                TokenEndpointPath = new PathString("/token"),
                Provider = GlobalConfiguration.Configuration.DependencyResolver.GetRootLifetimeScope().Resolve<ClientAuthorizationServerProvider>(),
                AccessTokenProvider = GlobalConfiguration.Configuration.DependencyResolver.GetRootLifetimeScope().Resolve<AccessTokenAuthorizationServerProvider>(),
                AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(1),
                AuthenticationMode = AuthenticationMode.Active,
                //HTTPS is allowed only AllowInsecureHttp = false
#if DEBUG
                AllowInsecureHttp = true,
#endif
                ApplicationCanDisplayErrors = true,
            });

            /*
               //PasswordAuthorizationServerProvider
               app.UseOAuthBearerTokens(new OAuthAuthorizationServerOptions
               {
                   //!!!
                   // AccessTokenProvider=
                   TokenEndpointPath = new PathString("/token"),
                   //Provider = new ClientApplicationOAuthProvider(),
                   //Provider = new PasswordAuthorizationServerProvider(),
                   //Provider = DependencyInjectionConfig.container.Resolve<PasswordAuthorizationServerProvider>(),
                   //Provider = DependencyResolver.Current.GetService<PasswordAuthorizationServerProvider>(),
                   Provider = GlobalConfiguration.Configuration.DependencyResolver.GetRootLifetimeScope().Resolve<PasswordAuthorizationServerProvider>(),
                   RefreshTokenProvider = GlobalConfiguration.Configuration.DependencyResolver.GetRootLifetimeScope().Resolve<RefreshAuthenticationTokenProvider>(),
                   AccessTokenExpireTimeSpan = TimeSpan.FromHours(2),
                   AuthenticationMode = AuthenticationMode.Active,
                   //HTTPS is allowed only AllowInsecureHttp = false
   #if DEBUG
                   AllowInsecureHttp = true,
   #endif
               });
               */

            //app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
            //app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        }

集成Autofac

            //注册 Password Grant 授权服务
              builder.RegisterType<PasswordAuthorizationServerProvider>().AsSelf().SingleInstance();
            builder.RegisterType<RefreshAuthenticationTokenProvider>().AsSelf().SingleInstance();
            //注册  Credential Grant Password 
            builder.RegisterType<ClientAuthorizationServerProvider>().AsSelf().SingleInstance();
            builder.RegisterType<AccessTokenAuthorizationServerProvider>().AsSelf().SingleInstance();
            //在Autofac中注册Redis的连接,并设置为Singleton (官方建議保留Connection,重複使用)
            //builder.Register(r =>{ return ConnectionMultiplexer.Connect(DBSetting.Redis);}).AsSelf().SingleInstance();
            var container = builder.Build();
            GlobalConfiguration.Configuration.DependencyResolver = new AutofacWebApiDependencyResolver(container);

启用不记名验证

public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            // Configure Web API to use only bearer token authentication.
            config.SuppressDefaultHostAuthentication();

            config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }

服务端

/// <summary>
    /// Client Credentials 授权
    /// </summary>
    public class ClientAuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        /// <summary>
        /// 授权服务
        /// </summary>
        private readonly IClientAuthorizationService _clientAuthorizationService;

        /// <summary>
        /// 账户服务
        /// </summary>
        private readonly IAccountService _accountService;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="clientAuthorizationService">授权服务</param>
        /// <param name="accountService">用户服务</param>
        public ClientAuthorizationServerProvider(IClientAuthorizationService clientAuthorizationService, IAccountService accountService)
        {
            _clientAuthorizationService = clientAuthorizationService;
            _accountService = accountService;
        }

        /// <summary>
        /// 验证Client Credentials[client_id与client_secret]
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            //http://localhost:48339/token
            //grant_type=client_credentials&client_id=irving&client_secret=123456&scope=user order
            /*
            grant_type     授与方式(固定为 “client_credentials”)
            client_id        分配的调用oauth的应用端ID
            client_secret  分配的调用oaut的应用端Secret
            scope            授权权限。以空格分隔的权限列表,若不传递此参数,代表请求用户的默认权限
            */
            //validate client credentials should be stored securely (salted, hashed, iterated)
            string clientId;
            string clientSecret;
            context.TryGetBasicCredentials(out clientId, out clientSecret);
            //验证用户名密码
            var clientValid = await _clientAuthorizationService.ValidateClientAuthorizationSecretAsync(clientId, clientSecret);
            if (!clientValid)
            {
                //Flurl 404 问题
                //context.Response.StatusCode = Convert.ToInt32(HttpStatusCode.OK);
                //context.Rejected();
                context.SetError(AbpConstants.InvalidClient, AbpConstants.InvalidClientErrorDescription);
                return;
            }
            //need to make the client_id available for later security checks
            context.OwinContext.Set<string>("as:client_id", clientId);
            context.Validated(clientId);
        }

        /// <summary>
        /// 客户端授权[生成access token]
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task GrantClientCredentials(OAuthGrantClientCredentialsContext context)
        {
            /*
               var client = _oauthClientService.GetClient(context.ClientId);
               claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, client.ClientName));
             */
            //验证权限
            int scopeCount = context.Scope.Count;
            if (scopeCount > 0)
            {
                string name = context.Scope[0].ToString();
            }
            //默认权限
            var claimsIdentity = new ClaimsIdentity(context.Options.AuthenticationType);
            //!!!
            claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, context.ClientId));
            var props = new AuthenticationProperties(new Dictionary<string, string> {
                            {
                                "client_id",context.ClientId
                            },
                            {
                                "scope",string.Join(" ",context.Scope)
                            }
                        });
            var ticket = new AuthenticationTicket(claimsIdentity, props);
            context.Validated(ticket);
            return base.GrantClientCredentials(context);
        }

        /// <summary>
        /// http://stackoverflow.com/questions/26357054/return-more-info-to-the-client-using-oauth-bearer-tokens-generation-and-owin-in
        /// My recommendation is not to add extra claims to the token if not needed, because will increase the size of the token and you will keep sending it with each request. As LeftyX advised add them as properties but make sure you override TokenEndPoint method to get those properties as a response when you obtain the toke successfully, without this end point the properties will not return in the response.
        /// </summary>
        /// <param name="context"></param>
        /// <returns></returns>
        public override Task TokenEndpoint(OAuthTokenEndpointContext context)
        {
            foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
            {
                context.AdditionalResponseParameters.Add(property.Key, property.Value);
            }
            return base.TokenEndpoint(context);
        }
    }

Token生成与验证

/// <summary>
    /// 生成与验证Token
    /// </summary>
    public class AccessTokenAuthorizationServerProvider : AuthenticationTokenProvider
    {
        /// <summary>
        /// 授权服务
        /// </summary>
        private readonly IClientAuthorizationService _clientAuthorizationService;

        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="clientAuthorizationService">授权服务</param>
        public AccessTokenAuthorizationServerProvider(IClientAuthorizationService clientAuthorizationService)
        {
            _clientAuthorizationService = clientAuthorizationService;
        }

        //<summary>
        //创建Token
        //</summary>
        //<param name="context">上下文</param>
        //<returns></returns>
        public override async Task CreateAsync(AuthenticationTokenCreateContext context)
        {
            if (string.IsNullOrEmpty(context.Ticket.Identity.Name)) return;
            string IpAddress = context.Request.RemoteIpAddress + ":" + context.Request.RemotePort;
            var token = new Token()
            {
                ClientId = context.Ticket.Identity.Name,
                ClientType = "client_credentials",
                Scope = context.Ticket.Properties.Dictionary["scope"],
                UserName = context.Ticket.Identity.Name,
                IssuedUtc = DateTime.Parse(context.Ticket.Properties.IssuedUtc.ToString()),
                ExpiresUtc = DateTime.Parse(context.Ticket.Properties.IssuedUtc.ToString()),
                IpAddress = IpAddress
            };
            token.AccessToken = context.SerializeTicket();
            token.RefreshToken = string.Empty;//await _clientAuthorizationService.GenerateOAuthClientSecretAsync();
            //Token没有过期的情况强行刷新,删除老的Token保存新的Token
            if (await _clientAuthorizationService.SaveTokenAsync(token))
            {
                context.SetToken(token.AccessToken);
            }
        }

        //<summary>
        //验证Token
        //</summary>
        //<param name="context">上下文</param>
        //<returns></returns>
        public override async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
        {
            var request = new OAuthRequestTokenContext(context.OwinContext, context.Token);
            var ticket = new AuthenticationTicket(new ClaimsIdentity(), new AuthenticationProperties()
            {
                IssuedUtc = DateTime.UtcNow.AddYears(-1),
                ExpiresUtc = DateTime.UtcNow.AddYears(-1)
            });
            if (request == null || request.Token.IsNullOrEmpty())
            {
                context.SetTicket(ticket);
            }
            //验证Token是否过期
            var vaild = await _clientAuthorizationService.VaildOAuthClientSecretAsync();
            if (vaild)
            {
                context.SetTicket(ticket);
            }
        }
    }

有赞API文档

无意看到有赞的API文档:http://open.koudaitong.com/doc,大致分为三个部分。

1.基于OAUTH2授权【几种授权模式全实现】

2.基于签名的方式【HMAC】

3.各种语言的SDK

大致设计比较规范,后续时间再参考规范基于ASP.NET WEBAPI 集成优化OAUTH2与HMAC部分。

posted @ 2015-11-06 17:10  花儿笑弯了腰  阅读(3914)  评论(22编辑  收藏  举报