Реализация OAuth 2.0 в приложениях Windows

Предпосылки

  • Экспериментальный канал выпуска Windows App SDK — OAuth2Manager недоступен в стабильном канале. Установите предварительную версию Microsoft.WindowsAppSDK с суффиксом -experimental (выберите Включить предварительные версии в NuGet). Дополнительные сведения см. в разделе "Настройка среды разработки".

Обзор

OAuth2Manager в Windows App SDK позволяет классическим приложениям, таким как WinUI 3, выполнять авторизацию OAuth 2.0 в Windows. API OAuth2Manager не предоставляет API для неявного запроса и учетных данных владельца ресурса из-за проблем безопасности, которые влечет за собой. Используйте тип предоставления кода авторизации с проверочным ключом для обмена кодами (PKCE). Дополнительную информацию см. в PKCE RFC.

Замечание

OAuth2Manager предназначен для общих потоков OAuth 2.0 с любым поставщиком удостоверений (GitHub, Google, пользовательским и т. д.) и всегда использует системный браузер для шага авторизации. Если вы специально хотите войти с помощью учетных записей Microsoft или учетных записей Microsoft Entra ID (работа/школа) через беззвучный единый вход — используя учетную запись, уже вошедшую в Windows, без запроса браузера, вместо этого используйте MSAL.NET с брокером WAM. Диспетчер веб-учетных записей также предоставляет Windows Hello интеграцию и поддержку условного доступа, что OAuth2Manager не поддерживает.

API OAuth2Manager в Windows App SDK

API OAuth2Manager для Windows App SDK обрабатывает потоки кода авторизации OAuth 2.0 с помощью PKCE для классических приложений. Он работает на всех платформах Windows, поддерживаемых Windows App SDK, предоставляя единый интерфейс получения токенов без использования обходных решений на базе сторонних браузеров.

OAuth2Manager отличается от WebAuthenticationBroker в WinRT. Он следует лучшим практикам OAuth 2.0 более строго - например, используя браузер пользователя по умолчанию. Рекомендации по API поступают из IETF (Группа задач по разработке Интернета) OAuth 2.0 Authorization Framework RFC 6749, PKCE RFC 7636 и OAuth 2.0 для собственных приложений RFC 8252.

Примеры кода OAuth 2.0

Полное пример приложения WinUI доступно в GitHub. В следующих разделах приведены фрагменты кода для наиболее распространенных потоков OAuth 2.0 с помощью API OAuth2Manager.

Запрос кода авторизации

В следующем примере показано, как выполнить запрос кода авторизации с помощью OAuth2Manager в Windows App SDK:

// Get the WindowId for the application window
Microsoft::UI::WindowId parentWindowId = this->AppWindow().Id();

AuthRequestParams authRequestParams = AuthRequestParams::CreateForAuthorizationCodeRequest(L"my_client_id",
   Uri(L"my-app:/oauth-callback/"));
authRequestParams.Scope(L"user:email user:birthday");

AuthRequestResult authRequestResult = co_await OAuth2Manager::RequestAuthWithParamsAsync(parentWindowId, 
   Uri(L"https://my.server.com/oauth/authorize"), authRequestParams);
if (AuthResponse authResponse = authRequestResult.Response())
{
   //To obtain the authorization code
   //authResponse.Code();

   //To obtain the access token
   DoTokenExchange(authResponse);
}
else
{
   AuthFailure authFailure = authRequestResult.Failure();
   NotifyFailure(authFailure.Error(), authFailure.ErrorDescription());
}

Обмен кода авторизации на токен доступа

В следующем примере показано, как обменять код авторизации на маркер доступа с помощью OAuth2Manager.

Для общедоступных клиентов (например, собственных классических приложений), использующих PKCE, не включайте секрет клиента. Средство проверки кода PKCE обеспечивает безопасность вместо этого.

AuthResponse authResponse = authRequestResult.Response();
TokenRequestParams tokenRequestParams = TokenRequestParams::CreateForAuthorizationCodeRequest(authResponse);

// For public clients using PKCE, do not include ClientAuthentication
TokenRequestResult tokenRequestResult = co_await OAuth2Manager::RequestTokenAsync(
    Uri(L"https://my.server.com/oauth/token"), tokenRequestParams);
