【发布时间】:2015-02-19 17:32:50
【问题描述】:
你好!
情况如下:
我在 iis7 上有一个带有 Identity2 的 MVC5 应用程序,它为多个网站提供服务。
主机名是特定网站的键。
网站,
另一个网站
等等
我决定在我的所有网站上使用谷歌外部登录,每个网站都应该是谷歌客户端,带有个人客户端 ID 和客户端密码。
例如:
site.com - clientid=123123, clientsecret=xxxaaabbb
anothersite.com - clientid=890890, clientsecret=zzzqqqeee
但是有一个小问题——
AuthenticationOptions 是在应用程序开始时设置的,我没有找到任何方法在运行时替换它。
所以,读完Creating Custom OAuth Middleware for MVC 5
和Writing an Owin Authentication Middleware
我意识到我应该覆盖AuthenticationHandler.ApplyResponseChallengeAsync()
并将这段代码放在此方法的开头:
Options.ClientId = OAuth2Helper.GetProviderAppId("google");
Options.ClientSecret = OAuth2Helper.GetProviderAppSecret("google");
我决定只使用 google,所以我们将讨论 google 中间件。
-
AuthenticationHandler由AuthenticationMiddleWare.CreateHandler()返回,在我的情况下,它们是GoogleOAuth2AuthenticationHandler和GoogleOAuth2AuthenticationMiddleware。
我在http://katanaproject.codeplex.com/ 找到了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<GoogleOAuth2AuthenticationMiddleware>(); _httpClient = new HttpClient(ResolveHttpMessageHandler(Options)); _httpClient.Timeout = Options.BackchannelTimeout; _httpClient.MaxResponseContentBufferSize = 1024 * 1024 * 10; // 10 MB } protected override AuthenticationHandler<GoogleOAuth2AuthenticationOptions> 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; } } -
然后我用修改过的 ApplyResponseChallengeAsync 创建了我自己的处理程序。在这一点上我有一个坏消息 -
GoogleOAuth2AuthenticationHandler是内部的,我不得不完全接受它并像这样投入我的项目(再次katanaproject.codeplex.com)public class GoogleOAuth2AuthenticationHandlerExtended : AuthenticationHandler<GoogleOAuth2AuthenticationOptions> { 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<AuthenticationTicket> AuthenticateCoreAsync() { AuthenticationProperties properties = null; try { string code = null; string state = null; IReadableStringCollection query = Request.Query; IList<string> 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<string, string>>(); body.Add(new KeyValuePair<string, string>("grant_type", "authorization_code")); body.Add(new KeyValuePair<string, string>("code", code)); body.Add(new KeyValuePair<string, string>("redirect_uri", redirectUri)); body.Add(new KeyValuePair<string, string>("client_id", Options.ClientId)); body.Add(new KeyValuePair<string, string>("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<string>("access_token"); string expires = response.Value<string>("expires_in"); string refreshToken = response.Value<string>("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<bool> InvokeAsync() { /* default code here */ } // no changes private async Task<bool> InvokeReplyPathAsync() { /* default code here */ } // no changes private static void AddQueryString(IDictionary<string, string> queryStrings, AuthenticationProperties properties, string name, string defaultValue = null) { /* default code here */ } }
毕竟我得到了一些惊喜。
-
在 myhost/signin-google 之后我得到 myhost/Account/ExternalLoginCallback?error=access_denied 和 302 重定向回登录页面,但没有成功。
那是因为GoogleOAuth2AuthenticatedContext构造函数的内部方法中很少有异常。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 没问题,但我仍然没有成功。为什么? 在 myhost/signin-google 之后我得到 myhost/Account/ExternalLoginCallback 和 302 重定向回登录页面没有成功。
谷歌回复中的id实际上是sub所以
• AuthenticatedContext 的 Id 属性未填充
• ClaimTypes.NameIdentifier 从未创建
• AccountController.ExternalLoginCallback(string returnUrl) 将始终重定向我们,因为 loginInfo 为空
GetExternalLoginInfo 采用 AuthenticateResult 不能为空
它会检查 result.Identity 是否存在 ClaimTypes.NameIdentifier
将sub 重命名为id 完成这项工作。
现在一切正常。
似乎 katana 的 microsoft 实现与 katana 源不同 因为如果我使用默认值,一切都没有任何魔法。
如果你能纠正我,如果你知道更简单的方法让 owin 使用在运行时根据主机名确定的 AuthenticationOptions,请告诉我
【问题讨论】:
标签: c# oauth-2.0 asp.net-identity-2