2014-09-09 8 views
5

Jak mogę zalogować się do usługi Microsoft Live (z aplikacją .NET WebClient?) I zautomatyzować proces OAuth, aby uzyskać token do wywoływania funkcji API Bing Ads?Automatyzacja OAuth Bing Ads przy użyciu tylko .NET?

Moje pytanie jest podobne do How do I get an OAuth request_token from live.com?. Jednak buduję (C#, .NET 4.5.2) bezgłową usługę Windows, korzystając z kontekstu superużytkownika Bing Ads, które jest połączone z wieloma innymi kontami Bing Ads. Pomysł polega na uwierzytelnianiu, uzyskiwaniu automatycznych bitów, a następnie nawiązywaniu połączeń za pomocą bitów o 3:00 rano. Niektóre konta "konkurują", więc np. Grupa A nie powinna widzieć danych z grupy B, więc posiadanie aplikacji dla wszystkich danych, filtrowanie jej i dystrybucja z dnia na dzień rozwiązuje wiele problemów biznesowych.

Obawiam się, że jeśli Live przeżyją problemy lub nasza aplikacja zostanie wyłączona na dłuższy czas z dowolnego powodu, będziemy musieli ponownie uwierzytelnić się ręcznie, aby odzyskać dane. Utrzymanie i zarządzanie danymi uwierzytelniającymi jest teraz dodatkowym obciążeniem (dotyczy to środowiska korporacyjnego), które będzie musiało przybrać formę strony internetowej/strony intranetowej, aby umożliwić młodym/niewtajemniczonym pracę w razie potrzeby (nie zapomnij o testowaniu i dokumentacja). Dla kontrastu Google oferuje opcję użycia par kluczy dla grup, które muszą pracować w pełni zautomatyzowany sposób. Wygląda na to, że implementacja OAuth2 na Twitterze może być zautomatyzowana bez logowania GUI. Wygląda na to, że inne usługi Bing (np. Translation) można również zautomatyzować za pomocą WebClient.

Mam już nazwę konta Microsoft i hasło oraz adres URL zwrotny "local-mydomain.com" ustawiony w interfejsie GUI aplikacji Bing Ads (i mieć wpis HOSTS dla local-mydomain.com).

Wygląda na to, że Microsoft sample działa, ale automatyzuje kontrolę MS Web Browser, oczekuje od użytkownika wprowadzenia danych uwierzytelniających w interfejsie GUI, a następnie podaje token. Podanie konta superadministratora użytkownikom, aby to zrobić, nie jest opcją. Oczekiwanie, że użytkownik wstanie o 3:00 rano w celu uwierzytelnienia w celu przesłania/pobrania danych, nie jest opcją. Oczekiwanie, że użytkownik uzyska dostęp pulpitu do serwera w farmie, aby "uruchomić coś", nie jest opcją.

Wszystkie pomysły OAuth zostały docenione.

Dzięki.

Oto kod uruchomienie:

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

'Próbka Microsoft wydaje się działać, ale automatyzuje przeglądarki internetowej MS sterowania, spodziewa się użytkownikowi poświadczenia wejściowych w GUI' można pokazać, że fragment kodu może używasz Poświadczenia Enum niepoprawnie, jeśli jest potrzebne .. – MethodMan

+0

@DJKRAZE, problem nie jest Enum, to że kontrola WinFit + przeglądarka internetowa jest wymagana w próbce MS, aby uzyskać token. Nie mogę mieć obiektu COM GUI pojawiały się na serwerze siedzącym w szafie tylko po to, aby uzyskać token uwierzytelniania. Moim prawdziwym problemem jest brak umiejętności wysyłania poświadczeń w czystym kodzie, aby odzyskać token - bez GUI. Dzięki. – Snowy

+0

czy możesz pokazać, co masz do tej pory proszę .. to nie powinno wydawać się trudne, zwykle większość obiektów, które używają tokenu lub nazwy użytkownika \ hasło domeny na przykład mają właściwość 'PropertyName.UseDefaultCredentials' na przykład możesz pokazać kod – MethodMan

Odpowiedz

5

Cokolwiek zrobisz, „super admin” będzie musiał zalogować się co najmniej raz, przy użyciu przeglądarki. Możesz to zrobić, umieszczając w swojej usłudze prostą stronę internetową lub możesz to zrobić w ramach procesu instalacji. Przykłady na żywo pokazują, jak to zrobić.

Po zalogowaniu się "superadministratora" przy użyciu nadawania kodu otrzymasz token dostępu i token odświeżania. Nie wiem, jak długo token dostępu na żywo jest prawidłowy, ale prawdopodobnie jest to wystarczający czas na jeden wieczorny bieg. Zapisz token odświeżania w bezpiecznym miejscu. Następnej nocy zaczniesz od wymiany tego tokena odświeżania przez nowy token dostępu i nowy token odświeżania. Ponownie zapisz ten nowy token odświeżający na następną noc.

Możesz utrzymać ten proces w nieskończoność, o ile "superadministrator" nie cofnie autoryzacji nadanej Twojej aplikacji.

UPDATE:

Niektóre serwery OAuth 2.0 obsługują "zasobów Hasło właściciela Poświadczenia Grant", patrz RFC w http://tools.ietf.org/html/rfc6749. Jeśli serwer Live obsługuje to, będzie to alternatywa dla Code Grant, która nie wymaga przeglądarki. Jednak nawet serwer obsługuje to, polecam przeciwko niemu ze względów bezpieczeństwa, ponieważ wymaga przechowywania hasła "superadministratora" na serwerze.Jeśli ktoś złapie hasło, ma pełny dostęp do konta i wszystkich zasobów przez niego chronionych. Spowoduje to również awarię, jeśli zmienisz hasło. Grant kodujący nie ma tych problemów.

Twoje pytanie mówi, że chcesz lub potrzebujesz działać jako "superadministrator". Inną opcją może być skorzystanie z "Wypowiedzi poświadczeń dla klientów". Wymaga to jednak również zachowania tajnego klucza klienta na serwerze (tak jak w przypadku udzielania poświadczeń hasła). Ponadto nadal wymaga od superadministratora autoryzacji klienta, a to samo w sobie wymaga przyznania kodu przy użyciu przeglądarki.

Pytasz, dlaczego przyznanie kodu wymaga przeglądarki, dlaczego nie można użyć skriningu ekranu do symulacji interakcji przeglądarki. Przede wszystkim nie można przewidzieć ekranów, które zostaną wyświetlone użytkownikowi. Te ekrany zmieniają się bez powiadomienia. Co ważniejsze, w zależności od opcji użytkownika i historii, serwer wyświetla różne ekrany. Na przykład użytkownik mógł włączyć uwierzytelnianie dwuskładnikowe. Wreszcie, dlaczego nie chcesz otworzyć przeglądarki? Prawdopodobnie będzie to łatwiejsze niż próba naśladowania go.

Wreszcie, ci użytkownicy "superadministratora" mogą sprzeciwić się podaniu hasła do aplikacji, ponieważ tak naprawdę nie wiedzą, co z nią robisz (możesz wysyłać na serwer własny, o ile oni wiedzą). Korzystając z Code Grant z przeglądarką, wiedzą, że twoja aplikacja nigdy nie zobaczy swojego hasła (rodzaj - możesz słuchać w wydarzeniach przeglądarki lub coś w tym stylu, chyba że kontrola przeglądarki jest uruchamiana w oddzielnym procesie, który nie jest pod Twoją kontrolą, na przykład Windows 8 WebAuthenticationBroker). Twoja aplikacja otrzymuje token tylko z autoryzowanymi zakresami.

+0

Dzięki Kris. Jedyne, czego nie rozumiem, to dlaczego przeglądarka jest wymagana.Sądzę, że WebClient mógłby zrobić wszystko, co może zrobić człowiek (natychmiastowe potwierdzenie POSTing, wypełnienie formularza itp.), A OAuth serwera-serwera nie jest nowym problemem i został rozwiązany. Pomyślałem, że może być nadzieja, gdy zobaczyłem http://weblog.west-wind.com/posts/2013/Jun/06/Setting-up-and-using-Bing-Translate-API-Service-for-Machine-Translation ale może nie. – Snowy

+1

@Snowowy Mam zaktualizowaną odpowiedź. –

+0

@ Chris, czy mogę użyć pojedynczego tokena odświeżania dla wszystkich kont reklam generowanych przez "Superadministratora". –

4

Po spędzeniu kilku godzin nad tym problemem dla siebie i znalezieniem absolutnie żadnego rozwiązania automatycznego łączenia się z usługą Bing. Oto co będzie działać przy użyciu wspaniałym Watin

First grab Watin i dodać go do rozwiązania poprzez Nuget.

Następnie użyj następującego kodu (mój przykład działa w aplikacji konsolowej jako Przykład), aby zautomatyzować cały chwytanie tokena od firmy Microsoft. Nie jest doskonały, ponieważ jest to próbka, ale zadziała.

Powinieneś dokładnie sprawdzić ID elementu, którego używam w przypadku, gdy się zmienił, są one zakodowane na sztywno - zazwyczaj usuń całe twarde kodowanie, jeśli zamierzasz użyć tego w środowisku produkcyjnym.

Nie chciałem, żeby ktokolwiek inny musiał przejść przez to.

Najpierw pobiera kod, który jest następnie używany do pobierania tokena, tak jak wymaga tego specyfikacja 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; } } 
} 
} 
Powiązane problemy