嗨!
情况如下:
我在iis7上有一个带有Identity2的MVC5应用程序,可以为多个网站提供服务。
主机名是某些网站的关键
site.com,
anothersite.com
等等
我决定在我的所有网站上使用google进行外部登录,并且每个网站都应该是具有个人clientid和clientsecret的google客户端。
例如:
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");
我决定只使用谷歌,所以我们将讨论谷歌中间件。
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创建了我自己的Handler。我此时得到了一个坏消息 - 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 /帐号/ ExternalLoginCallback?错误= 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
id
实际上是sub
所以
•AuthenticatedContext的Id属性未填写
•ClaimTypes.NameIdentifier
从未创建过
•由于loginInfo为null,AccountController.ExternalLoginCallback(string returnUrl)将始终重定向我们
GetExternalLoginInfo采用AuthenticateResult,不应该为null
并检查result.Identity
是否存在ClaimTypes.NameIdentifier
将sub
重命名为id
即可完成工作。
现在一切都好。
似乎katana的微软实现与katana源不同 因为如果我使用默认,一切都没有任何魔力。
如果你能纠正我,如果你知道更简单的方法让owin在运行时根据主机名确定AuthenticationOptions,请告诉我
答案 0 :(得分:1)
我最近一直在与尝试使用同一个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
的位置,确保为要验证的当前租户传递正确命名的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/