C# Bing广告OAuth自动化仅使用.NET?

C# Bing广告OAuth自动化仅使用.NET?,c#,oauth,oauth-2.0,bing-api,C#,Oauth,Oauth 2.0,Bing Api,如何登录到Microsoft Live(使用.NET WebClient?)并自动执行OAuth过程,以获取令牌以进行Bing Ads API调用 我的问题类似于。但是,我正在使用Bing Ads超级管理员帐户的上下文构建(C#,.NET 4.5.2)一个无头Windows服务,该帐户链接到多个其他Bing Ads帐户。其思想是进行身份验证,获取身份验证位,然后在凌晨3:00使用这些位进行调用。一些帐户“竞争”,例如,A组不应该看到B组的数据,因此让应用程序为每个人获取数据,并在一夜之间进行过滤

如何登录到Microsoft Live(使用.NET WebClient?)并自动执行OAuth过程,以获取令牌以进行Bing Ads API调用

我的问题类似于。但是,我正在使用Bing Ads超级管理员帐户的上下文构建(C#,.NET 4.5.2)一个无头Windows服务,该帐户链接到多个其他Bing Ads帐户。其思想是进行身份验证,获取身份验证位,然后在凌晨3:00使用这些位进行调用。一些帐户“竞争”,例如,A组不应该看到B组的数据,因此让应用程序为每个人获取数据,并在一夜之间进行过滤和分发可以解决许多业务问题

我担心的是,如果Live遇到问题,或者我们的应用程序由于任何原因长时间关闭,我们将不得不手动重新验证以再次获取数据。凭证的维护和管理现在是额外的开销(这是针对企业环境的),必须采用intranet网站/页面的形式,以允许初级/非初级人员在需要时完成工作(不要忘记测试和文档)。相比之下,谷歌提供了一个选项,可以为需要以完全自动化方式工作的组使用密钥对。Twitter的OAuth2实现似乎可以在没有GUI登录的情况下实现自动化。似乎其他必应服务(eg)也可以通过WebClient实现自动化

我已经有了Microsoft帐户名和密码,并且在Bing Ads应用程序GUI中设置了“local mydomain.com”的回调URL(并且为local mydomain.com设置了HOSTS条目)

微软似乎可以工作,但它自动化了MS Web浏览器控件,要求用户在GUI中输入凭据,然后给出令牌。将超级管理员帐户提供给用户来执行此操作不是一个选项。期望用户在凌晨3:00起床进行身份验证以上传/下载数据不是一个选项。期望用户获得服务器场中服务器的桌面访问权以“运行某些东西”不是一个选项

所有的OAuth想法都受到赞赏

谢谢

以下是启动代码:

 partial class OAuthForm : Form
    {
        private static OAuthForm _form;
        private static WebBrowser _browser;

        private static string _code;
        private static string _error;

        // When you register your application, the Client ID is provisioned.

        private const string ClientId = "000redacted000";

        // Request-related URIs that you use to get an authorization code, 
        // access token, and refresh token.

        private const string AuthorizeUri = "https://login.live.com/oauth20_authorize.srf"; 
        private const string TokenUri = "https://login.live.com/oauth20_token.srf"; 
        private const string DesktopUri = "https://login.live.com/oauth20_desktop.srf"; 
        private const string RedirectPath = "/oauth20_desktop.srf";
        private const string ConsentUriFormatter = "{0}?client_id={1}&scope=bingads.manage&response_type=code&redirect_uri={2}";
        private const string AccessUriFormatter = "{0}?client_id={1}&code={2}&grant_type=authorization_code&redirect_uri={3}";
        private const string RefreshUriFormatter = "{0}?client_id={1}&grant_type=refresh_token&redirect_uri={2}&refresh_token={3}";

        // Constructor

        public OAuthForm(string uri)
        {
            InitializeForm(uri);
        }

        [STAThread]
        static void Main()
        {
            // Create the URI to get user consent. Returns the authorization
            // code that is used to get an access token and refresh token.

            var uri = string.Format(ConsentUriFormatter, AuthorizeUri, ClientId, DesktopUri);

            _form = new OAuthForm(uri);

            // The value for "uri" is 
            // https://login.live.com/oauth20_authorize.srf?client_id=000redacted000&scope=bingads.manage&response_type=code&redirect_uri=https://login.live.com/oauth20_desktop.srf



            _form.FormClosing += form_FormClosing;
            _form.Size = new Size(420, 580);

            Application.EnableVisualStyles();

            // Launch the form and make an initial request for user consent.
            // For example POST /oauth20_authorize.srf?
            //                 client_id=<ClientId>
            //                 &scope=bingads.manage
            //                 &response_type=code
            //                 &redirect_uri=https://login.live.com/oauth20_desktop.srf HTTP/1.1

            Application.Run(_form);  // <!---------- Problem is here. 
                                     //  I do not want a web browser window to show,
                                     // I need to automate the part of the process where
                                     // a user enters their name/password and are
                                     // redirected.

            // While the application is running, browser_Navigated filters traffic to identify
            // the redirect URI. The redirect's query string will contain either the authorization  
            // code if the user consented or an error if the user declined.
            // For example https://login.live.com/oauth20_desktop.srf?code=<code>



            // If the user did not give consent or the application was 
            // not registered, the authorization code will be null.

            if (string.IsNullOrEmpty(_code))
            {
                Console.WriteLine(_error);
                return;
            }
无论你做什么,“超级管理员”都必须使用浏览器至少登录一次。您可以通过在服务中托管一个简单的网页来实现这一点,也可以作为安装过程的一部分来实现。现场样品向您展示了如何做到这一点

一旦“超级管理员”使用代码授权登录,您将收到一个访问令牌和一个刷新令牌。我不确定liveaccess令牌的有效期有多长,但它的日志可能足够每晚运行一次。将刷新令牌保存在安全的地方。第二天晚上,首先用新的访问令牌和新的刷新令牌交换该刷新令牌。同样,您要为第二天晚上保存这个新的刷新令牌

只要“超级管理员”不撤销他对你的应用程序的授权,你就可以让这个过程永远运行下去

更新:

一些OAuth 2.0服务器支持“资源所有者密码凭据授予”,请参阅RFC。如果Live server支持这一点,它将是不需要浏览器的代码授权的替代方案。然而,即使大多数服务器支持它,出于安全原因,我也建议不要使用它,因为它需要在服务器上存储您的“超级管理员”密码。如果有人获取了密码,他们就可以完全访问该帐户以及受其保护的所有资源。如果您更改密码,它也会崩溃。代码授权没有这些问题

您的问题表明您希望或需要作为“超级管理员”运行。另一种选择可能是使用“客户端凭据授予”。但是,这也需要在服务器上存储客户机机密(与授予密码凭据一样)。此外,它仍然需要超级管理员授权客户端,这本身就需要使用浏览器授予代码

您会问为什么代码授权需要浏览器,为什么不能使用某种屏幕抓取来模拟浏览器交互。首先,您无法预测将向用户显示的屏幕。这些屏幕会在没有通知的情况下更改。更重要的是,根据用户选项和历史记录,服务器会显示不同的屏幕。例如,用户可能已启用双因素身份验证。最后但并非最不重要的一点是,为什么您反对打开浏览器?这可能比试图模仿它更容易


最后,这些“超级管理员”用户可能会反对将他们的密码提供给您的应用程序,因为他们并不真正知道您在用它做什么(据他们所知,您可能正在向自己的服务器发送密码)。通过在浏览器中使用代码授权,他们知道您的应用程序永远看不到他们的密码(某种程度上-您可以监听浏览器事件或其他内容,除非浏览器控件在不受您控制的单独进程(如Windows 8 WebAuthenticationBroker)中运行)。您的应用程序只获得一个具有其授权范围的令牌。

我自己花了几个小时解决了这个问题,却完全没有找到从服务自动连接到Bing的解决方案。下面是使用奇妙的WatiN

首先抓取WatiN并通过Nuget将其添加到您的解决方案中

然后使用以下代码(我的示例以控制台应用程序为例)自动化从Microsoft获取令牌的整个过程。这不是完美的,因为这是一个样本,但它会工作

您应该仔细检查我正在使用的元素ID,以防它们发生更改,它们是硬编码的-如果要在生产环境中使用,通常会删除所有硬编码

我不想让其他任何人经历这一切

首先,它获取一个代码,然后用于获取令牌,就像OAuth2.0规范所要求的那样

using System;
using System.Collections.Generic;
using System.Net;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
using System.Text;
using WatiN.Core.Native;
using WatiN.Core;

namespace LouiesOAuthCodeGrantFlow
{
    // Using access tokens requires that you register your application and that
    // the user gives consent to your application to access their data. This 
    // example uses a form and WebBrowser control to get the user's consent.
    // The control and form require a single-threaded apartment.

partial class LouiesBingOAuthAutomation 
{

    private static LouiesBingOAuthAutomation _form;

    private static string _code;
    private static string _error;

    //your going to want to put these in a secure place this is for the sample
    public const string UserName = "your microsoft user name";
    public const string Password = "<your microsoft account password";

    // When you register your application, the Client ID is provisioned.
    //get your clientid https://developers.bingads.microsoft.com/Account
    private const string ClientId = "<your client id>";

    // Request-related URIs that you use to get an authorization code, 
    // access token, and refresh token.

    private const string AuthorizeUri = "https://login.live.com/oauth20_authorize.srf";
    private const string TokenUri = "https://login.live.com/oauth20_token.srf";
    private const string DesktopUri = "https://login.live.com/oauth20_desktop.srf";
    private const string RedirectPath = "/oauth20_desktop.srf";
    private const string ConsentUriFormatter = "{0}?client_id={1}&scope=bingads.manage&response_type=code&redirect_uri={2}";//&displayNone
    private const string AccessUriFormatter = "{0}?client_id={1}&code={2}&grant_type=authorization_code&redirect_uri={3}";
    private const string RefreshUriFormatter = "{0}?client_id={1}&grant_type=refresh_token&redirect_uri={2}&refresh_token={3}";

    // Constructor

    public LouiesBingOAuthAutomation(string uri)
    {
        InitializeForm(uri);
    }

    [STAThread]
    static void Main()
    {

        var uri = string.Format(ConsentUriFormatter, AuthorizeUri, ClientId, DesktopUri);
        _form = new LouiesBingOAuthAutomation(uri);

        if (string.IsNullOrEmpty(_code))
        {
            Console.WriteLine(_error);
            return;
        }

        uri = string.Format(AccessUriFormatter, TokenUri, ClientId, _code, DesktopUri);
        AccessTokens tokens = GetAccessTokens(uri);

        Console.WriteLine("Access token expires in {0} minutes: ", tokens.ExpiresIn / 60);
        Console.WriteLine("\nAccess token: " + tokens.AccessToken);
        Console.WriteLine("\nRefresh token: " + tokens.RefreshToken);


        uri = string.Format(RefreshUriFormatter, TokenUri, ClientId, DesktopUri, tokens.RefreshToken);
        tokens = GetAccessTokens(uri);

        Console.WriteLine("Access token expires in {0} minutes: ", tokens.ExpiresIn / 60);
        Console.WriteLine("\nAccess token: " + tokens.AccessToken);
        Console.WriteLine("\nRefresh token: " + tokens.RefreshToken);
    }


    private void InitializeForm(string uri)
    {

        using (var browser = new IE(uri))
        {
            var page = browser.Page<MyPage>();
            page.PasswordField.TypeText(Password);
            try
            {
                StringBuilder js = new StringBuilder();
                js.Append(@"var myTextField = document.getElementById('i0116');");
                js.Append(@"myTextField.setAttribute('value', '"+ UserName + "');");
                browser.RunScript(js.ToString());
                var field = browser.ElementOfType<TextFieldExtended>("i0116");
                field.TypeText(UserName);
            }
            catch (Exception ex)
            {
                Console.Write(ex.Message + ex.StackTrace);
            }
            page.LoginButton.Click();
            browser.WaitForComplete();
            browser.Button(Find.ById("idBtn_Accept")).Click();
            var len = browser.Url.Length - 43;
            string query = browser.Url.Substring(43, len);

            if (query.Length == 50)
            {
                if (!string.IsNullOrEmpty(query))
                {
                    Dictionary<string, string> parameters = ParseQueryString(query, new[] { '&', '?' });

                    if (parameters.ContainsKey("code"))
                    {
                        _code = parameters["code"];
                    }
                    else
                    {
                        _error = Uri.UnescapeDataString(parameters["error_description"]);
                    }
                }
            }

        }

    }

    // Parses the URI query string. The query string contains a list of name-value pairs 
    // following the '?'. Each name-value pair is separated by an '&'.

    private static Dictionary<string, string> ParseQueryString(string query, char[] delimiters)
    {
        var parameters = new Dictionary<string, string>();

        string[] pairs = query.Split(delimiters, StringSplitOptions.RemoveEmptyEntries);

        foreach (string pair in pairs)
        {
            string[] nameValue = pair.Split(new[] { '=' });
            parameters.Add(nameValue[0], nameValue[1]);
        }

        return parameters;
    }

    // Gets an access token. Returns the access token, access token 
    // expiration, and refresh token.

    private static AccessTokens GetAccessTokens(string uri)
    {
        var responseSerializer = new DataContractJsonSerializer(typeof(AccessTokens));
        AccessTokens tokenResponse = null;

        try
        {
            var realUri = new Uri(uri, UriKind.Absolute);

            var addy = realUri.AbsoluteUri.Substring(0, realUri.AbsoluteUri.Length - realUri.Query.Length);
            var request = (HttpWebRequest)WebRequest.Create(addy);

            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";

            using (var writer = new StreamWriter(request.GetRequestStream()))
            {
                writer.Write(realUri.Query.Substring(1));
            }

            var response = (HttpWebResponse)request.GetResponse();

            using (Stream responseStream = response.GetResponseStream())
            {
                if (responseStream != null)
                    tokenResponse = (AccessTokens)responseSerializer.ReadObject(responseStream);
            }
        }
        catch (WebException e)
        {
            var response = (HttpWebResponse)e.Response;

            Console.WriteLine("HTTP status code: " + response.StatusCode);
        }

        return tokenResponse;
    }

 }



 public class MyPage : WatiN.Core.Page
 {
    public TextField PasswordField
    {
        get { return Document.TextField(Find.ByName("passwd")); }
    }

    public WatiN.Core.Button LoginButton
    {
        get { return Document.Button(Find.ById("idSIButton9")); }
    }
 }

 [ElementTag("input", InputType = "text", Index = 0)]
 [ElementTag("input", InputType = "password", Index = 1)]
 [ElementTag("input", InputType = "textarea", Index = 2)]
 [ElementTag("input", InputType = "hidden", Index = 3)]
 [ElementTag("textarea", Index = 4)]
 [ElementTag("input", InputType = "email", Index = 5)]
 [ElementTag("input", InputType = "url", Index = 6)]
 [ElementTag("input", InputType = "number", Index = 7)]
 [ElementTag("input", InputType = "range", Index = 8)]
 [ElementTag("input", InputType = "search", Index = 9)]
 [ElementTag("input", InputType = "color", Index = 10)]
 public class TextFieldExtended : TextField
 {
    public TextFieldExtended(DomContainer domContainer, INativeElement element)
        : base(domContainer, element)
    {
    }

    public TextFieldExtended(DomContainer domContainer, ElementFinder finder)
        : base(domContainer, finder)
    {
    }

    public static void Register()
    {
        Type typeToRegister = typeof(TextFieldExtended);
        ElementFactory.RegisterElementType(typeToRegister);
    }
 }


 // The grant flow returns more fields than captured in this sample.
 // Additional fields are not relevant for calling Bing Ads APIs or refreshing the token.

 [DataContract]
 class AccessTokens
 {
    [DataMember]
    // Indicates the duration in seconds until the access token will expire.
    internal int expires_in = 0;

    [DataMember]
    // When calling Bing Ads service operations, the access token is used as  
    // the AuthenticationToken header element.
    internal string access_token = null;

    [DataMember]
    // May be used to get a new access token with a fresh expiration duration.
    internal string refresh_token = null;

    public string AccessToken { get { return access_token; } }
    public int ExpiresIn { get { return expires_in; } }
    public string RefreshToken { get { return refresh_token; } }
 }
}
使用系统;
使用System.Collections.Generic;
Net系统;
使用System.IO;
使用System.Runtime。