无法使用 Web API 中的外部身份验证提供程序创建新用户 (Visual Studio 2013 Update 2)



问题:

我正在尝试使外部身份验证与默认的 Web API 模板项目一起使用。我使用以下说明添加了对外部身份验证服务(FB/Google/Microsoft(的支持:http://www.asp.net/web-api/overview/security/external-authentication-services

仅供记录,我能够使外部身份验证与默认的SPA模板项目一起使用。此外,新的本地用户创建工作正常。

一旦我尝试使用我的客户端应用程序(基于 WPF(使用外部提供程序(例如 FB(注册用户,就会出现问题。

作为

记录,我使用这两篇文章作为起点:http://leastprivilege.com/2013/11/26/dissecting-the-web-api-individual-accounts-templatepart-3-external-accounts/和线程#21065648在堆栈溢出。他们真的帮助我理解了整个逻辑。

以下是我已完成的步骤的简短概述:两个窗口,主窗口和一个用于外部身份验证提供程序的窗口,带有嵌入式 Web 浏览器流程:2.1. 用户打开应用,出现主窗口2.2. 用户单击按钮以获取所有支持的外部身份验证提供程序的列表,以下是代码中发生的情况:

            var client = new HttpClient();
            client.BaseAddress = new Uri(baseAddress);
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            HttpResponseMessage loginResponse = await client.GetAsync("api/Account/ExternalLogins?returnUrl=%2F&generateState=true");
            if (loginResponse.IsSuccessStatusCode)
            {
            var externalLoginProviders = await loginResponse.Content.ReadAsAsync<IEnumerable<AuthenticationProvider>>();
            // cleaning resources
            client.Dispose();
            loginResponse.Dispose();
            // obtained data is sent to UI

这将导致外部身份验证提供程序列表,其中包含名称、URL 和状态。URL是相对的,用作重定向到实际提供商(例如FB或Google(。

2.3. 一旦填充了外部身份验证提供商的列表,用户就可以输入他/她的电子邮件,然后单击"登录"按钮,这将导致以下内容:

在第二个窗口中,使用嵌入式 Web 浏览器,后者将导航到上一步中获取的提供的 URL。如果用户成功登录到选定的提供商(例如Facebook(,并同意向我的应用程序(Facebook应用程序(提供必要的权限(个人资料(,则用户将导航回我们的网站,形式如下:

            (our base url) /#access_token=XXX&token_type=bearer&expires=YYY

然后解析此地址,并以结构化形式保存地址参数(令牌等(以供以后使用。

之后,我立即转到 api/UserInfo 以了解用户是否已使用此外部身份验证提供程序登录:

            var client = new HttpClient();
            client.BaseAddress = new Uri(baseAddress);
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
            // setting Bearer Token obtained from Auth provider
            client.SetBearerToken(result.AccessToken);
            // calling /api/Account/UserInfo
            var userInfoResponse = await client.GetAsync("api/Account/UserInfo");
            var userInfoMessage = userInfoResponse.Content.ReadAsStringAsync().Result;
            var userInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(userInfoMessage);
            // cleaning resources
            client.Dispose();
            userInfoResponse.Dispose();
            if (userInfo.hasRegistered == true)
            {
                // going to login
            }   

在那之后,假设我们的用户没有根据他/她使用给定的外部身份验证提供程序(例如FB(的登录来创建用户,但是,我们执行以下操作:

            var data = new Dictionary<string, string>();
            data.Add("Email", this.externalUserEmailTextBox.Text);
            var registerExternalUrl = new Uri(string.Concat(baseAddress, @"api/Account/RegisterExternal"));
            var client = new HttpClient();
            client.BaseAddress = new Uri(baseAddress);
            // setting Bearer Token obtained from External Authentication provider
            client.SetBearerToken(result.AccessToken);
            var response = client.PostAsync(registerExternalUrl.ToString(), new FormUrlEncodedContent(data)).Result;

此时,应该创建用户(除了取消注释提供程序的应用程序 ID 和 Startup.Auth.cs 中的应用程序秘密行外,我在服务端没有任何更改(。

不幸的是,这根本不会发生。相反,我得到"内部服务器错误",这意味着这一行,

            var info = await Authentication.GetExternalLoginInfoAsync();

带来空值,因此系统无法使用提供的持有者令牌正确进行身份验证。

我不明白为什么。框架中似乎有什么东西坏了,或者我做错了什么......

溶液:

多亏了@berhir,这是解决方案:

  1. 在 MainWindow.xaml.cs 中,定义 cookieContainer

            CookieContainer cookieContainer;
    
  2. 在MainWindow_Loaded事件处理程序中,实例化它:

            // we create new cookie container
            cookieContainer = new CookieContainer();
    
  3. 一旦用户要求应用显示所有外部登录提供程序,就可以使用以下建议的代码实例化 HttpClient,方法是@berhir:

            using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
            using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
            {
                // send request
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                HttpResponseMessage loginResponse = await client.GetAsync("api/Account/ExternalLogins?returnUrl=%2F&generateState=true");
    
  4. 一旦获得外部身份验证提供程序的列表,下一步就是显示第二个窗口,其中包含嵌入式 WebBrowser。在那里,您必须为 cookie 声明两个 WinAPI 调用:

            [DllImport("wininet.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern bool InternetSetCookie(string lpszUrlName, string lbszCookieName, string lpszCookieData);
            [DllImport("wininet.dll", SetLastError = true)]
            public static extern bool InternetGetCookieEx(string url, string cookieName, StringBuilder cookieData, ref int size, Int32 dwFlags, IntPtr lpReserved); 
            // and
            private const Int32 InternetCookieHttponly = 0x2000;
            /// <summary>
            /// Gets the URI cookie container.
            /// </summary>
            /// <param name="uri">The URI.</param>
            /// <returns></returns>
            public static CookieContainer GetUriCookieContainer(Uri uri)
            {
                CookieContainer cookies = null;
                // Determine the size of the cookie
                int datasize = 8192 * 16;
                StringBuilder cookieData = new StringBuilder(datasize);
                if (!InternetGetCookieEx(uri.ToString(), null, cookieData, ref datasize, InternetCookieHttponly, IntPtr.Zero))
                {
                    if (datasize < 0)
                    return null;
                    // Allocate stringbuilder large enough to hold the cookie
                    cookieData = new StringBuilder(datasize);
                    if (!InternetGetCookieEx(uri.ToString(), null, cookieData, ref datasize, InternetCookieHttponly, IntPtr.Zero))
                        return null;
                }
                if (cookieData.Length > 0)
                {
                    cookies = new CookieContainer();
                    cookies.SetCookies(uri, cookieData.ToString().Replace(';', ','));
                }
                return cookies;
            }
    
  5. 此带有嵌入式浏览器的窗口被实例化,构造函数接受多个参数,包括起始 URL(到外部身份验证提供程序(、结束 URL(以"#access_token=..."或"error...",回调,更重要的是,使用原始cookieContainer。我们使用InternetSetCookie WinAPI方法将原始cookieContainer传递给WebBrowser的会话:

        // set cookies
        var cookies = cookieContainer.GetCookies(baseAddress).OfType<Cookie>().ToList();
        foreach (var cookie in cookies)
        {
            InternetSetCookie(startUrl, cookie.Name, cookie.Value); 
        } 
    
  6. 因此,一旦用户成功登录到选定的外部身份验证提供程序(例如Facebook(,使用InternetGetCookieEx WinAPI调用获得的更新cookieContainer(包括在第一个HttpClient调用中设置的cookie,以及在登录到外部身份验证提供程序后立即在WebBrowser中设置的cookie(将通过回调发送回MainWindow.xaml.cs:

            if (this.callback != null)
            {
                var cookies = GetUriCookieContainer(e.Uri);
                this.callback(new AuthResult(e.Uri, this.providerName, cookies));
            }
    
  7. 在那里,我们向 api/Account/UserInformation 和 api/Account/RegisterExternal 发出两个新请求:

            this.cookieContainer = result.CookieContainer; // where result is AuthResult containing the cookies obtained from WebBrowser's session
            using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
            using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
            {
                // send request
                client.DefaultRequestHeaders.Accept.Clear();
                client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
                // setting Bearer Token obtained from Auth provider
                client.SetBearerToken(result.AccessToken);
                // calling /api/Account/UserInfo
                var userInfoResponse = await client.GetAsync("api/Account/UserInfo");
                var userInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<UserInfo>(userInfoMessage);
            if (userInfo.hasRegistered == false)
            {
                var data = new Dictionary<string, string>();
                data.Add("Email", this.externalUserEmailTextBox.Text);
                var registerExternalUrl = new Uri(string.Concat(baseAddress, @"api/Account/RegisterExternal"));
                var content = new FormUrlEncodedContent(data);
                var response = client.PostAsync(registerExternalUrl.ToString(), content).Result;
                // obtaining content
                var responseContent = response.Content.ReadAsStringAsync().Result;
                if (response != null && response.IsSuccessStatusCode)
                {
                    MessageBox.Show("New user registered, with " + result.ProviderName + " account");
                }
    

所以,我们开始了。Cookie 在整个生命周期内使用,从第一个 HttpClient 请求到我们使用 api/account/registerExternal 注册新用户的最后一刻。

看来你忘了处理饼干。最简单的方法是对所有请求使用相同的 HttpClient 实例,或者您可以使用同一个 CookieContainer。

var cookieContainer = new CookieContainer();
using (var handler = new HttpClientHandler() { CookieContainer = cookieContainer })
using (var client = new HttpClient(handler) { BaseAddress = baseAddress })
{
    // send request
}

相关内容

最新更新