Skip to content

Flutter windows desktop support#656

Open
NandanPrabhu wants to merge 21 commits intomainfrom
SDK-6071
Open

Flutter windows desktop support#656
NandanPrabhu wants to merge 21 commits intomainfrom
SDK-6071

Conversation

@NandanPrabhu
Copy link
Contributor

@NandanPrabhu NandanPrabhu commented Sep 8, 2025

  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

📋 Changes

This PR adds native Windows desktop support to the auth0_flutter SDK, enabling Auth0 Universal Login on Flutter Windows apps using the OAuth 2.0 Authorization Code Flow with PKCE. The implementation is a C++ Flutter plugin that integrates with the existing platform-interface layer without modifying the mobile (iOS/Android) code paths.


New: WindowsWebAuthentication class

A dedicated Windows authentication class exposed via Auth0.windowsWebAuthentication(). Unlike the mobile WebAuthentication class, this:

  • Requires redirectUrl explicitly (no platform default exists on Windows)
  • Does not auto-store credentials in CredentialsManager (no Keychain/Keystore on Windows)
  • Exposes a parameters map for Windows-specific configuration
final auth0 = Auth0('DOMAIN', 'CLIENT_ID');

// Simple: Auth0 redirects directly to the app via custom scheme
final credentials = await auth0.windowsWebAuthentication().login(
  redirectUrl: 'auth0flutter://callback',
);

// Intermediary server: HTTPS endpoint forwards to auth0flutter://callback
final credentials = await auth0.windowsWebAuthentication().login(
  redirectUrl: 'https://your-server.com/callback',
  parameters: {'authTimeoutSeconds': '300'},
);

authTimeoutSeconds (default '180'): How long the plugin polls for the OAuth callback before returning USER_CANCELLED. Increase for slow MFA flows; decrease for fast failure in tests.

Regardless of what redirectUrl is registered with Auth0, the Windows plugin always wakes the app by listening on the auth0flutter://callback custom scheme. When using an intermediary server, the server must forward the callback to auth0flutter://callback?code=…&state=….


New: Windows C++ plugin (auth0_flutter/windows/)

Component Purpose
login_web_auth_request_handler.cpp Orchestrates the full OAuth 2.0 + PKCE login flow
logout_web_auth_request_handler.cpp Builds and opens the Auth0 logout URL
oauth_helpers.cpp PKCE code verifier/challenge generation; auth0flutter:// callback polling
auth0_client.cpp HTTP token exchange via cpprestsdk
id_token_validator.cpp OpenID Connect ID token validation
id_token_signature_validator.cpp RS256 signature verification via OpenSSL
jwt_util.cpp JWT header/payload decoding
token_decoder.cpp Maps token exchange response → Credentials struct
user_profile.cpp / user_identity.cpp OIDC claims → UserProfile struct
time_util.cpp ISO 8601 / RFC 3339 timestamp parsing
url_utils.cpp RFC 3986 URL parsing and query parameter extraction
windows_utils.cpp WideToUtf8, BringFlutterWindowToFront

