Авторизация OAuth 2.0 для настольного приложения windows с использованием HttpListener - PullRequest
0 голосов
/ 05 марта 2020

Я пишу настольное приложение windows с внешней аутентификацией (Google, Facebook) в C#.

Я использую HttpListener, чтобы позволить пользователю получать маркер Barer с помощью внешней службы аутентификации с помощью ASP. NET Веб-API, но для этого требуются права администратора, и я хочу работать без режима администратора.

Моя ссылка была Пример настольного приложения для Windows.

Это лучший метод для внешнего провайдера аутентификации C#? Или есть другой способ сделать это?

Это мой код для получения токена Barer от внешнего провайдера:

public static async Task<string> RequestExternalAccessToken(string provider)
{
    // Creates a redirect URI using an available port on the loopback address.
    string redirectURI = string.Format("http://{0}:{1}/", IPAddress.Loopback, GetRandomUnusedPort());

    // Creates an HttpListener to listen for requests on that redirect URI.
    var http = new HttpListener();
    http.Prefixes.Add(redirectURI);
    http.Start();

    // Creates the OAuth 2.0 authorization request.
    string authorizationRequest = Properties.Settings.Default.Server
        + "/api/Account/ExternalLogin?provider="
        + provider
        + "&response_type=token&client_id=desktop"
        + "&redirect_uri="
        + redirectURI + "?";

    // Opens request in the browser.
    System.Diagnostics.Process.Start(authorizationRequest);

    // Waits for the OAuth authorization response.
    var context = await http.GetContextAsync();

    // Sends an HTTP response to the browser.
    var response = context.Response;
    string responseString = string.Format("<html><head></head><body></body></html>");
    var buffer = System.Text.Encoding.UTF8.GetBytes(responseString);
    response.ContentLength64 = buffer.Length;
    var responseOutput = response.OutputStream;
    Task responseTask = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) =>
    {
        responseOutput.Close();
        http.Stop();
        Console.WriteLine("HTTP server stopped.");
    });

    // Checks for errors.
    if (context.Request.QueryString.Get("access_token") == null)
    {
        throw new ApplicationException("Error connecting to server");
    }

    var externalToken = context.Request.QueryString.Get("access_token");

    var path = "/api/Account/GetAccessToken";

    var client = new RestClient(Properties.Settings.Default.Server + path);
    RestRequest request = new RestRequest() { Method = Method.GET };
    request.AddParameter("provider", provider);
    request.AddParameter("AccessToken", externalToken);
    request.AddHeader("Content-Type", "application/x-www-form-urlencoded");

    var clientResponse = client.Execute(request);

    if (clientResponse.StatusCode == HttpStatusCode.OK)
    {
        var responseObject = JsonConvert.DeserializeObject<dynamic>(clientResponse.Content);

        return responseObject.access_token;
    }
    else
    {
        throw new ApplicationException("Error connecting to server", clientResponse.ErrorException);
    }
}

Ответы [ 2 ]

1 голос
/ 05 марта 2020

Чтобы добавить к отличному ответу Пола:

  • Библиотеки моделей идентичности заслуживают внимания - одна из вещей, которые они сделают для вас, - поток кода авторизации (PKCE) что рекомендуется для нативных приложений
  • Мои предпочтения такие же, как у Пола - использовать собственные схемы URI - удобство использования лучше, я думаю
  • Сказав это, решение с обратной связью должно работать без прав администратора для количество портов больше 1024

Если это поможет, в моем блоге есть кое-что об этом - в том числе образец Nodejs / Electron, который вы можете запустить с здесь , чтобы увидеть, что закончено Решение выглядит так.

1 голос
/ 05 марта 2020

Я не знаю о Facebook, но обычно (у меня есть опыт работы с Google OAuth2 и Azure AD, а также Azure AD B2 C), поставщик аутентификации позволяет вам использовать custom Схема URI для обратного вызова аутентификации, что-то вроде badcompany://auth

Чтобы получить токен аутентификации, я реализовал следующую схему (Весь код представлен без гарантии и не копируется бездумно.)

1. Регистрация URI-обработчика при запуске приложения

