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

本文关键字:NET 自动化 广告 OAuth Bing | 更新日期: 2023-09-27 18:21:26

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

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

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

我已经有了Microsoft帐户名和密码,并且在Bing Ads应用程序GUI中设置了一个回调URL"localmydomain.com"(并且有一个localmydomaincom的HOSTS条目)。

Microsoft示例似乎可以工作,但它自动执行MS Web浏览器控制,期望用户在GUI中输入凭据,然后给出令牌。将超级管理员帐户交给用户来执行此操作不是一种选择。期望用户在凌晨3点起床进行身份验证以上传/下载数据不是一种选择。期望用户获得对服务器场中服务器的桌面访问权限以"运行某些内容"不是一种选择。

感谢所有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;
            }

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

无论你做什么;超级管理员";必须使用浏览器至少登录一次。您可以通过在服务中托管一个简单的网页来实现这一点,也可以将其作为设置过程的一部分。Live示例将向您展示如何做到这一点。

一旦";超级管理员";使用代码授权登录后,您将收到一个访问令牌和一个刷新令牌。我不确定Live访问令牌的有效期,但它可能足够每晚运行一次。将刷新令牌保存在安全的地方。第二天晚上,您将开始用新的访问令牌和新的刷新令牌交换刷新令牌。再次,您将此新刷新令牌保存到第二天晚上。

你可以让这个过程永远运行,只要;超级管理员";不会撤销他对您的应用程序的授权。

更新:

一些OAuth 2.0服务器支持";"资源所有者密码证书授予";,请参阅RFChttps://www.rfc-editor.org/rfc/rfc6749.如果Live服务器支持这一点,它将是不需要浏览器的代码授予的替代方案。然而,即使服务器支持它,出于安全原因,我也建议不要使用它,因为它需要存储您的";超级管理员";服务器上的密码。如果有人盗用了密码,他们就可以完全访问该帐户以及受其保护的所有资源。如果你更改密码,该帐户也会崩溃。代码授予不存在这些问题。

你的问题表明你想或需要以这种方式运行;超级管理员";。另一种选择可能是使用";客户端凭据授予";。然而,这也需要在服务器上存储客户端机密(与授予密码凭据一样)。此外,它仍然需要超级管理员来授权客户端,而这本身就需要使用浏览器授予代码。

你会问为什么代码授权需要浏览器,为什么你不能使用某种屏幕抓取来模拟浏览器交互。首先,您无法预测将显示给用户的屏幕。这些屏幕发生变化,恕不另行通知。更重要的是,根据用户选项和历史记录,服务器会显示不同的屏幕。例如,用户可能已经开启了双因素身份验证。最后但同样重要的是,你为什么反对打开浏览器?这可能比试图模仿它更容易

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

我自己花了几个小时解决这个问题,却没有找到从服务自动连接到Bing的解决方案。以下是使用出色的WatiN的效果

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

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

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

我不想让其他人经历这些。

首先,它获取一个代码,然后用于获取令牌,就像OAuth 2.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; } }
 }
}