OWIN AuthenticationOptions在运行时在mvc5应用程序中更新

嗨!

情况如下:
我在iis7上有一个带有Identity2的MVC5应用程序,可以为多个网站提供服务。 主机名是某些网站的关键。
site.com,anothersite.com等

我决定在我的所有网站上使用google进行外部登录,并且每个网站都应该是具有个人clientid和clientsecret的google客户端。
例如:
site.com – clientid = 123123,clientsecret = xxxaaabbb
anothersite.com – clientid = 890890,clientsecret = zzzqqqeee

但有一点问题 – 在应用程序的开始设置AuthenticationOptions ,我没有找到任何方法在运行时替换它。

所以,在阅读为MVC 5创建自定义OAuth中间件和编写Owin身份validation中间件后,我意识到我应该覆盖AuthenticationHandler.ApplyResponseChallengeAsync()并将这段代码放在这个方法的开头:

  Options.ClientId = OAuth2Helper.GetProviderAppId("google"); Options.ClientSecret = OAuth2Helper.GetProviderAppSecret("google"); 

我决定只使用谷歌,所以我们将谈论谷歌中间件。

  1. AuthenticationHandler AuthenticationMiddleWare.CreateHandler()返回AuthenticationMiddleWare.CreateHandler() ,在我的例子中,它们是GoogleOAuth2AuthenticationHandlerGoogleOAuth2AuthenticationMiddleware
    我在http://katanaproject.codeplex.com/上找到了GoogleOAuth2AuthenticationMiddleware并将其GoogleOAuth2AuthenticationMiddleware我的项目中

     public class GoogleAuthenticationMiddlewareExtended : GoogleOAuth2AuthenticationMiddleware { private readonly ILogger _logger; private readonly HttpClient _httpClient; public GoogleAuthenticationMiddlewareExtended( OwinMiddleware next, IAppBuilder app, GoogleOAuth2AuthenticationOptions options) : base(next, app, options) { _logger = app.CreateLogger(); _httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); _httpClient.Timeout = Options.BackchannelTimeout; _httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB } protected override AuthenticationHandler CreateHandler() { return new GoogleOAuth2AuthenticationHandlerExtended(_httpClient, _logger); } private static HttpMessageHandler ResolveHttpMessageHandler(GoogleOAuth2AuthenticationOptions options) { HttpMessageHandler handler = options.BackchannelHttpHandler ?? new WebRequestHandler(); // If they provided a validator, apply it or fail. if (options.BackchannelCertificateValidator != null) { // Set the cert validate callback var webRequestHandler = handler as WebRequestHandler; if (webRequestHandler == null) { throw new InvalidOperationException("Exception_ValidatorHandlerMismatch"); } webRequestHandler.ServerCertificateValidationCallback = options.BackchannelCertificateValidator.Validate; } return handler; } } 
  2. 然后我用修改后的ApplyResponseChallengeAsync创建了我自己的Handler。 我在这一点上有一个坏消息 – GoogleOAuth2AuthenticationHandler是内部的,我不得不完全接受它并像我这样放入我的项目( katanaproject.codeplex.com )

     public class GoogleOAuth2AuthenticationHandlerExtended : AuthenticationHandler { private const string TokenEndpoint = "https://accounts.google.com/o/oauth2/token"; private const string UserInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo?access_token="; private const string AuthorizeEndpoint = "https://accounts.google.com/o/oauth2/auth"; private readonly ILogger _logger; private readonly HttpClient _httpClient; public GoogleOAuth2AuthenticationHandlerExtended(HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; } // i've got some surpises here protected override async Task AuthenticateCoreAsync() { AuthenticationProperties properties = null; try { string code = null; string state = null; IReadableStringCollection query = Request.Query; IList values = query.GetValues("code"); if (values != null && values.Count == 1) { code = values[0]; } values = query.GetValues("state"); if (values != null && values.Count == 1) { state = values[0]; } properties = Options.StateDataFormat.Unprotect(state); if (properties == null) { return null; } // OAuth2 10.12 CSRF if (!ValidateCorrelationId(properties, _logger)) { return new AuthenticationTicket(null, properties); } string requestPrefix = Request.Scheme + "://" + Request.Host; string redirectUri = requestPrefix + Request.PathBase + Options.CallbackPath; // Build up the body for the token request var body = new List<KeyValuePair>(); body.Add(new KeyValuePair("grant_type", "authorization_code")); body.Add(new KeyValuePair("code", code)); body.Add(new KeyValuePair("redirect_uri", redirectUri)); body.Add(new KeyValuePair("client_id", Options.ClientId)); body.Add(new KeyValuePair("client_secret", Options.ClientSecret)); // Request the token HttpResponseMessage tokenResponse = await _httpClient.PostAsync(TokenEndpoint, new FormUrlEncodedContent(body)); tokenResponse.EnsureSuccessStatusCode(); string text = await tokenResponse.Content.ReadAsStringAsync(); // Deserializes the token response JObject response = JObject.Parse(text); string accessToken = response.Value("access_token"); string expires = response.Value("expires_in"); string refreshToken = response.Value("refresh_token"); if (string.IsNullOrWhiteSpace(accessToken)) { _logger.WriteWarning("Access token was not found"); return new AuthenticationTicket(null, properties); } // Get the Google user HttpResponseMessage graphResponse = await _httpClient.GetAsync( UserInfoEndpoint + Uri.EscapeDataString(accessToken), Request.CallCancelled); graphResponse.EnsureSuccessStatusCode(); // i will show content of this var later text = await graphResponse.Content.ReadAsStringAsync(); JObject user = JObject.Parse(text); //because of permanent exception in GoogleOAuth2AuthenticatedContext constructor i prepare user data with my extension JObject correctUser = OAuth2Helper.PrepareGoogleUserInfo(user); // i've replaced this with selfprepared user2 //var context = new GoogleOAuth2AuthenticatedContext(Context, user, accessToken, refreshToken, expires); var context = new GoogleOAuth2AuthenticatedContext(Context, correctUser, accessToken, refreshToken, expires); context.Identity = new ClaimsIdentity( Options.AuthenticationType, ClaimsIdentity.DefaultNameClaimType, ClaimsIdentity.DefaultRoleClaimType); if (!string.IsNullOrEmpty(context.Id)) { context.Identity.AddClaim(new Claim(ClaimTypes.NameIdentifier, context.Id, ClaimValueTypes.String, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.GivenName)) { context.Identity.AddClaim(new Claim(ClaimTypes.GivenName, context.GivenName, ClaimValueTypes.String, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.FamilyName)) { context.Identity.AddClaim(new Claim(ClaimTypes.Surname, context.FamilyName, ClaimValueTypes.String, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.Name)) { context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Name, ClaimValueTypes.String, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.Email)) { context.Identity.AddClaim(new Claim(ClaimTypes.Email, context.Email, ClaimValueTypes.String, Options.AuthenticationType)); } if (!string.IsNullOrEmpty(context.Profile)) { context.Identity.AddClaim(new Claim("urn:google:profile", context.Profile, ClaimValueTypes.String, Options.AuthenticationType)); } context.Properties = properties; await Options.Provider.Authenticated(context); return new AuthenticationTicket(context.Identity, context.Properties); } catch (Exception ex) { _logger.WriteError("Authentication failed", ex); return new AuthenticationTicket(null, properties); } } protected override Task ApplyResponseChallengeAsync() { // finaly! here it is. i just want to put this two lines here. thats all Options.ClientId = OAuth2Helper.GetProviderAppId("google"); Options.ClientSecret = OAuth2Helper.GetProviderAppSecret("google"); /* default code ot the method */ } // no changes public override async Task InvokeAsync() { /* default code here */ } // no changes private async Task InvokeReplyPathAsync() { /* default code here */ } // no changes private static void AddQueryString(IDictionary queryStrings, AuthenticationProperties properties, string name, string defaultValue = null) { /* default code here */ } } 

毕竟我得到了一些惊喜。

  1. 在myhost / signin-google之后我得到myhost / Account / ExternalLoginCallback?error = access_denied并且302重定向回登录页面但没有成功。
    这是因为GoogleOAuth2AuthenticatedContext构造函数的内部方法中的Exception很少。

     GivenName = TryGetValue(user, "name", "givenName"); FamilyName = TryGetValue(user, "name", "familyName"); 

  Email = TryGetFirstValue(user, "emails", "value"); 

这是我们转换为JObject user的谷歌回复

  { "sub": "XXXXXXXXXXXXXXXXXX", "name": "John Smith", "given_name": "John", "family_name": "Smith", "profile": "https://plus.google.com/XXXXXXXXXXXXXXXXXX", "picture": "https://lh5.googleusercontent.com/url-to-the-picture/photo.jpg", "email": "usermail@domain.com", "email_verified": true, "gender": "male", "locale": "ru", "hd": "google application website" } 

name是字符串, TryGetValue(user, "name", "givenName")将失败,因为TryGetValue(user, "name", "familyName")
错过了emails

这就是为什么我使用帮助器翻译用户纠正correctUser

  1. correctUser还可以,但我还是没有成功。 为什么? 在myhost / signin-google之后我得到myhost / Account / ExternalLoginCallback并且302重定向回登录页面但没有成功。

谷歌响应中的id实际上是sub
•未填充AuthenticatedContext的Id属性
ClaimTypes.NameIdentifier从未创建过
•AccountController.ExternalLoginCallback(string returnUrl)将始终重定向我们,因为loginInfo为null

GetExternalLoginInfo采用AuthenticateResult,它不应该为null并且它检查result.Identity对于ClaimTypes.NameIdentifier存在

sub重命名为id完成工作。 现在一切都好。

似乎katana的微软实现与katana源不同,因为如果我使用默认,一切都是没有任何魔法的工作。

如果你能纠正我,如果你知道更简单的方法让owin在运行时根据主机名确定AuthenticationOptions,请告诉我

我最近一直在努力争取让多个租户与同一个OAuth提供商合作,但是使用不同的帐户。 我知道你想在运行时动态更新选项,但你可能不需要这样做,希望这有助于……

我认为你没有这个工作的原因,即使覆盖所有这些类,因为每个配置的Google OAuth帐户都需要有一个唯一的CallbackPath。 这决定了哪个注册的提供者和选项将在回调上执行。

您可以在启动时声明每个OAuth提供程序,并确保它们具有唯一的AuthenticationType和唯一的CallbackPath,而不是尝试动态执行此操作,例如:

 //Provider #1 app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions { AuthenticationType = "Google-Site.Com", ClientId = "abcdef...", ClientSecret = "zyxwv....", CallbackPath = new PathString("/sitecom-signin-google") }); //Provider #2 app.UseGoogleAuthentication(new GoogleOAuth2AuthenticationOptions { AuthenticationType = "Google-AnotherSite.com", ClientId = "abcdef...", ClientSecret = "zyxwv....", CallbackPath = new PathString("/anothersitecom-signin-google") }); 

然后,在您调用IOwinContext.Authentication.Challenge ,确保为要validation的当前租户传递正确命名的AuthenticationType。 示例: HttpContext.GetOwinContext().Authentication.Challenge(properties, "Google-AnotherSite.com");

下一步是更新Google开发者控制台中的回调路径,以匹配您的自定义回调路径。 默认情况下,它是“signin-google”,但每个都需要在您声明的提供程序中是唯一的,以便提供程序知道它需要处理该路径上的特定回调。

我实际上只是在这里详细介绍了所有这些内容: http : //shazwazza.com/post/configuring-aspnet-identity-oauth-login-providers-for-multi-tenancy/