【问题标题】:OWIN AuthenticationOptions updating at runtime in mvc5 applicationOWIN AuthenticationOptions 在 mvc5 应用程序运行时更新
【发布时间】: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 5Writing an Owin Authentication Middleware 我意识到我应该覆盖AuthenticationHandler.ApplyResponseChallengeAsync() 并将这段代码放在此方法的开头:

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

我决定只使用 google,所以我们将讨论 google 中间件。

  1. AuthenticationHandlerAuthenticationMiddleWare.CreateHandler() 返回,在我的情况下,它们是 GoogleOAuth2AuthenticationHandlerGoogleOAuth2AuthenticationMiddleware
    我在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;
        }
    }
    
  2. 然后我用修改过的 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 */
        }   
    }
    

毕竟我得到了一些惊喜。

  1. 在 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 丢失

这就是为什么我使用助手将用户翻译为正确的用户

  1. 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


    【解决方案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/

    【讨论】:

    • 感谢您关注Shazwazza 的问题。我的英语不好,所以我认为你不理解我)这是我的失败不是你的。当我发布我的问题时,我已经解决了这个问题 - 我可以在运行时根据需要更改选项,但我怀疑实施还不够好。 p.s.我无法切换到您的变体,因为我的租户将在运行时创建,我必须在运行时完成所有工作:) 无论如何,谢谢
    猜你喜欢
    • 2018-06-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-10-06
    相关资源
    最近更新 更多