如何登录Microsoft Live(使用.NET WebClient?)并自动执行OAuth流程以获取令牌以进行Bing Ads API调用?
我的问题类似于How do I get an OAuth request_token from live.com?。但是,我正在构建(C#,. NET 4.5.2)无头Windows服务,使用Bing Ads超级管理员帐户的上下文链接到多个其他Bing Ads帐户。我们的想法是验证,获取auth位,然后在凌晨3:00使用位进行调用。一些帐户“竞争”,例如A组不应该看到来自B组的数据,因此让应用程序获取每个人的数据并过滤它并在一夜之间分发它可以解决许多业务问题。
我担心如果直播遇到问题,或者我们的应用程序由于任何原因而长时间停机,我们将不得不手动重新进行身份验证以重新获取数据。现在,凭证的维护和管理是额外的开销(这是针对企业环境的),必须采用内部网站点/页面的形式,以便初级/不熟练的人员在需要时进行工作(不要忘记测试和文档)。相比之下,Google提供了一个选项,可以为需要以完全自动化方式工作的组使用密钥对。看来Twitter的OAuth2实现可以在没有GUI登录的情况下实现自动化。似乎其他Bing服务(例如Translation)也可以通过WebClient自动化。
我已经拥有Microsoft帐户名和密码,并且在Bing Ads应用GUI中设置了“local-mydomain.com”的回调网址(并且还有local-mydomain.com的HOSTS条目)。
Microsoft sample似乎可以正常工作,但它会自动化MS Web Browser Control,期望用户在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;
}
答案 0 :(得分:5)
无论你做什么,&#34;超级管理员&#34;必须使用浏览器至少登录一次。您可以通过在服务中托管一个简单的网页来实现,或者您可以在设置过程中执行此操作。实时样本向您展示如何做到这一点。
一旦&#34;超级管理员&#34;已使用代码授权登录,您将收到访问令牌和刷新令牌。我不确定Live访问令牌的有效期是多久,但它可能足以记录每晚运行一次。将刷新令牌保存在安全的地方。第二天晚上,您将通过新的访问令牌和新的刷新令牌交换该刷新令牌。同样,您将在第二天晚上保存此新刷新令牌。
只要&#34;超级管理员&#34;你可以让这个过程永远运行。不会撤销他对您的应用授予的授权。
更新:
某些OAuth 2.0服务器支持&#34;资源所有者密码凭据授予&#34;,请参阅http://tools.ietf.org/html/rfc6749处的RFC。如果Live服务器支持,它将是不需要浏览器的代码授予的替代方案。但是,即使服务器支持它,我也会出于安全考虑而建议不要使用它,因为它需要存储你的超级管理员&#34;服务器上的密码。如果有人抓取密码,他们可以完全访问该帐户,并拥有受其保护的所有资源。如果更改密码,它也会崩溃。代码授予没有这些问题。
您的问题表明您希望或需要以此身份运行&#34;超级管理员&#34;。另一种选择可能是使用&#34;客户端凭据授权&#34;。但是,这还需要将客户机密钥存储在服务器上(与密码凭证授权一样)。此外,它仍然需要超级管理员授权客户端,并且本身需要使用浏览器进行代码授权。
您问为什么代码授权需要浏览器,为什么您不能使用某种屏幕抓取来模拟浏览器交互。首先,您无法预测将向用户显示的屏幕。这些屏幕在没有通知的情更重要的是,根据用户选项和历史记录,服务器会显示不同的屏幕。例如,用户可能已启用双因素身份验证。最后但同样重要的是,您为什么反对打开浏览器?它可能比试图模仿它更容易。
最后,这些&#34;超级管理员&#34;用户可能会反对将密码提供给您的应用程序,因为他们并不真正知道您正在使用它做什么(就您所知,您可能会发送到您自己的服务器)。通过浏览器使用代码授权,他们知道您的应用程序永远不会看到他们的密码(您可以收听浏览器事件或其他内容,除非浏览器控件在不受您控制的单独进程中运行,例如Windows 8 WebAuthenticationBroker)。您的应用程序仅获取具有其授权范围的令牌。
答案 1 :(得分:4)
为自己花了几个小时来解决这个问题并且找不到自动从服务连接到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; } }
}
}