Bing Ads OAuth Automation uses only .NET?

How can I log into Microsoft Live (with .NET WebClient?) And automate the OAuth process to get a token in order to make Bing Ads API calls?

My question is similar to How to get OAuth request_token from live.com? . However, I build (C #, .NET 4.5.2) a headless Windows service using the context of the Bing Ads super administrator account, which is associated with several other Bing Ads accounts. The idea is to authenticate, receive auth bits, and then make calls using the bit at 3:00 in the morning. Some of the accounts “compete”, therefore, for example, group A should not see data from group B, therefore, when the application receives data for everyone and filters it and distributes it overnight, it solves many business problems.

I am concerned that if Live is having problems or our application has not been working for a long time for any reason, we will have to re-authenticate manually in order to receive the data again. Serving and managing credentials is now additional overhead (this is for the corporate environment), which will have to be in the form of a website / intranet page to allow younger / uninitiated people to do work if necessary (let us not forget the testing and documentation). To contrast, Google provides the ability to use key pairs for groups that need to work in a fully automated way. It seems that the Twitter OAuth2 implementation can be automated without logging into the GUI. It seems that other Bing services (like Translation ) can also be automated using WebClient.

I already have a Microsoft account name and password, and I have the “local-mydomain.com” callback URL specified in the Bing Ads application GUI (and there is a HOSTS entry for local-mydomain.com).

The Microsoft example works, but it automates MS Web Browser Control, expects the user to enter credentials in the graphical interface, and then a token is given. Providing a super administrator account for users is not an option for this. Waiting for a user to get up at 3:00 AM for authentication to upload / download data is not an option. Waiting for a user to access the desktop on a server in the farm to “run something” is not an option.

All OAuth ideas are appreciated.

Thanks.

Here is the startup code:

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 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; } 
+5
source share
2 answers

No matter what you do, the “super admin” must log in at least once using a browser. You can do this by hosting a simple web page in your service, or you can do this as part of the setup process. Live samples will show you how to do this.

Once the “super admin” logs in using the grant code, you will receive an access token and an update token. I'm not sure how long the Internet access token has been valid, but it is probably enough for one nightly launch. Keep the update token in a safe place. The next night, you will begin by exchanging this update token using a new access token and a new update token. Again, you save this new update token the next night.

You can save this process forever if the "super administrator" does not cancel the authorization that he gave your application.

UPDATE:

Some OAuth 2.0 servers support "Credentials Credentials Credentials", see the RFC at http://tools.ietf.org/html/rfc6749 . If Live Server supports this, it will be an alternative to Code Grant that does not require a browser. However, even the server supports it, I would recommend against it for security reasons, since it requires the storage of your "super administrator" password on the server. If someone captures a password, they have full access to the account and all resources protected by it. It will also break if you change the password. Providing code does not have these problems.

Your question says that you want or should run as this "super admin". Another option would be to use a “Client Mandate Grant”. However, this also requires that the client secret be stored on the server (as in providing password credentials). In addition, it still requires the super administrator to allow the client, and by itself requires the provision of code using a browser.

You ask why a browser is required to provide the code, why you cannot use any screen scraper to simulate browser interaction. First of all, you cannot predict the screens that will be shown to the user. These screens are subject to change without notice. More importantly, depending on user settings and history, the server displays different screens. For example, a user may enable two-factor authentication. Last but not least, why do you mind opening a browser? This will probably be easier than trying to imitate him.

Finally, these “super administrator” users may object to providing their password to your application, since they really do not know what you are doing with it (you can send to your own server, as far as they know). Using Code Grant with a browser, they know that your application will never see their password (like - you could listen to browser events or something like that if the browser is not controlled in a separate process that is not under your control, for example Windows 8 WebAuthenticationBroker). Your application receives only a token with the permissions that they allow.

+5
source

After spending several hours on this problem for yourself and not finding absolutely any solution to automate the connection to Bing from the service. Here's what will work with the wonderful WatiN

First take WatiN and add it to your solution through Nuget.

Then use the following code (my example works as an example in a console application) to automate all marker capture from Microsoft. This is not ideal, as it is a sample, but it will work.

You should double-check the identifier of the element that I am using, if they change, they are hard-coded - they usually delete all hard-coded ones if you intend to use them in a production environment.

I did not want anyone else to be able to get through this.

It first captures the code, which is then used to capture the token, just like the OAuth 2.0 specification requires.

 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 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; } } } } 
+4
source

Source: https://habr.com/ru/post/1202178/


All Articles