Вы можете зарегистрировать URI-обработчик, создав ключ в HKEY_CURRENT_USER/Software/Classes (следовательно, права администратора не требуются) в Windows реестре

  • Имя ключа равно префиксу URI, badcompany в нашем случае
  • Ключ содержит пустое строковое значение с именем URL Protocol
  • Ключ содержит подключ DefaultIcon для иконки (на самом деле я не знаю, нужно ли это), я использовал путь к текущему исполняемому файлу
  • . Здесь есть подраздел shell/open/command, значение которого по умолчанию определяет путь к команде, которую нужно выполнить. ** при попытке открытия URI ** обратите внимание *, что "%1" необходимо для передачи URI исполняемому файлу
    this.EnsureKeyExists(Registry.CurrentUser, "Software/Classes/badcompany", "URL:BadCo Applications");
    this.SetValue(Registry.CurrentUser, "Software/Classes/badcompany", "URL Protocol", string.Empty);
    this.EnsureKeyExists(Registry.CurrentUser, "Software/Classes/badcompany/DefaultIcon", $"{location},1");
    this.EnsureKeyExists(Registry.CurrentUser, "Software/Classes/badcompany/shell/open/command", $"\"{location}\" \"%1\"");

// ...

private void SetValue(RegistryKey rootKey, string keys, string valueName, string value)
{
    var key = this.EnsureKeyExists(rootKey, keys);
    key.SetValue(valueName, value);
}

private RegistryKey EnsureKeyExists(RegistryKey rootKey, string keys, string defaultValue = null)
{
    if (rootKey == null)
    {
        throw new Exception("Root key is (null)");
    }

    var currentKey = rootKey;
    foreach (var key in keys.Split('/'))
    {
        currentKey = currentKey.OpenSubKey(key, RegistryKeyPermissionCheck.ReadWriteSubTree) 
                     ?? currentKey.CreateSubKey(key, RegistryKeyPermissionCheck.ReadWriteSubTree);

        if (currentKey == null)
        {
            throw new Exception("Could not get or create key");
        }
    }

    if (defaultValue != null)
    {
        currentKey.SetValue(string.Empty, defaultValue);
    }

    return currentKey;
}

2. Откройте канал для IP C

Поскольку вам придется передавать сообщения из одного экземпляра вашей программы в другой, вам придется открыть именованный канал, который можно использовать для этой цели.

Я назвал этот код в al oop на фоне Task

private async Task<string> ReceiveTextFromPipe(CancellationToken cancellationToken)
{
    string receivedText;

    PipeSecurity ps = new PipeSecurity();
    System.Security.Principal.SecurityIdentifier sid = new System.Security.Principal.SecurityIdentifier(System.Security.Principal.WellKnownSidType.WorldSid, null);
    PipeAccessRule par = new PipeAccessRule(sid, PipeAccessRights.ReadWrite, System.Security.AccessControl.AccessControlType.Allow);
    ps.AddAccessRule(par);

    using (var pipeStream = new NamedPipeServerStream(this._pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Message, PipeOptions.Asynchronous, 4096, 4096, ps))
    {
        await pipeStream.WaitForConnectionAsync(cancellationToken);

        using (var streamReader = new StreamReader(pipeStream))
        {
            receivedText = await streamReader.ReadToEndAsync();
        }
    }

    return receivedText;
}

3. Убедитесь, что приложение запускается только один раз

. Это можно получить с помощью Mutex.

internal class SingleInstanceChecker
{
    private static Mutex Mutex { get; set; }

    public static async Task EnsureIsSingleInstance(string id, Action onIsSingleInstance, Func<Task> onIsSecondaryInstance)
    {
        SingleInstanceChecker.Mutex = new Mutex(true, id, out var isOnlyInstance);
        if (!isOnlyInstance)
        {
            await onIsSecondaryInstance();
            Application.Current.Shutdown(0);
        }
        else
        {
            onIsSingleInstance();
        }
    }
}

Когда мьютекс был получен другим экземпляром, приложение запускается не полностью, , но

4. Обработчик вызова с URI перенаправления аутентификации

  1. Если это единственный (первый) экземпляр, он может обрабатывать сам URI перенаправления аутентификации
    • Извлечь токен из URI
    • Сохранить токен (если необходимо и / или необходимо)
    • Использовать токен для запросов
  2. Если это дополнительный экземпляр
    • Передать перенаправление URI для первого экземпляра с использованием каналов
    • Первый экземпляр теперь выполняет шаги в 1.
    • Закройте второй экземпляр

URI отправляется первому экземпляру с

using (var client = new NamedPipeClientStream(this._pipeName))
{
    try
    {
        var millisecondsTimeout = 2000;
        await client.ConnectAsync(millisecondsTimeout);
    }
    catch (Exception)
    {
        onSendFailed();
        return;
    }

    if (!client.IsConnected)
    {
        onSendFailed();
    }

    using (StreamWriter writer = new StreamWriter(client))
    {
        writer.Write(stringToSend);
        writer.Flush();
    }
}
...