if (TokenResponse tokenResponse = tokenRequestResult.Response())
{
    String accessToken = tokenResponse.AccessToken();
    String tokenType = tokenResponse.TokenType();

    // RefreshToken string null/empty when not present
    if (String refreshToken = tokenResponse.RefreshToken(); !refreshToken.empty())
    {
        // ExpiresIn is zero when not present
        DateTime expires = winrt::clock::now();
        if (String expiresIn = tokenResponse.ExpiresIn(); std::stoi(expiresIn) != 0)
        {
            expires += std::chrono::seconds(static_cast<int64_t>(std::stoi(expiresIn)));
        }
        else
        {
            // Assume a duration of one hour
            expires += std::chrono::hours(1);
        }

        //Schedule a refresh of the access token
        myAppState.ScheduleRefreshAt(expires, refreshToken);
    }

    // Use the access token for resources
    DoRequestWithToken(accessToken, tokenType);
}
else
{
    TokenFailure tokenFailure = tokenRequestResult.Failure();
    NotifyFailure(tokenFailure.Error(), tokenFailure.ErrorDescription());
}

Important

Настольные приложения являются общедоступными клиентами и не должны встраивать секреты клиента. Приведенный ниже шаблон конфиденциального клиента применяется только к компонентам на стороне сервера (веб-приложениям, API, внутренним службам), которые могут безопасно хранить учетные данные. Собственное классическое приложение не может защитить секрет клиента от извлечения— используйте шаблон общедоступного клиента с PKCE, показанным выше. Если в вашей архитектуре требуется секрет клиента для обмена токенами, выполните этот обмен на серверной службе, а не в классическом приложении.

Для конфиденциальных клиентов (например, web apps или служб), имеющих секрет клиента, включите параметр ClientAuthentication:

AuthResponse authResponse = authRequestResult.Response();
TokenRequestParams tokenRequestParams = TokenRequestParams::CreateForAuthorizationCodeRequest(authResponse);
ClientAuthentication clientAuth = ClientAuthentication::CreateForBasicAuthorization(L"my_client_id",
    L"my_client_secret");

TokenRequestResult tokenRequestResult = co_await OAuth2Manager::RequestTokenAsync(
    Uri(L"https://my.server.com/oauth/token"), tokenRequestParams, clientAuth);
// Handle the response as shown in the previous example

Обновление токена доступа

В следующем примере показано, как обновить токен доступа с помощью метода OAuth2ManagerRefreshTokenAsync.

Для общедоступных клиентов , использующих PKCE, опустите ClientAuthentication параметр:

TokenRequestParams tokenRequestParams = TokenRequestParams::CreateForRefreshToken(refreshToken);

// For public clients using PKCE, do not include ClientAuthentication
TokenRequestResult tokenRequestResult = co_await OAuth2Manager::RequestTokenAsync(
    Uri(L"https://my.server.com/oauth/token"), tokenRequestParams);
if (TokenResponse tokenResponse = tokenRequestResult.Response())
{
    UpdateToken(tokenResponse.AccessToken(), tokenResponse.TokenType(), tokenResponse.ExpiresIn());

    //Store new refresh token if present
    if (String refreshToken = tokenResponse.RefreshToken(); !refreshToken.empty())
    {
        // ExpiresIn is zero when not present
        DateTime expires = winrt::clock::now();
        if (String expiresInStr = tokenResponse.ExpiresIn(); !expiresInStr.empty())
        {
            int expiresIn = std::stoi(expiresInStr);
            if (expiresIn != 0)
            {
                expires += std::chrono::seconds(static_cast<int64_t>(expiresIn));
            }
        }
        else
        {
            // Assume a duration of one hour
            expires += std::chrono::hours(1);
        }

        //Schedule a refresh of the access token
        myAppState.ScheduleRefreshAt(expires, refreshToken);
    }
}
else
{
    TokenFailure tokenFailure = tokenRequestResult.Failure();
    NotifyFailure(tokenFailure.Error(), tokenFailure.ErrorDescription());
}

Для конфиденциальных клиентов , имеющих секрет клиента, включите ClientAuthentication параметр:

TokenRequestParams tokenRequestParams = TokenRequestParams::CreateForRefreshToken(refreshToken);
ClientAuthentication clientAuth = ClientAuthentication::CreateForBasicAuthorization(L"my_client_id",
    L"my_client_secret");
TokenRequestResult tokenRequestResult = co_await OAuth2Manager::RequestTokenAsync(
    Uri(L"https://my.server.com/oauth/token"), tokenRequestParams, clientAuth);
// Handle the response as shown in the previous example

Завершение запроса авторизации

Чтобы завершить запрос авторизации при активации протокола, приложение должно обрабатывать событие AppInstance.Activated. Это событие необходимо, если приложение имеет пользовательскую логику перенаправления. Полный пример доступен в GitHub.

Используйте следующий код:

void App::OnActivated(const IActivatedEventArgs& args)
{
    if (args.Kind() == ActivationKind::Protocol)
    {
        auto protocolArgs = args.as<ProtocolActivatedEventArgs>();
        if (OAuth2Manager::CompleteAuthRequest(protocolArgs.Uri()))
        {
            TerminateCurrentProcess();
        }

        DisplayUnhandledMessageToUser();
    }
}