Hi!
情况如下:
我在 iis 7 上有一个带有 Identity 2 的 MVC 5 应用程序,该应用程序为多个网站提供服务。
主机名是某些网站的关键。
网站,
另一个网站
等等
我决定在我的所有网站上使用谷歌外部登录,每个网站都应该是带有个人 clientid 和 clientsecret 的谷歌客户端。
例如:
site.com - clientid=123123,clientsecret=xxxaaabbb
anothersite.com - clientid=890890,clientsecret=zzzqqqeee
但有一个小问题——AuthenticationOptions
是在应用程序启动时设置的,我没有找到任何方法在运行时替换它。
所以,读完后为 MVC 5 创建自定义 OAuth 中间件 https://www.simple-talk.com/dotnet/.net-framework/creating-custom-oauth-middleware-for-mvc-5/
and 编写 Owin 身份验证中间件 http://coding.abel.nu/2014/06/writing-an-owin-authentication-middleware/我意识到我应该重写AuthenticationHandler.ApplyResponseChallengeAsync()
并将这段代码放在该方法的开头:
Options.ClientId = OAuth2Helper.GetProviderAppId("google");
Options.ClientSecret = OAuth2Helper.GetProviderAppSecret("google");
我决定只使用谷歌,所以我们将讨论谷歌中间件。
-
AuthenticationHandler
由返回AuthenticationMiddleWare.CreateHandler()
就我而言,他们是GoogleOAuth2AuthenticationHandler
and GoogleOAuth2AuthenticationMiddleware
.
我找到了GoogleOAuth2AuthenticationMiddleware
at the http://katanaproject.codeplex.com/ http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.Google/GoogleOAuth2AuthenticationMiddleware.cs并把它像这样放入我的项目中
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 http://katanaproject.codeplex.com/SourceControl/latest#src/Microsoft.Owin.Security.Google/GoogleOAuth2AuthenticationHandler.cs)
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");
and
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": "[email protected] /cdn-cgi/l/email-protection",
"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/帐户/ExternalLoginCallback
302 重定向回登录页面但没有成功。
id
在谷歌的回应实际上是sub
so
• AuthenticationContext 的 Id 属性未填充
• ClaimTypes.NameIdentifier
从未创建过
• AccountController.ExternalLoginCallback(string returnUrl) 将始终重定向我们,因为 loginInfo 为 null
GetExternalLoginInfo 采用 AuthenticateResult ,该结果不应为 null
它检查result.Identity
对于 ClaimTypes.NameIdentifier 是否存在
重命名sub
into id
做工作。
现在一切都好了。
微软对 katana 的实现似乎与 katana 源不同
因为如果我使用默认值,一切都会正常工作,没有任何魔法。
如果您可以纠正我,如果您知道使 owin 与运行时基于主机名确定的 AuthenticationOptions 一起使用的更简单方法,请告诉我