|
| 1 | +--- |
| 2 | +title: Implement OAuth 2.0 functionality in Windows apps |
| 3 | +description: Learn how to implement OAuth 2.0 functionality in Windows apps using the Windows App SDK's OAuth2Manager. |
| 4 | +ms.date: 03/19/2025 |
| 5 | +ms.topic: concept-article |
| 6 | +keywords: windows, winui, winrt, dotnet, security |
| 7 | +#customer intent: As a Windows app developer, I want to learn how to implement OAuth 2.0 functionality in my app so that I can securely authenticate users and access protected resources. |
| 8 | +--- |
| 9 | + |
| 10 | +# Implement OAuth 2.0 functionality in Windows apps |
| 11 | + |
| 12 | +The [OAuth2Manager](/windows/windows-app-sdk/api/winrt/microsoft.security.authentication.oauth.oauth2manager) in Windows App SDK enables desktop applications such as WinUI 3 to seamlessly perform OAuth 2.0 authorization on Windows. **OAuth2Manager** API intentionally doesn't provide APIs for the implicit request and resource owner password credential because of the security concerns that entails. It's recommended to use the authorization code grant type using Proof Key for Code Exchange (PKCE). For more information, see the [PKCE RFC](https://tools.ietf.org/html/rfc7636). |
| 13 | + |
| 14 | +## OAuth background |
| 15 | + |
| 16 | +The Windows Runtime (WinRT) [WebAuthenticationBroker](/uwp/api/windows.security.authentication.web.webauthenticationbroker), primarily designed for UWP apps, presents several challenges when used in desktop apps. Key issues include the dependency on [ApplicationView](/uwp/api/windows.ui.viewmanagement.applicationview), which isn't compatible with desktop app frameworks. As a result, developers are forced to resort to workarounds involving interop interfaces and additional code to implement OAuth 2.0 functionality into WinUI 3 and other desktop apps. |
| 17 | + |
| 18 | +## OAuth2Manager API in Windows App SDK |
| 19 | + |
| 20 | +The **OAuth2Manager** API for Windows App SDK aims to provide a streamlined solution that meets the expectations of developers. It offers seamless OAuth 2.0 capabilities with full feature parity across all Windows platforms supported by Windows App SDK. The new API eliminates the need for cumbersome workarounds and simplifies the process of incorporating OAuth 2.0 functionality into desktop apps. |
| 21 | + |
| 22 | +The **OAuth2Manager** is different than the **WebAuthenticationBroker** in WinRT. It follows OAuth 2.0 best practices more closely - e.g. using the user's default browser. The best practices for the API are taken from the IETF (Internet Engineering Task Force) OAuth 2.0 Authorization Framework [RFC 6749](https://tools.ietf.org/html/rfc6749), PKCE [RFC 7636](https://tools.ietf.org/html/rfc7636), and OAuth 2.0 for Native Apps [RFC 8252](https://tools.ietf.org/html/rfc8252). |
| 23 | + |
| 24 | +## Perform OAuth 2.0 examples |
| 25 | + |
| 26 | +A full WinUI 3 sample app is available on [GitHub](https://github.com/microsoft/WindowsAppSDK-Samples/tree/release/experimental/Samples/OAuth2Manager). The following sections provide code snippets for the most common OAuth 2.0 flows using the **OAuth2Manager** API. |
| 27 | + |
| 28 | +### Authorization code request |
| 29 | + |
| 30 | +The following example demonstrates how to perform an authorization code request using the **OAuth2Manager** in Windows App SDK: |
| 31 | + |
| 32 | +# [C++](#tab/cpp) |
| 33 | + |
| 34 | +```cpp |
| 35 | +// Get the WindowId for the application window |
| 36 | +Microsoft::UI::WindowId parentWindowId = this->AppWindow().Id(); |
| 37 | + |
| 38 | +AuthRequestParams authRequestParams = AuthRequestParams::CreateForAuthorizationCodeRequest(L"my_client_id", |
| 39 | + Uri(L"my-app:/oauth-callback/")); |
| 40 | +authRequestParams.Scope(L"user:email user:birthday"); |
| 41 | + |
| 42 | +AuthRequestResult authRequestResult = co_await OAuth2Manager::RequestAuthWithParamsAsync(parentWindowId, |
| 43 | + Uri(L"https://my.server.com/oauth/authorize"), authRequestParams); |
| 44 | +if (AuthResponse authResponse = authRequestResult.Response()) |
| 45 | +{ |
| 46 | + //To obtain the authorization code |
| 47 | + //authResponse.Code(); |
| 48 | + |
| 49 | + //To obtain the access token |
| 50 | + DoTokenExchange(authResponse); |
| 51 | +} |
| 52 | +else |
| 53 | +{ |
| 54 | + AuthFailure authFailure = authRequestResult.Failure(); |
| 55 | + NotifyFailure(authFailure.Error(), authFailure.ErrorDescription()); |
| 56 | +} |
| 57 | +``` |
| 58 | +
|
| 59 | +# [C#](#tab/csharp) |
| 60 | +
|
| 61 | +```csharp |
| 62 | +// Get the WindowId for the application window |
| 63 | +Microsoft.UI.WindowId parentWindowId = this.AppWindow.Id; |
| 64 | +
|
| 65 | +AuthRequestParams authRequestParams = AuthRequestParams.CreateForAuthorizationCodeRequest("my_client_id", |
| 66 | + new Uri("my-app:/oauth-callback/")); |
| 67 | +authRequestParams.Scope = "user:email user:birthday"; |
| 68 | +
|
| 69 | +AuthRequestResult authRequestResult = await OAuth2Manager.RequestAuthWithParamsAsync(parentWindowId, |
| 70 | + new Uri("https://my.server.com/oauth/authorize"), authRequestParams); |
| 71 | +
|
| 72 | +if (AuthResponse authResponse == authRequestResult.Response) |
| 73 | +{ |
| 74 | + //To obtain the authorization code |
| 75 | + //authResponse.Code; |
| 76 | +
|
| 77 | + //To obtain the access token |
| 78 | + DoTokenExchange(authResponse); |
| 79 | +} |
| 80 | +else |
| 81 | +{ |
| 82 | + AuthFailure authFailure = authRequestResult.Failure; |
| 83 | + NotifyFailure(authFailure.Error, authFailure.ErrorDescription); |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +--- |
| 88 | + |
| 89 | +### Exchange authorization code for access token |
| 90 | + |
| 91 | +The following example demonstrates how to exchange an authorization code for an access token using the **OAuth2Manager**: |
| 92 | + |
| 93 | +# [C++](#tab/cpp) |
| 94 | + |
| 95 | +```cpp |
| 96 | +AuthResponse authResponse = authRequestResult.Response(); |
| 97 | +TokenRequestParams tokenRequestParams = TokenRequestParams::CreateForAuthorizationCodeRequest(authResponse); |
| 98 | +ClientAuthentication clientAuth = ClientAuthentication::CreateForBasicAuthorization(L"my_client_id", |
| 99 | + L"my_client_secret"); |
| 100 | + |
| 101 | +TokenRequestResult tokenRequestResult = co_await OAuth2Manager::RequestTokenAsync( |
| 102 | + Uri(L"https://my.server.com/oauth/token"), tokenRequestParams, clientAuth); |
| 103 | +if (TokenResponse tokenResponse = tokenRequestResult.Response()) |
| 104 | +{ |
| 105 | + String accessToken = tokenResponse.AccessToken(); |
| 106 | + String tokenType = tokenResponse.TokenType(); |
| 107 | + |
| 108 | + // RefreshToken string null/empty when not present |
| 109 | + if (String refreshToken = tokenResponse.RefreshToken(); !refreshToken.empty()) |
| 110 | + { |
| 111 | + // ExpiresIn is zero when not present |
| 112 | + DateTime expires = winrt::clock::now(); |
| 113 | + if (String expiresIn = tokenResponse.ExpiresIn(); std::stoi(expiresIn) != 0) |
| 114 | + { |
| 115 | + expires += std::chrono::seconds(static_cast<int64_t>(std::stoi(expiresIn))); |
| 116 | + } |
| 117 | + else |
| 118 | + { |
| 119 | + // Assume a duration of one hour |
| 120 | + expires += std::chrono::hours(1); |
| 121 | + } |
| 122 | + |
| 123 | + //Schedule a refresh of the access token |
| 124 | + myAppState.ScheduleRefreshAt(expires, refreshToken); |
| 125 | + } |
| 126 | + |
| 127 | + // Use the access token for resources |
| 128 | + DoRequestWithToken(accessToken, tokenType); |
| 129 | +} |
| 130 | +else |
| 131 | +{ |
| 132 | + TokenFailure tokenFailure = tokenRequestResult.Failure(); |
| 133 | + NotifyFailure(tokenFailure.Error(), tokenFailure.ErrorDescription()); |
| 134 | +} |
| 135 | +``` |
| 136 | +
|
| 137 | +# [C#](#tab/csharp) |
| 138 | +
|
| 139 | +```csharp |
| 140 | +AuthResponse authResponse = authRequestResult.Response; |
| 141 | +TokenRequestParams tokenRequestParams = TokenRequestParams.CreateForAuthorizationCodeRequest(authResponse); |
| 142 | +ClientAuthentication clientAuth = ClientAuthentication.CreateForBasicAuthorization("my_client_id", |
| 143 | + "my_client_secret"); |
| 144 | +
|
| 145 | +TokenRequestResult tokenRequestResult = await OAuth2Manager.RequestTokenAsync( |
| 146 | + new Uri("https://my.server.com/oauth/token"), tokenRequestParams, clientAuth); |
| 147 | +
|
| 148 | +if (TokenResponse tokenResponse == tokenRequestResult.Response) |
| 149 | +{ |
| 150 | + string accessToken = tokenResponse.AccessToken; |
| 151 | + string tokenType = tokenResponse.TokenType; |
| 152 | +
|
| 153 | + // RefreshToken string null/empty when not present |
| 154 | + if (!string.IsNullOrEmpty(tokenResponse.RefreshToken)) |
| 155 | + { |
| 156 | + // ExpiresIn is zero when not present |
| 157 | + DateTime expires = DateTime.Now; |
| 158 | + if (tokenResponse.ExpiresIn != 0) |
| 159 | + { |
| 160 | + expires += TimeSpan.FromSeconds(tokenResponse.ExpiresIn); |
| 161 | + } |
| 162 | + else |
| 163 | + { |
| 164 | + // Assume a duration of one hour |
| 165 | + expires += TimeSpan.FromHours(1); |
| 166 | + } |
| 167 | +
|
| 168 | + //Schedule a refresh of the access token |
| 169 | + myAppState.ScheduleRefreshAt(expires, tokenResponse.RefreshToken); |
| 170 | + } |
| 171 | +
|
| 172 | + // Use the access token for resources |
| 173 | + DoRequestWithToken(accessToken, tokenType); |
| 174 | +} |
| 175 | +else |
| 176 | +{ |
| 177 | + TokenFailure tokenFailure = tokenRequestResult.Failure; |
| 178 | + NotifyFailure(tokenFailure.Error, tokenFailure.ErrorDescription); |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | +--- |
| 183 | + |
| 184 | +### Refresh an access token |
| 185 | + |
| 186 | +The following example shows how to refresh an access token using the **OAuth2Manager**'s [RefreshTokenAsync](/windows/windows-app-sdk/api/winrt/microsoft.security.authentication.oauth.oauth2manager.requesttokenasync) method: |
| 187 | + |
| 188 | +# [C++](#tab/cpp) |
| 189 | + |
| 190 | +```cpp |
| 191 | +TokenRequestParams tokenRequestParams = TokenRequestParams::CreateForRefreshToken(refreshToken); |
| 192 | +ClientAuthentication clientAuth = ClientAuthentication::CreateForBasicAuthorization(L"my_client_id", |
| 193 | + L"my_client_secret"); |
| 194 | +TokenRequestResult tokenRequestResult = co_await OAuth2Manager::RequestTokenAsync( |
| 195 | + Uri(L"https://my.server.com/oauth/token"), tokenRequestParams, clientAuth)); |
| 196 | +if (TokenResponse tokenResponse = tokenRequestResult.Response()) |
| 197 | +{ |
| 198 | + UpdateToken(tokenResponse.AccessToken(), tokenResponse.TokenType(), tokenResponse.ExpiresIn()); |
| 199 | + |
| 200 | + //Store new refresh token if present |
| 201 | + if (String refreshToken = tokenResponse.RefreshToken(); !refreshToken.empty()) |
| 202 | + { |
| 203 | + // ExpiresIn is zero when not present |
| 204 | + DateTime expires = winrt::clock::now(); |
| 205 | + if (String expiresInStr = tokenResponse.ExpiresIn(); !expiresInStr.empty()) |
| 206 | + { |
| 207 | + int expiresIn = std::stoi(expiresInStr); |
| 208 | + if (expiresIn != 0) |
| 209 | + { |
| 210 | + expires += std::chrono::seconds(static_cast<int64_t>(expiresIn)); |
| 211 | + } |
| 212 | + } |
| 213 | + else |
| 214 | + { |
| 215 | + // Assume a duration of one hour |
| 216 | + expires += std::chrono::hours(1); |
| 217 | + } |
| 218 | + |
| 219 | + //Schedule a refresh of the access token |
| 220 | + myAppState.ScheduleRefreshAt(expires, refreshToken); |
| 221 | + } |
| 222 | +} |
| 223 | +else |
| 224 | +{ |
| 225 | + TokenFailure tokenFailure = tokenRequestResult.Failure(); |
| 226 | + NotifyFailure(tokenFailure.Error(), tokenFailure.ErrorDescription()); |
| 227 | +} |
| 228 | +``` |
| 229 | +
|
| 230 | +# [C#](#tab/csharp) |
| 231 | +
|
| 232 | +```csharp |
| 233 | +TokenRequestParams tokenRequestParams = TokenRequestParams.CreateForRefreshToken(refreshToken); |
| 234 | +ClientAuthentication clientAuth = ClientAuthentication.CreateForBasicAuthorization("my_client_id", |
| 235 | + "my_client_secret"); |
| 236 | +TokenRequestResult tokenRequestResult = await OAuth2Manager.RequestTokenAsync( |
| 237 | + new Uri("https://my.server.com/oauth/token"), tokenRequestParams, clientAuth); |
| 238 | +if (TokenResponse tokenResponse == tokenRequestResult.Response) |
| 239 | +{ |
| 240 | + UpdateToken(tokenResponse.AccessToken, tokenResponse.TokenType, tokenResponse.ExpiresIn); |
| 241 | +
|
| 242 | + //Store new refresh token if present |
| 243 | + if (!string.IsNullOrEmpty(tokenResponse.RefreshToken)) |
| 244 | + { |
| 245 | + // ExpiresIn is zero when not present |
| 246 | + DateTime expires = DateTime.Now; |
| 247 | + if (tokenResponse.ExpiresIn != 0) |
| 248 | + { |
| 249 | + expires += TimeSpan.FromSeconds(tokenResponse.ExpiresIn); |
| 250 | + } |
| 251 | + else |
| 252 | + { |
| 253 | + // Assume a duration of one hour |
| 254 | + expires += TimeSpan.FromHours(1); |
| 255 | + } |
| 256 | +
|
| 257 | + //Schedule a refresh of the access token |
| 258 | + myAppState.ScheduleRefreshAt(expires, tokenResponse.RefreshToken); |
| 259 | + } |
| 260 | +} |
| 261 | +else |
| 262 | +{ |
| 263 | + TokenFailure tokenFailure = tokenRequestResult.Failure; |
| 264 | + NotifyFailure(tokenFailure.Error, tokenFailure.ErrorDescription); |
| 265 | +} |
| 266 | +``` |
| 267 | + |
| 268 | +--- |
| 269 | + |
| 270 | +### Complete an authorization request |
| 271 | + |
| 272 | +Finally, to complete an authorization request from a protocol activation, your app should handle the [AppInstance.Activated](/windows/windows-app-sdk/api/winrt/microsoft.windows.applifecycle.appinstance.activated) event. This is required when having custom redirect logic. A full example is available on [GitHub](https://github.com/microsoft/WindowsAppSDK-Samples/tree/release/experimental/Samples/OAuth2Manager). |
| 273 | + |
| 274 | +Use the following code: |
| 275 | + |
| 276 | +# [C++](#tab/cpp) |
| 277 | + |
| 278 | +```cpp |
| 279 | +void App::OnActivated(const IActivatedEventArgs& args) |
| 280 | +{ |
| 281 | + if (args.Kind() == ActivationKind::Protocol) |
| 282 | + { |
| 283 | + auto protocolArgs = args.as<ProtocolActivatedEventArgs>(); |
| 284 | + if (OAuth2Manager::CompleteAuthRequest(protocolArgs.Uri())) |
| 285 | + { |
| 286 | + TerminateCurrentProcess(); |
| 287 | + } |
| 288 | + |
| 289 | + DisplayUnhandledMessageToUser(); |
| 290 | + } |
| 291 | +} |
| 292 | +``` |
| 293 | +
|
| 294 | +# [C#](#tab/csharp) |
| 295 | +
|
| 296 | +```csharp |
| 297 | +protected override void OnActivated(IActivatedEventArgs args) |
| 298 | +{ |
| 299 | + if (args.Kind == ActivationKind.Protocol) |
| 300 | + { |
| 301 | + ProtocolActivatedEventArgs protocolArgs = args as ProtocolActivatedEventArgs; |
| 302 | + if (OAuth2Manager.CompleteAuthRequest(protocolArgs.Uri)) |
| 303 | + { |
| 304 | + TerminateCurrentProcess(); |
| 305 | + } |
| 306 | +
|
| 307 | + DisplayUnhandledMessageToUser(); |
| 308 | + } |
| 309 | +} |
| 310 | +``` |
| 311 | + |
| 312 | +--- |
| 313 | + |
| 314 | +## Related content |
| 315 | + |
| 316 | +- [WebAuthenticationBroker](/uwp/api/windows.security.authentication.web.webauthenticationbroker) |
| 317 | +- [OAuth2Manager](/windows/windows-app-sdk/api/winrt/microsoft.security.authentication.oauth.oauth2manager) |
| 318 | +- [PKCE RFC 7636](https://tools.ietf.org/html/rfc7636) |
0 commit comments