使用 Google 进行 WPF 应用程序身份验证 [英] WPF application authentication with Google

查看:29
本文介绍了使用 Google 进行 WPF 应用程序身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我发现了许多不同的 OAuth 解决方案,以及一些库或纯请求(

I have found many different solutions with OAuth and either with some libraries or with pure requests (https://github.com/googlesamples/oauth-apps-for-windows).

However, none of the solutions looks like the one that I really need. Currently, my application uses its own database for users to log in using WCF service request (with username and password). However, all users have their domain e-mail created with Google accounts, so I want to add another button "Sign In with Google", which will just make sure that user can also login with his Google e-mail-password pair. I don't need a returned token for the further use etc.

What is the simplest way to achieve this functionality in WPF/C# desktop application?

解决方案

Here is a self-sufficient, 3rd party-free, WPF sample that does Google auth (it could be easily converted to winforms as well).

If you run it you won't be logged and the app will show you a button. If you click that button, an embedded webbrowser control will run though the Google auth. Once you are authenticated, the app will just display your name as returned by Google.

Note it's based on official Google's sample here: https://github.com/googlesamples/oauth-apps-for-windows but it uses an embedded browser instead of spawning an external one (and few other differences).

XAML code:

  <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; }
          }
      }
  }

This code uses an embedded Internet Explorer control, but since Google doesn't support old Internet Explorer versions, it's likely that you also need to add some code to play with IE's compatibility features as described here for example: https://stackoverflow.com/a/28626667/403671

You can put that code in App.xaml.cs, like this:

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

Note what the webbrowser will display depends entirely on Google, and it can considerably vary depending on the environment like cookies, language, etc.

这篇关于使用 Google 进行 WPF 应用程序身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