【问题标题】:WPF application authentication with Google使用 Google 进行 WPF 应用程序身份验证
【发布时间】:2018-06-27 12:14:41
【问题描述】:

我发现了许多不同的 OAuth 解决方案,或者使用一些库,或者使用纯请求 (https://github.com/googlesamples/oauth-apps-for-windows)。

但是,没有一个解决方案看起来像我真正需要的解决方案。目前,我的应用程序使用自己的数据库供用户使用 WCF 服务请求(使用用户名和密码)登录。但是,所有用户的域电子邮件都使用 Google 帐户创建,所以我想添加另一个按钮“使用 Google 登录”,这将确保用户也可以使用他的 Google 电子邮件-密码对登录。我不需要返回的令牌以供进一步使用等。

在 WPF/C# 桌面应用程序中实现此功能的最简单方法是什么?

【问题讨论】:

  • 这是来自链接示例的随机提示:“如果您对这些示例或 Google OAuth 有任何疑问,请使用 google-oauth 标签在 Stack Overflow 上提问”。所以基本上,你选择了错误的标签来获得谷歌对此事的支持(但是,我不知道正确的标签是否会吸引一些质量支持)。
  • 我使用了这里描述的方法:developers.google.com/api-client-library/dotnet/guide/aaa_oauth。有关如何从桌面应用程序执行此操作的信息,请参阅“已安装的应用程序”部分。
  • 您需要获取 oauth 授权码(通过嵌入式\外部浏览器),然后将此代码而不是用户名\密码对发布到您的服务器。然后在服务器上交换 id 令牌的代码,其中包含有关用户的信息(包括电子邮件)。现在您可以使用此电子邮件对用户进行身份验证并像往常一样建立会话,而无需明确的用户名或密码。

标签: c# wpf oauth-2.0 google-oauth google-authentication


【解决方案1】:

这是一个自给自足、无第三方的 WPF 示例,它执行 Google 身份验证(它也可以轻松转换为 winform)。

如果您运行它,您将不会被记录下来,应用程序会显示一个按钮。如果您单击该按钮,嵌入式网络浏览器控件将通过 Google 身份验证运行。 一旦您通过身份验证,该应用将仅显示您的姓名,如 Google 返回的那样。

请注意,它基于此处的 Google 官方示例:https://github.com/googlesamples/oauth-apps-for-windows,但它使用嵌入式浏览器而不是生成外部浏览器(以及其他一些差异)。

XAML 代码:

  <Window x:Class="GoogleAuth.MainWindow"
          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
          Title="MainWindow" Height="750" Width="525">
      <Window.Resources>
          <BooleanToVisibilityConverter x:Key="btv" />
      </Window.Resources>
      <Grid>
          <DockPanel Visibility="{Binding State.IsSigned, Converter={StaticResource btv}}">
              <Label>You are signed as:</Label>
              <Label Content="{Binding State.Token.Name}" />
          </DockPanel>
          <Grid Visibility="{Binding State.IsNotSigned, Converter={StaticResource btv}}">
              <Grid.RowDefinitions>
                  <RowDefinition Height="23" />
                  <RowDefinition Height="*" />
              </Grid.RowDefinitions>
              <Button Click="Button_Click">Click to Sign In</Button>
              <WebBrowser Grid.Row="1" x:Name="Wb" Height="Auto" />
          </Grid>
      </Grid>
  </Window>

C#代码:

  using System;
  using System.ComponentModel;
  using System.IO;
  using System.Net;
  using System.Net.Sockets;
  using System.Runtime.Serialization;
  using System.Runtime.Serialization.Json;
  using System.Security.Cryptography;
  using System.Text;
  using System.Threading;
  using System.Windows;
  using System.Windows.Threading;

  namespace GoogleAuth
  {
      public partial class MainWindow : Window
      {
          public MainWindow()
          {
              InitializeComponent();
              State = new OAuthState();
              DataContext = this;
          }

          public OAuthState State { get; }

          private void Button_Click(object sender, RoutedEventArgs e)
          {
              var thread = new Thread(HandleRedirect);
              thread.Start();
          }

          private async void HandleRedirect()
          {
              State.Token = null;

              // for example, let's pretend I want also want to have access to WebAlbums
              var scopes = new string[] { "https://picasaweb.google.com/data/" };

              var request = OAuthRequest.BuildLoopbackRequest(scopes);
              var listener = new HttpListener();
              listener.Prefixes.Add(request.RedirectUri);
              listener.Start();

              // note: add a reference to System.Windows.Presentation and a 'using System.Windows.Threading' for this to compile
              await Dispatcher.BeginInvoke(() =>
              {
                  Wb.Navigate(request.AuthorizationRequestUri);
              });

              // here, we'll wait for redirection from our hosted webbrowser
              var context = await listener.GetContextAsync();

              // browser has navigated to our small http servern answer anything here
              string html = string.Format("<html><body></body></html>");
              var buffer = Encoding.UTF8.GetBytes(html);
              context.Response.ContentLength64 = buffer.Length;
              var stream = context.Response.OutputStream;
              var responseTask = stream.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) =>
              {
                  stream.Close();
                  listener.Stop();
              });

              string error = context.Request.QueryString["error"];
              if (error != null)
                  return;

              string state = context.Request.QueryString["state"];
              if (state != request.State)
                  return;

              string code = context.Request.QueryString["code"];
              State.Token = request.ExchangeCodeForAccessToken(code);
          }
      }

      // state model
      public class OAuthState : INotifyPropertyChanged
      {
          public event PropertyChangedEventHandler PropertyChanged;

          private OAuthToken _token;
          public OAuthToken Token
          {
              get => _token;
              set
              {
                  if (_token == value)
                      return;

                  _token = value;
                  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Token)));
                  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsSigned)));
                  PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsNotSigned)));
              }
          }

          public bool IsSigned => Token != null && Token.ExpirationDate > DateTime.Now;
          public bool IsNotSigned => !IsSigned;
      }

      // This is a sample. Fille information (email, etc.) can depend on scopes
      [DataContract]
      public class OAuthToken
      {
          [DataMember(Name = "access_token")]
          public string AccessToken { get; set; }

          [DataMember(Name = "token_type")]
          public string TokenType { get; set; }

          [DataMember(Name = "expires_in")]
          public int ExpiresIn { get; set; }

          [DataMember(Name = "refresh_token")]
          public string RefreshToken { get; set; }

          [DataMember]
          public string Name { get; set; }

          [DataMember]
          public string Email { get; set; }

          [DataMember]
          public string Picture { get; set; }

          [DataMember]
          public string Locale { get; set; }

          [DataMember]
          public string FamilyName { get; set; }

          [DataMember]
          public string GivenName { get; set; }

          [DataMember]
          public string Id { get; set; }

          [DataMember]
          public string Profile { get; set; }

          [DataMember]
          public string[] Scopes { get; set; }

          // not from google's response, but we store this
          public DateTime ExpirationDate { get; set; }
      }

      // largely inspired from
      // https://github.com/googlesamples/oauth-apps-for-windows
      public sealed class OAuthRequest
      {
          // TODO: this is a sample, please change these two, use your own!
          private const string ClientId = "581786658708-elflankerquo1a6vsckabbhn25hclla0.apps.googleusercontent.com";
          private const string ClientSecret = "3f6NggMbPtrmIBpgx-MK2xXK";

          private const string AuthorizationEndpoint = "https://accounts.google.com/o/oauth2/v2/auth";
          private const string TokenEndpoint = "https://www.googleapis.com/oauth2/v4/token";
          private const string UserInfoEndpoint = "https://www.googleapis.com/oauth2/v3/userinfo";

          private OAuthRequest()
          {
          }

          public string AuthorizationRequestUri { get; private set; }
          public string State { get; private set; }
          public string RedirectUri { get; private set; }
          public string CodeVerifier { get; private set; }
          public string[] Scopes { get; private set; }

          // https://developers.google.com/identity/protocols/OAuth2InstalledApp
          public static OAuthRequest BuildLoopbackRequest(params string[] scopes)
          {
              var request = new OAuthRequest
              {
                  CodeVerifier = RandomDataBase64Url(32),
                  Scopes = scopes
              };

              string codeChallenge = Base64UrlEncodeNoPadding(Sha256(request.CodeVerifier));
              const string codeChallengeMethod = "S256";

              string scope = BuildScopes(scopes);

              request.RedirectUri = string.Format("http://{0}:{1}/", IPAddress.Loopback, GetRandomUnusedPort());
              request.State = RandomDataBase64Url(32);
              request.AuthorizationRequestUri = string.Format("{0}?response_type=code&scope=openid%20profile{6}&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
                  AuthorizationEndpoint,
                  Uri.EscapeDataString(request.RedirectUri),
                  ClientId,
                  request.State,
                  codeChallenge,
                  codeChallengeMethod,
                  scope);

              return request;
          }

          // https://developers.google.com/identity/protocols/OAuth2InstalledApp Step 5: Exchange authorization code for refresh and access tokens
          public OAuthToken ExchangeCodeForAccessToken(string code)
          {
              if (code == null)
                  throw new ArgumentNullException(nameof(code));

              string tokenRequestBody = string.Format("code={0}&redirect_uri={1}&client_id={2}&code_verifier={3}&client_secret={4}&scope=&grant_type=authorization_code",
                  code,
                  Uri.EscapeDataString(RedirectUri),
                  ClientId,
                  CodeVerifier,
                  ClientSecret
                  );

              return TokenRequest(tokenRequestBody, Scopes);
          }

          // this is not used in this sample, but can be used to refresh a token from an old one
          // https://developers.google.com/identity/protocols/OAuth2InstalledApp Refreshing an access token
          public OAuthToken Refresh(OAuthToken oldToken)
          {
              if (oldToken == null)
                  throw new ArgumentNullException(nameof(oldToken));

              string tokenRequestBody = string.Format("refresh_token={0}&client_id={1}&client_secret={2}&grant_type=refresh_token",
                  oldToken.RefreshToken,
                  ClientId,
                  ClientSecret
                  );

              return TokenRequest(tokenRequestBody, oldToken.Scopes);
          }

          private static T Deserialize<T>(string json)
          {
              if (string.IsNullOrWhiteSpace(json))
                  return default(T);

              return Deserialize<T>(Encoding.UTF8.GetBytes(json));
          }

          private static T Deserialize<T>(byte[] json)
          {
              if (json == null || json.Length == 0)
                  return default(T);

              using (var ms = new MemoryStream(json))
              {
                  return Deserialize<T>(ms);
              }
          }

          private static T Deserialize<T>(Stream json)
          {
              if (json == null)
                  return default(T);

              var ser = CreateSerializer(typeof(T));
              return (T)ser.ReadObject(json);
          }

          private static DataContractJsonSerializer CreateSerializer(Type type)
          {
              if (type == null)
                  throw new ArgumentNullException(nameof(type));

              var settings = new DataContractJsonSerializerSettings
              {
                  DateTimeFormat = new DateTimeFormat("yyyy-MM-dd'T'HH:mm:ss.fffK")
              };
              return new DataContractJsonSerializer(type, settings);
          }

          // https://stackoverflow.com/questions/223063/how-can-i-create-an-httplistener-class-on-a-random-port-in-c/
          private static int GetRandomUnusedPort()
          {
              var listener = new TcpListener(IPAddress.Loopback, 0);
              listener.Start();
              var port = ((IPEndPoint)listener.LocalEndpoint).Port;
              listener.Stop();
              return port;
          }

          private static string RandomDataBase64Url(int length)
          {
              using (var rng = new RNGCryptoServiceProvider())
              {
                  var bytes = new byte[length];
                  rng.GetBytes(bytes);
                  return Base64UrlEncodeNoPadding(bytes);
              }
          }

          private static byte[] Sha256(string text)
          {
              using (var sha256 = new SHA256Managed())
              {
                  return sha256.ComputeHash(Encoding.ASCII.GetBytes(text));
              }
          }

          private static string Base64UrlEncodeNoPadding(byte[] buffer)
          {
              string b64 = Convert.ToBase64String(buffer);
              // converts base64 to base64url.
              b64 = b64.Replace('+', '-');
              b64 = b64.Replace('/', '_');
              // strips padding.
              b64 = b64.Replace("=", "");
              return b64;
          }

          private static OAuthToken TokenRequest(string tokenRequestBody, string[] scopes)
          {
              var request = (HttpWebRequest)WebRequest.Create(TokenEndpoint);
              request.Method = "POST";
              request.ContentType = "application/x-www-form-urlencoded";
              byte[] bytes = Encoding.ASCII.GetBytes(tokenRequestBody);
              using (var requestStream = request.GetRequestStream())
              {
                  requestStream.Write(bytes, 0, bytes.Length);
              }

              var response = request.GetResponse();
              using (var responseStream = response.GetResponseStream())
              {
                  var token = Deserialize<OAuthToken>(responseStream);
                  token.ExpirationDate = DateTime.Now + new TimeSpan(0, 0, token.ExpiresIn);
                  var user = GetUserInfo(token.AccessToken);
                  token.Name = user.Name;
                  token.Picture = user.Picture;
                  token.Email = user.Email;
                  token.Locale = user.Locale;
                  token.FamilyName = user.FamilyName;
                  token.GivenName = user.GivenName;
                  token.Id = user.Id;
                  token.Profile = user.Profile;
                  token.Scopes = scopes;
                  return token;
              }
          }

          private static UserInfo GetUserInfo(string accessToken)
          {
              var request = (HttpWebRequest)WebRequest.Create(UserInfoEndpoint);
              request.Method = "GET";
              request.Headers.Add(string.Format("Authorization: Bearer {0}", accessToken));
              var response = request.GetResponse();
              using (var stream = response.GetResponseStream())
              {
                  return Deserialize<UserInfo>(stream);
              }
          }

          private static string BuildScopes(string[] scopes)
          {
              string scope = null;
              if (scopes != null)
              {
                  foreach (var sc in scopes)
                  {
                      scope += "%20" + Uri.EscapeDataString(sc);
                  }
              }
              return scope;
          }

          // https://developers.google.com/+/web/api/rest/openidconnect/getOpenIdConnect
          [DataContract]
          private class UserInfo
          {
              [DataMember(Name = "name")]
              public string Name { get; set; }

              [DataMember(Name = "kind")]
              public string Kind { get; set; }

              [DataMember(Name = "email")]
              public string Email { get; set; }

              [DataMember(Name = "picture")]
              public string Picture { get; set; }

              [DataMember(Name = "locale")]
              public string Locale { get; set; }

              [DataMember(Name = "family_name")]
              public string FamilyName { get; set; }

              [DataMember(Name = "given_name")]
              public string GivenName { get; set; }

              [DataMember(Name = "sub")]
              public string Id { get; set; }

              [DataMember(Name = "profile")]
              public string Profile { get; set; }

              [DataMember(Name = "gender")]
              public string Gender { get; set; }
          }
      }
  }

