Windows App SDK 中的 OAuth2Manager 使 WinUI 3 等桌面应用程序能够在 Windows 上无缝执行 OAuth 2.0 授权。 OAuth2Manager API 有意不提供隐式请求和资源所有者密码凭据的 API,因为存在安全问题。 建议使用带有代码交换证明密钥(PKCE)的授权代码授权类型。 有关详细信息,请参阅 PKCE RFC。
OAuth 后台
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 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交换访问令牌的授权代码:
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);
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());
}
刷新访问令牌
以下示例演示如何使用 OAuth2Manager的 RefreshTokenAsync 方法刷新访问令牌:
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));
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());
}
完成授权请求
最后,若要完成协议激活的授权请求,应用应处理 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();
}
}