使用 Google 进行 WPF 应用程序身份验证

2023-11-26

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

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

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


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

如果您运行它,您将不会被记录,并且应用程序将向您显示一个按钮。如果您单击该按钮,嵌入式网络浏览器控件将通过 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# code:

  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、语言等环境而有很大差异。

enter image description here

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

使用 Google 进行 WPF 应用程序身份验证 的相关文章

随机推荐

  • Firestore 不保存文本区域中的换行符

    我正在使用带有 Vue v model 的文本区域并将其保存在 firestore 集合中 但换行符在数据库中消失了 如何解决 div class form group div
  • 从 m 大小的列索引向量创建一个由 0 和 1 组成的 m × n 矩阵

    我有一个m整数维向量 范围从 1 到n 这些整数是列索引m n matrix 我想创建一个m n由 0 和 1 组成的矩阵 其中m 第 行 指定的列中有一个 1m我的向量中的第一个值 Example my vector 3 dimensio
  • 为什么 CoUninitialize 会导致退出时出错?

    我正在开发一个 C 应用程序来从 Excel 文件中读取一些数据 我已经成功了 但我对其中一部分感到困惑 这是代码 简化为仅读取第一个单元格 Mostly copied from http www codeproject com KB wt
  • 在windows C中加载dll进行跨平台设计

    我写了一个为linux平台设计的c代码 现在 我想让它跨平台 以便也可以在 Windows 中使用 在我的代码中 我 dlopen 一个 so 文件并利用其中的函数 下面是我的代码的样子 但我刚刚发现 在windows中 加载和使用动态库的
  • 关于HotSpot的动态反优化

    我在读 深入Scala 这本书的时候 提到HotSpot编译器有几个重要的特性 其中之一就是 动态去优化 它是确定优化是否有效的能力not 事实上 提高性能并撤消该优化 允许应用其他优化 看来HotSpot会尝试各种 优化 并选择其中最好的
  • 空闲和蟒蛇

    我曾经有过idle 然后我下载了Anaconda并通过那里闲置地打开 我已经有一段时间没有使用idle了 但最近才去打开它并再次使用它 然而 我的计算机似乎不再空闲 据我了解 我仍然可以通过 Anaconda 空闲 但我忘记了如何操作 有没
  • 如何在WebBrowser控件中注入Javascript?

    我试过这个 string newScript textBox1 Text HtmlElement head browserCtrl Document GetElementsByTagName head 0 HtmlElement scrip
  • 为什么 Android 中不提供大于 SECONDS 的 Java.util.concurrent.TimeUnit 类型?

    I miss MINUTES HOURS DAYS 存在于文档自 API 级别 1 起 我使用应用程序的第 7 或 2 1 版本 我读过了这个问题 其中也指出了这个失误 尽管 它不在问题本身中 但作为解决方案 仅提出了自己的计算 我并不懒惰
  • 使用 .htaccess 与 php5.4 内置服务器

    在我的开发环境中 我使用php5 4的web内置Web服务器 但似乎 htaccess无法正常工作 我找不到该服务器的文档 有人可以告诉我是否可以像apache一样使用htaccess和mod rewrite 非常感谢 正如我的评论中提到的
  • 站点匹配查询不存在

    该网站运行良好 直到我在应用程序上单击 注销 之后 该网站会给我这个错误 不存在于 login 站点匹配查询不存在 我到处搜索 得到的唯一解决方案与设置站点框架 SITE ID 等有关 我认为我计算机上的这些项目都很好 但我找不到演练 指南
  • 如何从 Hex NSString 获取十进制 int

    我想从十六进制编码的字符串中获取十进制值 例如 A gt 10 B gt 11 等 我编码如下 NSString tempNumber tempNumber number text NSScanner scanner NSScanner s
  • 如何通过curl发布数组值?

    我喜欢测试一个 API 后端 其设计如下例所示 http localhost 3000 api v1 shops 1 json JSON 响应 id 1 name Supermarket products fruit eggs 这是对应的模
  • 为什么在 JavaScript 中使用 getter 和 setter?

    我知道 JavaScript 中的 getter 和 setter 是如何工作的 我不明白的是 当我们可以使用普通函数得到相同的结果时 为什么我们需要它们 考虑以下代码 var person firstName Jimmy lastName
  • 如何在 django 中集成 Foundation 5

    我想开始在 django 项目中使用 Foundation 5 我的疑问是如何设置 Foundation 项目的文件夹 Foundation 现在使用 Bower 来处理 js 依赖项 我认为将 Foundation 5 设置到 djang
  • 如何在使用较少内存的情况下在单列中存储多个值?

    我有一张桌子users其中 1 列存储用户的 角色 我们可以分配多重角色给特定用户 然后我想将角色 ID 存储在 角色 列中 但是如何才能以易于使用的方式将多个值存储到单个列中以节省内存呢 例如 使用逗号分隔字段进行存储并不容易并且会占用内
  • 这段嵌套 for 循环反复将计数器加倍的代码的复杂性是多少?

    在书里编程面试曝光它说下面的程序的复杂度是 O N 但我不明白这是怎么可能的 有人可以解释这是为什么吗 int var 2 for int i 0 i lt N i for int j i 1 j lt N j 2 var var 你需要一
  • PIL 解码器 jpeg 在 ubuntu x64 上不可用,

    我知道这个问题看起来像是重复的 但我遵循了许多有关如何正确安装 PIL 的在线说明 但没有一个起作用 我已经尝试了一切 Python 图像库失败并显示消息 解码器 JPEG 不可用 PIL没有成功 当我运行 sudo pip install
  • Keras模型训练内存泄漏

    我是 Keras Tensorflow Python 的新手 我正在尝试构建一个供个人使用 未来学习的模型 我刚刚开始使用 python 并想出了这段代码 在视频和教程的帮助下 我的问题是 我的 Python 内存使用量在每个时期甚至在构建
  • 基于 CSV 的 Spark DataFrame 查询是否比基于 Parquet 的 Spark DataFrame 查询更快?

    我必须使用 Spark 从 HDFS 加载 CSV 文件到DataFrame 我想知道由 CSV 文件支持的 DataFrame 与由 parquet 文件支持的 DataFrame 是否有 性能 改进 查询速度 通常 我将如下所示的 CS
  • 使用 Google 进行 WPF 应用程序身份验证

    我发现了许多不同的 OAuth 解决方案以及一些库或纯请求 https github com googlesamples oauth apps for windows 然而 没有一个解决方案看起来像我真正需要的 目前 我的应用程序使用自己的