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

2024-06-29

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");

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

  1. 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;
        }
    }
    
  2. 然后我使用修改后的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 */
        }   
    }
    

毕竟我得到了一些惊喜。

  1. 在 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被错过了

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

  1. 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 一起使用的更简单方法,请告诉我


我最近一直在努力尝试让多租户与同一个 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/ http://shazwazza.com/post/configuring-aspnet-identity-oauth-login-providers-for-multi-tenancy/

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

OWIN AuthenticationOptions 在 mvc5 应用程序中运行时更新 的相关文章

  • 如何反向遍历 C# 集合?

    是否有可能有一个foreach将以相反顺序遍历 Collections 对象的语句 如果不是foreach声明 还有其他方法吗 你可以用普通的for向后循环 如下所示 for int i collection Count 1 i gt 0
  • C++:空类的对象的大小是多少?

    我想知道可能是什么空类对象的大小 肯定可以not为 0 字节 因为它应该可以像任何其他对象一样引用和指向它 但是 这样的物体有多大呢 我用了这个小程序 include
  • ~ C 中的运算符

    该程序的输出是 13 我从来没有完全理解 C 中的 运算符 为什么它给出 13 作为输出 如何将 运算符限制为数字的 4 位 include
  • 在运行时更改语言的正确方法

    在运行时更改表单语言的正确方法是什么 使用递归手动设置所有控件 例如this https stackoverflow com questions 7556367 how do i change the culture of a winfor
  • Qt:将事件发布到 QThread 的正确方法?

    在我的 Qt 应用程序中 我有一个主线程和一个工作线程 工作线程子类QThread并通过处理事件customEvent 这是主线程发送要由工作线程处理的事件的正确方法吗 QThread myWorkerThread QApplication
  • IE7 中的多个选项卡和一个工具栏实例

    我用 C 开发了一个用于 Internet Explorer 的工具栏 用于从外部应用程序发送和接收 URL 它在 IE6 中运行得很好 但我想在新版本 IE7 中利用选项卡式浏览 但我在那里遇到了障碍 似乎在 IE7 中 每个选项卡都有一
  • 如何使用鼠标指针和键盘快捷键捕获文本?

    我想使用 C 或 java 使用鼠标指针和键盘快捷键从打开的窗口捕获文本 喜欢babylon http babylon com 所以 我需要知道什么以及如何实施 我需要使用哪些库 或者我可以使用 winapi 吗 使用脚本语言创建您想要执行
  • 字符集中字符的顺序

    是否通过标准保证字符的顺序 例如 我可以算出字符集表中 1 符号后面跟着 2 符号吗 或者它是特定于平台的 1999 年的 C 标准对字符集是这样规定的 基本源字符集和基本执行字符集都应具有以下成员 拉丁字母表中的 26 个大写字母 拉丁字
  • 创建新文件夹的“Shell 命名空间”方式是什么?

    显然 这对于 win32 api CreateDirectory 来说是微不足道的 但我正在尝试托管一个 IShellView 并且希望以最面向 shell 的方式来做到这一点 我本以为 IShellFolder 中会有一个 createo
  • 指示 GDB 6.5 使用目标文件中嵌入的源代码

    我一直在努力让GNU gdb 6 5 14在调试时使用嵌入在目标文件中的源代码 而不是扫描某些目录 主要原因是我是为嵌入式平台开发的 并且是交叉编译的 这意味着所有源代码都在我的电脑中 我读到了关于 ggdb3标志 其中包含许多额外信息 包
  • 在所有 DataTable 列中查找字符串

    我正在尝试找到一种快速方法来在所有数据表列中查找字符串 跟随不起作用 因为我想在所有列值中搜索 string str whatever foreach DataRow row in dataTable Rows foreach DataCo
  • 从 C# 运行多个 python 脚本

    我希望有人能够在这里帮助我 我对 C 比较陌生 正在尝试执行我在 C winform 应用程序中编写的一些 Python 代码 我想做的是从 winform 中的文本框中输入名称 并让它通过 python 脚本进行处理 并在 winform
  • 如何检测任务栏上的右键单击

    我有一个 C 语言的 Windows 窗体应用程序 它以加载对话框开始 正如预期的那样 该应用程序的按钮会显示在 Windows 任务栏中 我想检测可能对该按钮进行的 右键单击 最终 我希望禁用右键单击或只是让加载对话框重新获得焦点 我看到
  • 无法从 GetSystemTime() 获取毫秒

    我正在尝试打印秒和毫秒分辨率计时 我正在使用GetSystemTime 这是我的代码 GetSystemTime datetime RETAILMSG 1 T Time After Data Sent to USB d d r n date
  • IE8固定标题,可滚动GridView

    我知道有人问过这个话题 但这些帖子都已经过时了 或者在 IE8 上不起作用 简而言之 我们基本上想要在 GridView 中对列标题进行 Excel 样式锁定 我见过几个解决方案 其中一个 jquery css setExpression
  • C#:迭代数据表:Rows、Select() 或 AsEnumerable()

    foreach DataRow row in myDataTable Select foreach DataRow row in myDataTable AsEnumerable foreach DataRow row in myDataT
  • 使用脚本取消设置 PDF 字体

    我正在使用 xhtml2pdf 库自动创建 PDF 几个月前我有过这个问题 https stackoverflow com questions 25203219 xhtml2pdf doesnt embed helvetica 库嵌入了我没
  • C# 中线程之间发送消息

    如何在线程之间发送和接收消息 例如 一种解决方案是共享并发队列 尽管它的名称 并发队列 http msdn microsoft com en us library dd267265 aspx 这将允许您将一个对象从一个线程入队 并让另一个线
  • ListView 内的 TextBox 绑定到对象,双向绑定不起作用

    Edit 好吧 在尝试了无数次但没有成功之后 我创建了一个非常小的 Wpf 应用程序 您可以直接复制此代码 请注意 当您更改文本框中的值并按 测试 按钮时 这些值永远不会更新 我不明白为什么双向绑定不起作用 请帮忙 这是 xaml
  • 画笔和钢笔使用指南

    制作 GDI 画笔和钢笔有多贵 我应该在添加所需的基础上创建它们并将它们包装在 using 中以便快速处理它们 还是应该创建一个类似于 System Drawing Brushes 类的静态类 IMO 它们足够高效 您通常不应该创建在多个方

随机推荐