在 Windows 应用中实现 OAuth 2.0

Prerequisites

  • Windows 应用 SDK实验性发布通道 - OAuth2Manager 在稳定通道中不可用。 安装带有 -experimental 后缀的预发行 Microsoft.WindowsAppSDK 版本(在 NuGet 中选择 包括预发行版)。 有关详细信息,请参阅 设置开发环境

Overview

Windows 应用 SDK中的 OAuth2Manager 使 WinUI 3 等桌面应用程序能够在Windows上执行 OAuth 2.0 授权。 由于存在安全问题, OAuth2Manager API 不提供隐式请求和资源所有者密码凭据的 API。 将授权代码授权类型与 Proof Key for Code Exchange(PKCE)一起使用。 有关详细信息,请参阅 PKCE RFC

注释

OAuth2Manager专为具有任何标识提供者(GitHub、Google、自定义等)的常规 OAuth 2.0 流而设计,并且始终使用系统浏览器执行授权步骤。 如果你特别想通过 Microsoft 帐户或 Microsoft Entra ID(工作/学校)帐户 使用 无提示单点登录(SSO) 方式登录——即使用已登录 Windows 的帐户,而没有浏览器提示——请改用 MSAL.NET Web 帐户管理器(WAM)代理程序。 Web 帐户管理器还提供 OAuth2Manager 未提供的Windows Hello集成和条件访问支持。

Windows 应用 SDK中的 OAuth2Manager API

适用于Windows 应用 SDK的 OAuth2Manager API 使用适用于桌面应用的 PKCE 处理 OAuth 2.0 授权代码流。 它适用于Windows 应用 SDK支持的所有Windows平台,提供一致的令牌获取接口,而无需基于第三方浏览器的解决方法。

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 示例应用。 以下部分使用 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());
}

Important

桌面应用是公共客户端,不得嵌入客户端机密。 下面的机密客户端模式仅适用于可以安全地存储凭据的服务器端组件(Web 应用、API、后端服务)。 本机桌面应用无法保护客户端机密免遭提取 , 请改用上面所示的 PKCE 的公共客户端模式。 如果你的体系结构需要令牌交换的客户端密码,请在后端服务上执行该交换,而不是在桌面应用中执行该交换。

对于具有客户端机密的 保密客户端(如 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();
    }
}