Authentication flow:

  1. Generate PKCE code_verifier (32 cryptographically random bytes via RAND_bytes) and code_challenge (SHA-256 via OpenSSL, base64-URL encoded)
  2. Generate a random state value for CSRF protection
  3. Build the Auth0 /authorize URL with all parameters RFC 3986-encoded
  4. Open the URL in the system default browser via ShellExecuteA
  5. Poll PLUGIN_STARTUP_URL environment variable (set by Windows when the app is launched via the auth0flutter:// custom scheme) every 200 ms until the callback arrives or the timeout expires
  6. Validate state to prevent CSRF; extract code
  7. Exchange code + code_verifier for tokens via POST to /oauth/token
  8. Validate the ID token (issuer, audience, expiry, auth_time, nonce, RS256 signature)
  9. Bring the Flutter window back to the foreground and return credentials

Key design decisions:

  • The app always listens on auth0flutter://callback (kDefaultRedirectUri). The redirectUrl sent to Auth0 may differ (e.g. an HTTPS intermediary server URL); that server is responsible for forwarding to auth0flutter://callback?code=…&state=….
  • Internal plugin parameters (authTimeoutSeconds) are consumed before building the authorize URL and are not appended to it.
  • The authentication flow runs on a background std::thread to avoid blocking the Flutter UI thread.
  • openid scope is always enforced even when not explicitly passed, as required by OpenID Connect.
  • All URL parameters are RFC 3986 percent-encoded to prevent injection and handle special characters in values such as redirect URIs and scopes.

New: vcpkg.json dependency manifest

Manages C++ dependencies via vcpkg, integrating automatically with CMake through the vcpkg toolchain file set by Flutter during flutter build windows:

Library Purpose
cpprestsdk Async HTTP client and JSON for token exchange
openssl RAND_bytes (PKCE entropy), SHA-256 (code challenge), RS256 signature verification, TLS
boost-system / boost-date-time / boost-regex Transitive cpprestsdk dependencies

New: Unit tests (Google Test, auth0_flutter/windows/test/)

Test file Coverage
oauth_helpers_test.cpp Base64-URL encoding, code verifier/challenge generation, callback timeout behaviour
id_token_validator_test.cpp Issuer, audience, expiry, auth_time, nonce, leeway validation
jwt_util_test.cpp JWT splitting, header/payload decoding
time_util_test.cpp ISO 8601 and RFC 3339 timestamp parsing
token_decoder_test.cpp Token response → Credentials mapping
url_utils_test.cpp URL parsing, query string extraction, RFC 3986 encoding
user_identity_test.cpp Identity claims extraction
user_profile_test.cpp User profile claims mapping
windows_utils_test.cpp WideToUtf8 wide-to-UTF-8 conversion

Tests are compiled as a separate auth0_flutter_tests executable and registered with CTest, enabled via -DAUTH0_FLUTTER_ENABLE_TESTS=ON.


New: CI pipeline (.github/workflows/main.yml)

Added a windows-tests job that installs vcpkg dependencies, builds the test executable with CMake, and runs all C++ unit tests via CTest on windows-latest.

📎 References


🎯 Testing

Automated — C++ unit tests (Windows)

cd auth0_flutter/windows
cmake -B build -S . \
  -DCMAKE_TOOLCHAIN_FILE=<vcpkg-root>/scripts/buildsystems/vcpkg.cmake \
  -DAUTH0_FLUTTER_ENABLE_TESTS=ON
cmake --build build
cd build && ctest --output-on-failure

All 9 test suites pass.

Automated — Flutter unit tests (any platform)

cd auth0_flutter
flutter test test/mobile/web_authentication_test.dart
# 34/34 tests pass

Manual — end-to-end on Windows

Prerequisites:

  1. Register auth0flutter as a custom URL scheme pointing to your app executable (via installer or registry)
  2. Add auth0flutter://callback to Allowed Callback URLs in the Auth0 dashboard
cd auth0_flutter/example
flutter run -d windows
  • Tap Log in → system default browser opens Auth0 Universal Login
  • Authenticate; browser redirects to auth0flutter://callback?code=…&state=…
  • Windows launches the app via the registered custom scheme; the Flutter window comes to the foreground and displays the returned credentials

To test the intermediary server pattern, point redirectUrl at an HTTPS endpoint that reads the code and state query parameters and responds with a redirect to auth0flutter://callback?code=…&state=….

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 61 out of 68 changed files in this pull request and generated 12 comments.

Files not reviewed (4)
  • .idea/.gitignore: Language not supported
  • .idea/auth0-flutter.iml: Language not supported
  • .idea/modules.xml: Language not supported
  • .idea/vcs.xml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +16 to +22
Credentials creds = DecodeTokenResponse(json);

EXPECT_EQ(creds.accessToken, "test_access_token");
EXPECT_EQ(creds.tokenType, "Bearer");
EXPECT_FALSE(creds.idToken.empty()); // Empty, not uninitialized
EXPECT_FALSE(creds.refreshToken.has_value());
EXPECT_FALSE(creds.expiresIn.has_value());
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the minimal token response case, DecodeTokenResponse leaves creds.idToken as an empty string when id_token is absent, but this test asserts it is non-empty (EXPECT_FALSE(creds.idToken.empty())). This assertion will fail; either require id_token in DecodeTokenResponse (throw if missing) or update the test expectation to allow an empty idToken for non-OIDC responses.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +27
static std::optional<bool> GetBoolOrFalse(
const EncodableMap& map,
const std::string& key) {
auto it = map.find(EncodableValue(key));
if (it == map.end()) return std::nullopt;
if (!std::holds_alternative<bool>(it->second)) return std::nullopt;
return std::get<bool>(it->second);
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GetBoolOrFalse returns std::nullopt when the key is missing or not a bool, despite the name suggesting a default false. This also conflicts with the added unit test that calls .value() expecting false for non-bool values. Either return false in those cases, rename the helper to reflect optional<bool> semantics, or adjust the tests/callers to handle nullopt safely.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
import 'dart:io' show Platform;

import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart';

import '../../auth0_flutter.dart';
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Platform is imported but not used anywhere in this file, which will trigger an unused import warning in analyzer/lints. Remove the import or use it (e.g., by asserting Windows-only usage) to keep the package analysis clean.

Copilot uses AI. Check for mistakes.
Comment on lines +20 to +27
/// Basic login:
/// ```dart
/// final auth0 = Auth0('DOMAIN', 'CLIENT_ID');
/// final result = await auth0.webAuthentication().login(
/// redirectUrl: 'http://localhost:8080/callback',
/// );
/// final accessToken = result.accessToken;
/// ```
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The “Basic login” doc snippet calls auth0.webAuthentication().login(...), but this class is Windows-specific and the rest of the docs reference windowsWebAuthentication(). This example should use auth0.windowsWebAuthentication().login(...) to avoid misleading users on Windows.

Copilot uses AI. Check for mistakes.
Comment on lines +331 to +333
- name: Set up vcpkg
uses: lukka/run-vcpkg@v11 # pin@v11
with:
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This workflow introduces unpinned third-party actions (lukka/run-vcpkg@v11 and multiple actions/upload-artifact@v6). Elsewhere in this workflow actions are pinned to commit SHAs; these should be pinned as well to reduce supply-chain risk and keep consistency with the repo’s existing CI security posture.

Copilot uses AI. Check for mistakes.
Comment on lines +111 to +116
# === Tests ===
option(AUTH0_FLUTTER_ENABLE_TESTS "Build auth0_flutter unit tests" ON)

if (AUTH0_FLUTTER_ENABLE_TESTS)
enable_testing()

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unit tests are enabled by default (AUTH0_FLUTTER_ENABLE_TESTS is ON), which means normal plugin consumers will download/build GoogleTest via FetchContent during flutter build windows. Default this option to OFF (and only enable in CI/dev) to avoid slow, network-dependent, and potentially policy-violating builds for end users.

Copilot uses AI. Check for mistakes.
/// ```dart
/// final auth0 = Auth0('DOMAIN', 'CLIENT_ID');
/// final result = await auth0.windowsWebAuthentication().login(
/// redirectUrl: 'auth0-flutter://callback',
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows WebAuth usage example uses redirectUrl: 'auth0-flutter://callback', but the Windows implementation and docs elsewhere use auth0flutter://callback (no hyphen). This example should be corrected to avoid users registering/using the wrong custom scheme.

Suggested change
/// redirectUrl: 'auth0-flutter://callback',
/// redirectUrl: 'auth0flutter://callback',

Copilot uses AI. Check for mistakes.
Comment on lines 80 to 95
@@ -86,11 +87,37 @@ If your Auth0 domain was `company.us.auth0.com` and your package name (Android)
- Android: `https://company.us.auth0.com/android/com.company.myapp/callback`
- iOS: `https://company.us.auth0.com/ios/com.company.myapp/callback,com.company.myapp://company.us.auth0.com/ios/com.company.myapp/callback`
- macOS: `https://company.us.auth0.com/macos/com.company.myapp/callback,com.company.myapp://company.us.auth0.com/macos/com.company.myapp/callback`
- Windows: `https://your-app.example.com/callback` (your intermediary server endpoint)

</details>

> 💡 **Windows**: The Windows implementation uses a custom scheme callback architecture (`auth0flutter://callback`). This requires an intermediary server to receive the Auth0 callback and forward it to your Windows app via the custom protocol. The intermediary server URL (e.g., `https://your-app.example.com/callback`) should be configured as the callback URL in your Auth0 dashboard. The server should handle the Auth0 redirect and trigger the `auth0flutter://` protocol to activate your app with the authorization code and state parameters.

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README states that Windows “requires an intermediary server”, but the new WindowsWebAuthentication docs and PR description both describe a direct callback option using auth0flutter://callback without a server. The README should document both patterns (direct custom-scheme redirect and intermediary HTTPS redirect) to avoid contradictory setup guidance.

Copilot uses AI. Check for mistakes.
Comment on lines +35 to +45
std::string urlEncode(const std::string &str)
{
std::ostringstream encoded;
encoded.fill('0');
encoded << std::hex << std::uppercase;

for (unsigned char c : str)
{
if (isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~')
{
encoded << c;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

urlEncode uses isalnum without including <cctype> (and without qualifying it as std::isalnum), which can lead to build failures or locale-dependent behavior. Include <cctype> and call std::isalnum(static_cast<unsigned char>(c)) (or equivalent) for a well-defined implementation.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +25
#include "url_utils.h"
#include <sstream>

namespace auth0_flutter
{

std::string UrlDecode(const std::string &str)
{
std::string out;
out.reserve(str.size());
for (size_t i = 0; i < str.size(); ++i)
{
char c = str[i];
if (c == '%')
{
if (i + 2 < str.size())
{
std::string hex = str.substr(i + 1, 2);
char decoded = (char)strtol(hex.c_str(), nullptr, 16);
out.push_back(decoded);
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UrlDecode calls strtol but this file doesn’t include <cstdlib>/<stdlib.h>, which can cause missing-declaration build errors depending on the toolchain. Add the appropriate standard header include for strtol.

Copilot uses AI. Check for mistakes.
final Account _account;
final UserAgent _userAgent;
final String? _scheme;
final CredentialsManager? _credentialsManager;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the _scheme and _credentialsManager in this class.
scheme is used for generating the redirection url. But since we are asking the user to explicitly add the redirect url for Windows, this serves no purpose. Also we currently don't have the any credentials storing support. We can add this when we add that support

/// Auth0 redirects directly to the app; no server is required.
/// - `https://your-server.com/callback` — an HTTPS endpoint on an
/// intermediary server that receives the Auth0 redirect and forwards it
/// to the app via the `auth0flutter://callback` scheme.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intermediary server redirect is a workaround . So we shouldn't add that in the official docs. We can add the workaround flow and related references in Examples.md file or a new file

/// **Example:**
/// ```dart
/// await auth0.webAuthentication().login(
/// redirectUrl: 'http://localhost:8080/callback',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets give a windows specific redirect url here

/// [federated] controls whether to perform federated logout, which also logs
/// the user out from their identity provider.
Future<void> logout({
final String? returnTo,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't his also be a required field to return back post login ?

///
static void cancel() {
Auth0FlutterWebAuthPlatform.instance.cancel();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this cancel method if it doesn't apply to Windows

await Auth0('test-domain', 'test-clientId')
.webAuthentication()
.login(parameters: {
'appCallbackUrl': 'myapp://callback',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appCallbackUrl property is not being used anymore. Update the tests to avoid any confusion

.single as WebAuthRequest<WebAuthLoginOptions>;
// ignore: inference_failure_on_collection_literal
expect(verificationResult.options.parameters, {
'appCallbackUrl': 'myapp://callback',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

appCallbackUrl property is not being used anymore. Update the tests to avoid any confusion

expect(verificationResult.options.useEphemeralSession, false);
});

test('passes custom parameters to platform', () async {
Copy link
Contributor

@pmathew92 pmathew92 Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should have a new testClass for WindowsWebAuthentication. The new test class should test specific scenarios like mandatory redirect uri to login and logout method etc

// If no scopes provided, use just "openid"
scopeStr = "openid";
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need such a long logic for scopes? Can't it be simple as append scopes from the list to the existing default scope string ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is required to append open id if not already present in the scopes

Copy link
Contributor

@pmathew92 pmathew92 Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I know we need the openId scope always. What I meant is to simplify this logic a bit if we can . For example we can define a set with default scopes like
std::set<std::string> scopes = {"openid", "profile", "email"};
And then add to this set from the scope list passed. This will ensure no duplicates are added.
In the end just append all the items to a single string.
This will be more readable and straightforward. No need for all the flag check and everything for openid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants