在 Windows 应用中实现 OAuth 2.0

Windows App SDK 中的 OAuth2Manager 使 WinUI 3 等桌面应用程序能够在 Windows 上无缝执行 OAuth 2.0 授权。 由于存在安全问题, OAuth2Manager API 不提供隐式请求和资源所有者密码凭据的 API。 将授权代码授予类型与代码交换的证明密钥(PKCE)配合使用。 有关详细信息,请参阅 PKCE RFC

适用于 Windows 应用的 OAuth 2.0 背景

Windows 运行时(WinRT)的 WebAuthenticationBroker主要是为 UWP 应用设计的,但在桌面应用中使用时会带来若干挑战。 关键问题包括依赖于 ApplicationView,它与桌面应用框架不兼容。 因此,开发人员必须求助于涉及互作接口和其他代码的解决方法,以便将 OAuth 2.0 功能实现到 WinUI 3 和其他桌面应用中。

Windows 应用 SDK 中的 OAuth2Manager API

适用于 Windows 应用 SDK 的 OAuth2Manager API 提供了一种简化的解决方案,可满足开发人员的期望。 它提供无缝的 OAuth 2.0 功能,在 Windows 应用 SDK 支持的所有 Windows 平台上实现全功能一致性。 新的 API 消除了需要繁琐的解决方法,并简化了将 OAuth 2.0 功能合并到桌面应用中的过程。

OAuth2Manager 不同于 WinRT 中的 WebAuthenticationBroker。 它遵循 OAuth 2.0 最佳做法,例如,使用用户的默认浏览器。 API 的最佳做法来自 IETF(Internet 工程工作队)OAuth 2.0 授权框架 RFC 6749、PKCE RFC 7636 和 OAuth 2.0 for Native Apps RFC 8252

OAuth 2.0 代码示例

gitHub 上提供了完整的 WinUI 3 示例应用。 以下部分使用 OAuth2Manager API 为最常见的 OAuth 2.0 流提供代码片段。

授权代码请求

以下示例演示如何在 Windows 应用 SDK 中使用 OAuth2Manager 执行授权代码请求:

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

用于访问令牌的 Exchange 授权代码

以下示例演示如何使用 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 应用或服务),请包括参数 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();
    }
}