前面在ASP.NET WEBAPI中集成了Client Credentials Grant與Resource 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部分。
