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

时间:2015-02-19 17:32:50

标签: c# oauth-2.0 asp.net-identity-2

嗨!

情况如下:
我在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");

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

    {li>

    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;
        }
    }
    
  1. 然后我用修改后的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 */
        }   
    }
    
  2. 毕竟我得到了一些惊喜。

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

      GivenName = TryGetValue(user, "name", "givenName");
      FamilyName = TryGetValue(user, "name", "familyName");
      
    2.     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 /帐号/ ExternalLoginCallback 并且302重定向回登录页面但没有成功。
      2. 谷歌回复中的

        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,请告诉我

1 个答案:

答案 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/