如何在WPF中使用CEFSharp浏览器登录Google?

时间:2020-06-22 02:57:59

标签: oauth-2.0 google-signin cefsharp

我知道这是一个非常基本的问题,但是我正在使用CEFSharp在wpf中创建一个浏览器(我几乎完成了),但是在某些帐户上登录google却遇到了麻烦。我收到一条“此浏览器或应用可能不安全。” 消息。现在,我已经研究过使用OAuth获得令牌来访问和使用Google的某些功能,而不仅仅是获得整体登录的方式。我只希望用户能够登录Google,就像在Chrome浏览器上一样(当然没有Sync选项)。

enter image description here enter image description here enter image description here

我在Google控制台中注册了我的应用程序,并收到了ClientId和ClientSecret,并创建了一个窗口,该窗口在尝试获取令牌的google页面上单击登录按钮时被调用,但是当用户键入时在他们的电子邮件中,他们收到相同的消息(请参见下文)。

enter image description here enter image description here

我不确定是否可以使用API​​或可以让用户通过浏览器登录google而不必调用Google Chrome浏览器本身的东西,因为这违背了浏览器自我实现的目的足够。我已经为此努力了好几天。有人可以帮忙吗?

以下是我的Google登录窗口的xaml:

<Window x:Class="MyProject.Windows.GoogleLoginWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:wpf="clr-namespace:CefSharp.Wpf;assembly=CefSharp.Wpf"
    xmlns:local="clr-namespace:NP_Browser.Windows"
    mc:Ignorable="d"
    Title="Google Login Window" Height="450" Width="800" Icon="../Images/NPBrowserLogo.ico">
<Window.Resources>
    <BooleanToVisibilityConverter x:Key="btv" />
</Window.Resources>
<Grid>
    <DockPanel Visibility="{Binding State.IsSigned, Converter={StaticResource btv}}">
        <Label Content="{Binding State.Token.Name}" />
    </DockPanel>
    <Grid Visibility="{Binding State.IsNotSigned, Converter={StaticResource btv}}">
        <wpf:ChromiumWebBrowser x:Name="Wb" FontSize="16"/>
    </Grid>
</Grid>

以下是我的“ Google登录”窗口的代码隐藏:

namespace MyProject.Windows

{ 公共局部类GoogleLoginWindow:Window { 公共GoogleLoginWindow() { InitializeComponent(); 状态=新的OAuthState(); DataContext = this; 最上层=真; var thread = new Thread(HandleRedirect); thread.Start(); }

    public OAuthState State { get; }

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

        var request = OAuthRequest.BuildLoopbackRequest();
        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.Address = request.AuthorizationRequestUri;
        });

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

        // browser has navigated to our small http server 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
{
    private const string ClientId = "My-Client-Id";
    private const string ClientSecret = "My-Client-Secret";

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

}

2 个答案:

答案 0 :(得分:0)

您正在尝试登录Web视图和Google blocks that。 不幸的是,用于桌面应用程序的OAuth非常棘手。我有一些visual blog posts和一个代码示例,您可以运行这些代码来了解行为:

  • 通过调用系统浏览器登录
  • 通过启动环回Web服务器来接收响应
  • 或通过私有URI方案(我的偏爱)接收响应

我的样本以Electron / Javascript编码。但是,下面的C#代码示例随IdentityModel安全库一起提供,我建议为您的应用使用此库:

答案 1 :(得分:0)

Google 阻止从嵌入式浏览器登录它,以对抗中间人攻击。

相关问题