Поделиться через


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

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

Замечание

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

API OAuth2Manager в Windows App SDK

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

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());
}

Для конфиденциальных клиентов (например, 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();
    }
}