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;
}
无论你做什么;超级管理员";必须使用浏览器至少登录一次。您可以通过在服务中托管一个简单的网页来实现这一点,也可以将其作为设置过程的一部分。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; } }
}
}