此代码使用嵌入式 Internet Explorer 控件,但由于 Google 不支持旧的 Internet Explorer 版本,您可能还需要添加一些代码来使用 IE 的兼容性功能,如下所述,例如:https://stackoverflow.com/a/28626667/403671

您可以将该代码放在 App.xaml.cs 中,如下所示:

public partial class App : Application
{
    public App()
    {
        // use code from here: https://stackoverflow.com/a/28626667/403671
        SetWebBrowserFeatures();
    }
}

请注意,网络浏览器将显示的内容完全取决于 Google,并且可能会因 cookie、语言等环境而有很大差异。

【讨论】:

  • 您不需要使用 http 监听器,因为您使用的是嵌入式浏览器。您还需要在服务器上交换代码以获取访问令牌(至少在 OP 的情况下)。也许这只是一个例子,但没有在任何地方说明。此外,由于 OP 需要身份验证 - 最好使用 open id connect (您可能已经在使用它,很难说)。它将返回“id_token”以及(对 OP 无用)访问令牌,并且 id 令牌将包含 OP 需要的所有信息。
  • @evk - 1)我们需要监听,因为浏览器将被重定向到 localhost(每个应用程序配置,我使用的与 google 示例中的相同),2)阅读代码,3)代码在没有打开 id 的情况下工作正常,我不明白我为什么要打扰它。
  • 1.正如您自己所说,他们使用外部浏览器。您正在使用嵌入式并且可以拦截重定向到任何端点并从那里提取代码。我认为使用不必要的 HttpListener 您的应用程序需要管理员权限(或配置权限),但我不确定。 2. 我阅读并没有看到您声明此步骤应在 OPs 服务器上执行,而不是在任何地方的客户端上执行。 3. 它是出于某种原因创建的,供人们使用它进行身份验证,这是 OP 所需要的,那么为什么要使用其他不是为此而设计的工具呢?使用正确的方法几乎不费吹灰之力。
  • @SimonMourier - 首先感谢您的回答。但基本上我想要的是更简单的东西。我只想让谷歌向我发送一些布尔值,指示身份验证是否成功。另外,嵌入式浏览器在这里不是一个巨大的安全风险吗?
  • @Sherlock - 不,检查 SetWebBrowserFeatures 代码,它使用 HKCU 编写。至于嵌入IE,你可以更改代码并使用任何你喜欢的能够嵌入的浏览器。我只是认为 IE 更容易和集成。我相信其他人使用 CEF。
【解决方案2】:

我认为您忽略了 OAuth 是什么,首先阅读 thishere 是对流程的简单描述。

这将确保用户也可以使用他的谷歌电子邮件密码对登录,我不需要返回的令牌以供进一步使用

没有用于用户/通行证验证的 api,令牌是 OAuth 的重点。如果您确实使用谷歌注册了您的应用程序,则拥有现有电子邮件的用户应该能够登录。此外,我认为事先验证用户/通行证没有任何意义,只需添加按钮。

【讨论】:

  • 我的意思是我永远不会使用返回的令牌,我只会检查身份验证是否成功,在这种情况下进行应用程序会话。
  • @Gab 只是从安全的角度考虑它。使用 web 视图和令牌,您的应用程序没有关于用户凭据的信息。谷歌不想让你知道。因此他们提供了一种基于令牌识别用户的方法。
  • 此外,Google 会首先询问您的用户,他是否真的信任您的应用,并列出在积极选择时授予您的应用的权限。
猜你喜欢
  • 2011-06-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-03
  • 1970-01-01
  • 1970-01-01
  • 2017-03-11
  • 2021-07-30
相关资源
最近更新 更多