diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index afefc719d..80086cc27 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -307,6 +307,72 @@ jobs:
name: Test results
path: 'auth0_flutter/example/build/app/reports/androidTests/*.xml'
+ test-windows-unit:
+ name: Run native Windows unit tests
+ runs-on: windows-latest
+ environment: ${{ github.event.pull_request.head.repo.fork && 'external' || 'internal' }}
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3
+
+ - name: Install Flutter
+ uses: subosito/flutter-action@fd55f4c5af5b953cc57a2be44cb082c8f6635e8e # pin@v2.21.0
+ with:
+ flutter-version: ${{ env.flutter }}
+ channel: stable
+ cache: true
+
+ - name: Add example/.env
+ working-directory: auth0_flutter
+ run: Copy-Item example/.env.example example/.env
+ shell: powershell
+
+ - name: Set up vcpkg
+ uses: lukka/run-vcpkg@v11 # pin@v11
+ with:
+ vcpkgDirectory: '${{ github.workspace }}/vcpkg'
+ vcpkgGitCommitId: '66c0373dc7fca549e5803087b9487edfe3aca0a1'
+
+ - name: Install vcpkg dependencies
+ run: |
+ ${{ github.workspace }}\vcpkg\vcpkg install cpprestsdk:x64-windows openssl:x64-windows boost-system:x64-windows boost-date-time:x64-windows boost-regex:x64-windows
+ shell: cmd
+
+ - name: Build Windows example app
+ working-directory: auth0_flutter/example
+ run: flutter build windows --debug
+ env:
+ CMAKE_TOOLCHAIN_FILE: ${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake
+
+ - name: Install OpenCppCoverage
+ run: choco install opencppcoverage
+ shell: powershell
+
+ - name: Build Windows unit tests
+ working-directory: auth0_flutter/windows
+ run: |
+ cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake -DAUTH0_FLUTTER_ENABLE_TESTS=ON -DCMAKE_BUILD_TYPE=Debug
+ cmake --build build --config Debug
+ shell: cmd
+
+ - name: Run Windows unit tests with coverage
+ working-directory: auth0_flutter/windows
+ run: |
+ & "C:\Program Files\OpenCppCoverage\OpenCppCoverage.exe" `
+ --sources ${{ github.workspace }}\auth0_flutter\windows `
+ --excluded_sources ${{ github.workspace }}\auth0_flutter\windows\test `
+ --export_type cobertura:coverage.xml `
+ --export_type html:coverage_html `
+ -- .\build\Debug\auth0_flutter_tests.exe
+ shell: powershell
+
+ - name: Upload coverage report
+ uses: actions/upload-artifact@v6
+ with:
+ name: Windows coverage
+ path: auth0_flutter/windows/coverage.xml
+
# test-android-smoke:
# name: Run native Android smoke tests using API-level ${{ matrix.android-api }}
# runs-on: macos-latest-xl
@@ -419,7 +485,8 @@ jobs:
test-auth0_flutter,
test-auth0_flutter_platform_interface,
test-ios-unit,
- test-android-unit
+ test-android-unit,
+ test-windows-unit
]
steps:
@@ -450,6 +517,12 @@ jobs:
name: Android coverage
path: coverage/android
+ - name: Download coverage report for Windows
+ uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
+ with:
+ name: Windows coverage
+ path: coverage/windows
+
- name: Upload coverage report for auth0_flutter
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
with:
@@ -477,3 +550,10 @@ jobs:
name: Auth0 Flutter
flags: auth0_flutter_android
directory: coverage/android
+
+ - name: Upload coverage report for Windows
+ uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de
+ with:
+ name: Auth0 Flutter
+ flags: auth0_flutter_windows
+ directory: coverage/windows
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..b08d30f1a
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,87 @@
+{
+ "files.associations": {
+ "variant": "cpp",
+ "tuple": "cpp",
+ "utility": "cpp",
+ "array": "cpp",
+ "vector": "cpp",
+ "xstring": "cpp",
+ "xutility": "cpp",
+ "algorithm": "cpp",
+ "any": "cpp",
+ "atomic": "cpp",
+ "bit": "cpp",
+ "bitset": "cpp",
+ "chrono": "cpp",
+ "cmath": "cpp",
+ "compare": "cpp",
+ "complex": "cpp",
+ "concepts": "cpp",
+ "deque": "cpp",
+ "exception": "cpp",
+ "format": "cpp",
+ "forward_list": "cpp",
+ "fstream": "cpp",
+ "functional": "cpp",
+ "future": "cpp",
+ "iosfwd": "cpp",
+ "istream": "cpp",
+ "iterator": "cpp",
+ "limits": "cpp",
+ "list": "cpp",
+ "map": "cpp",
+ "memory": "cpp",
+ "new": "cpp",
+ "numeric": "cpp",
+ "optional": "cpp",
+ "queue": "cpp",
+ "random": "cpp",
+ "ratio": "cpp",
+ "regex": "cpp",
+ "string": "cpp",
+ "system_error": "cpp",
+ "type_traits": "cpp",
+ "unordered_map": "cpp",
+ "xlocale": "cpp",
+ "xlocnum": "cpp",
+ "xmemory": "cpp",
+ "xtr1common": "cpp",
+ "xtree": "cpp",
+ "cctype": "cpp",
+ "charconv": "cpp",
+ "clocale": "cpp",
+ "codecvt": "cpp",
+ "condition_variable": "cpp",
+ "csetjmp": "cpp",
+ "cstddef": "cpp",
+ "cstdint": "cpp",
+ "cstdio": "cpp",
+ "cstdlib": "cpp",
+ "cstring": "cpp",
+ "ctime": "cpp",
+ "cwchar": "cpp",
+ "initializer_list": "cpp",
+ "iomanip": "cpp",
+ "ios": "cpp",
+ "iostream": "cpp",
+ "locale": "cpp",
+ "mutex": "cpp",
+ "ostream": "cpp",
+ "set": "cpp",
+ "sstream": "cpp",
+ "stdexcept": "cpp",
+ "stop_token": "cpp",
+ "streambuf": "cpp",
+ "thread": "cpp",
+ "typeinfo": "cpp",
+ "unordered_set": "cpp",
+ "xfacet": "cpp",
+ "xhash": "cpp",
+ "xiosbase": "cpp",
+ "xlocbuf": "cpp",
+ "xlocinfo": "cpp",
+ "xlocmes": "cpp",
+ "xlocmon": "cpp",
+ "xloctime": "cpp"
+ }
+}
\ No newline at end of file
diff --git a/auth0_flutter/.metadata b/auth0_flutter/.metadata
index fe4a72344..5a30c6421 100644
--- a/auth0_flutter/.metadata
+++ b/auth0_flutter/.metadata
@@ -4,7 +4,39 @@
# This file should be version controlled and should not be manually edited.
version:
- revision: 097d3313d8e2c7f901932d63e537c1acefb87800
- channel: stable
+ revision: "ea121f8859e4b13e47a8f845e4586164519588bc"
+ channel: "[user-branch]"
project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ - platform: android
+ create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ - platform: ios
+ create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ - platform: macos
+ create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ - platform: web
+ create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ - platform: windows
+ create_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+ base_revision: ea121f8859e4b13e47a8f845e4586164519588bc
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/auth0_flutter/README.md b/auth0_flutter/README.md
index d2f8c45e1..cf9d01080 100644
--- a/auth0_flutter/README.md
+++ b/auth0_flutter/README.md
@@ -28,11 +28,11 @@ We're excited to announce the release of auth0_flutter v2.0.0!
### Requirements
-| Flutter | Android | iOS | macOS |
-| :---------- | :-------------- | :---------------- | :---------------- |
-| SDK 3.24.0+ | Android API 21+ | iOS 14+ | macOS 11+ |
-| Dart 3.5.0+ | Java 8+ | Swift 5.9+ | Swift 5.9+ |
-| | | Xcode 15.x / 16.x | Xcode 15.x / 16.x |
+| Flutter | Android | iOS | macOS | Windows |
+| :---------- | :-------------- | :---------------- | :---------------- | :---------------- |
+| SDK 3.24.0+ | Android API 21+ | iOS 14+ | macOS 11+ | Windows 10+ |
+| Dart 3.5.0+ | Java 8+ | Swift 5.9+ | Swift 5.9+ | C++ 17 |
+| | | Xcode 15.x / 16.x | Xcode 15.x / 16.x | |
### Installation
@@ -77,6 +77,7 @@ Under the **Application URIs** section of the **Settings** page, configure the f
- Android: `SCHEME://YOUR_DOMAIN/android/YOUR_PACKAGE_NAME/callback`
- iOS: `https://YOUR_DOMAIN/ios/YOUR_BUNDLE_ID/callback,YOUR_BUNDLE_ID://YOUR_DOMAIN/ios/YOUR_BUNDLE_ID/callback`
- macOS: `https://YOUR_DOMAIN/macos/YOUR_BUNDLE_ID/callback,YOUR_BUNDLE_ID://YOUR_DOMAIN/macos/YOUR_BUNDLE_ID/callback`
+- Windows: `http://localhost:8080/callback`
Example
@@ -86,9 +87,12 @@ 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: `http://localhost:8080/callback`
+> π‘ **Windows**: The Windows implementation uses a localhost HTTP callback by default, which provides a friendly "Authentication Successful!" page with auto-close functionality instead of leaving the browser tab stuck. **You can customize this page with your own HTML** (inline or hosted URL). See the [Windows README](windows/README.md) for customization details.
+
Take note of the **client ID** and **domain** values under the **Basic Information** section. You'll need these values in the next step.
#### π Web
@@ -407,19 +411,25 @@ Check the [FAQ](FAQ.md) for more information about the alert box that pops up **
- [Retrieve stored credentials](EXAMPLES.md#retrieve-stored-credentials) - fetch the user's credentials from the storage, automatically renewing them if they have expired.
- [Retrieve user information](EXAMPLES.md#retrieve-user-information) - fetch the latest user information from the `/userinfo` endpoint.
+### πͺ Windows
+
+- [Windows Authentication Setup](windows/README.md) - detailed guide for Windows-specific authentication configuration with HTTP callback support.
+
### π Web
- [Handling credentials on the web](EXAMPLES.md#handling-credentials-on-the-web) - how to check and retrieve credentials on the web platform.
## API reference
-### π± Mobile/macOS
+### π± Mobile/macOS/Windows
#### Web Authentication
- [login](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/WebAuthentication/login.html)
- [logout](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/WebAuthentication/logout.html)
+> π‘ **Windows**: Web Authentication on Windows uses a localhost HTTP callback by default for the best user experience. See the [Windows README](windows/README.md) for configuration details.
+
#### API
- [login](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/AuthenticationApi/login.html)
@@ -432,6 +442,8 @@ Check the [FAQ](FAQ.md) for more information about the alert box that pops up **
#### Credentials Manager
+> β οΈ **Note**: Credentials Manager is available on Mobile and macOS platforms only. Windows does not currently support credential storage.
+
- [credentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/DefaultCredentialsManager/credentials.html)
- [hasValidCredentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/DefaultCredentialsManager/hasValidCredentials.html)
- [storeCredentials](https://pub.dev/documentation/auth0_flutter/latest/auth0_flutter/DefaultCredentialsManager/storeCredentials.html)
diff --git a/auth0_flutter/example/.env.example b/auth0_flutter/example/.env.example
index 99a2d408b..894cc348f 100644
--- a/auth0_flutter/example/.env.example
+++ b/auth0_flutter/example/.env.example
@@ -14,4 +14,4 @@ AUTH0_CLIENT_ID=YOUR_AUTH0_CLIENT_ID
# settings page of your Auth0 application with the custom scheme value.
# 2. Update the scheme value in android/app/src/main/res/values/strings.xml
#
-AUTH0_CUSTOM_SCHEME=YOUR_AUTH0_CUSTOM_SCHEME
+AUTH0_CUSTOM_SCHEME=YOUR_AUTH0_CUSTOM_SCHEME
\ No newline at end of file
diff --git a/auth0_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/auth0_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index b891863c2..792fc0474 100644
--- a/auth0_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/auth0_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -81,6 +81,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
+ enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
diff --git a/auth0_flutter/example/lib/example_app.dart b/auth0_flutter/example/lib/example_app.dart
index a27f8d8f7..ea55bb78d 100644
--- a/auth0_flutter/example/lib/example_app.dart
+++ b/auth0_flutter/example/lib/example_app.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:io' show Platform;
import 'package:auth0_flutter/auth0_flutter.dart';
import 'package:auth0_flutter/auth0_flutter_web.dart';
import 'package:flutter/foundation.dart';
@@ -50,7 +51,9 @@ class _ExampleAppState extends State {
return auth0Web.loginWithRedirect(redirectUrl: 'http://localhost:3000');
}
- final result = await webAuth.login(useHTTPS: true);
+ final result = await webAuth.login(
+ useHTTPS: true,
+ );
setState(() {
_isLoggedIn = true;
diff --git a/auth0_flutter/example/windows/flutter/CMakeLists.txt b/auth0_flutter/example/windows/flutter/CMakeLists.txt
index 930d2071a..903f4899d 100644
--- a/auth0_flutter/example/windows/flutter/CMakeLists.txt
+++ b/auth0_flutter/example/windows/flutter/CMakeLists.txt
@@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
+# Set fallback configurations for older versions of the flutter tool.
+if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
+ set(FLUTTER_TARGET_PLATFORM "windows-x64")
+endif()
+
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
@@ -92,7 +97,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
- windows-x64 $
+ ${FLUTTER_TARGET_PLATFORM} $
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
diff --git a/auth0_flutter/example/windows/flutter/generated_plugin_registrant.cc b/auth0_flutter/example/windows/flutter/generated_plugin_registrant.cc
index 8b6d4680a..9e7029719 100644
--- a/auth0_flutter/example/windows/flutter/generated_plugin_registrant.cc
+++ b/auth0_flutter/example/windows/flutter/generated_plugin_registrant.cc
@@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ Auth0FlutterPluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("Auth0FlutterPluginCApi"));
}
diff --git a/auth0_flutter/example/windows/flutter/generated_plugins.cmake b/auth0_flutter/example/windows/flutter/generated_plugins.cmake
index b93c4c30c..930991068 100644
--- a/auth0_flutter/example/windows/flutter/generated_plugins.cmake
+++ b/auth0_flutter/example/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ auth0_flutter
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
diff --git a/auth0_flutter/example/windows/runner/.vs/CMake Overview b/auth0_flutter/example/windows/runner/.vs/CMake Overview
new file mode 100644
index 000000000..e69de29bb
diff --git a/auth0_flutter/example/windows/runner/.vs/ProjectSettings.json b/auth0_flutter/example/windows/runner/.vs/ProjectSettings.json
new file mode 100644
index 000000000..8f0d73346
--- /dev/null
+++ b/auth0_flutter/example/windows/runner/.vs/ProjectSettings.json
@@ -0,0 +1,3 @@
+{
+ "CurrentProjectSetting": "x64-Debug"
+}
\ No newline at end of file
diff --git a/auth0_flutter/example/windows/runner/.vs/VSWorkspaceState.json b/auth0_flutter/example/windows/runner/.vs/VSWorkspaceState.json
new file mode 100644
index 000000000..287f4fc17
--- /dev/null
+++ b/auth0_flutter/example/windows/runner/.vs/VSWorkspaceState.json
@@ -0,0 +1,12 @@
+{
+ "OutputFoldersPerTargetSystem": {
+ "Local Machine": [
+ "out\\build\\x64-Debug",
+ "out\\install\\x64-Debug"
+ ]
+ },
+ "ExpandedNodes": [
+ ""
+ ],
+ "PreviewInSolutionExplorer": false
+}
\ No newline at end of file
diff --git a/auth0_flutter/example/windows/runner/.vs/cmake.db b/auth0_flutter/example/windows/runner/.vs/cmake.db
new file mode 100644
index 000000000..442fd4033
Binary files /dev/null and b/auth0_flutter/example/windows/runner/.vs/cmake.db differ
diff --git a/auth0_flutter/example/windows/runner/.vs/runner/FileContentIndex/4c9b582b-a146-48e4-aa6e-e983816e5e8e.vsidx b/auth0_flutter/example/windows/runner/.vs/runner/FileContentIndex/4c9b582b-a146-48e4-aa6e-e983816e5e8e.vsidx
new file mode 100644
index 000000000..a304a758c
Binary files /dev/null and b/auth0_flutter/example/windows/runner/.vs/runner/FileContentIndex/4c9b582b-a146-48e4-aa6e-e983816e5e8e.vsidx differ
diff --git a/auth0_flutter/example/windows/runner/.vs/runner/v17/.wsuo b/auth0_flutter/example/windows/runner/.vs/runner/v17/.wsuo
new file mode 100644
index 000000000..811c83a78
Binary files /dev/null and b/auth0_flutter/example/windows/runner/.vs/runner/v17/.wsuo differ
diff --git a/auth0_flutter/example/windows/runner/.vs/runner/v17/Browse.VC.db b/auth0_flutter/example/windows/runner/.vs/runner/v17/Browse.VC.db
new file mode 100644
index 000000000..087c3dd95
Binary files /dev/null and b/auth0_flutter/example/windows/runner/.vs/runner/v17/Browse.VC.db differ
diff --git a/auth0_flutter/example/windows/runner/.vs/runner/v17/DocumentLayout.json b/auth0_flutter/example/windows/runner/.vs/runner/v17/DocumentLayout.json
new file mode 100644
index 000000000..4781bc392
--- /dev/null
+++ b/auth0_flutter/example/windows/runner/.vs/runner/v17/DocumentLayout.json
@@ -0,0 +1,12 @@
+{
+ "Version": 1,
+ "WorkspaceRootPath": "C:\\Users\\Administrator\\Documents\\auth0-flutter\\auth0_flutter\\example\\windows\\runner\\",
+ "Documents": [],
+ "DocumentGroupContainers": [
+ {
+ "Orientation": 0,
+ "VerticalTabListWidth": 256,
+ "DocumentGroups": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/auth0_flutter/example/windows/runner/.vs/slnx.sqlite b/auth0_flutter/example/windows/runner/.vs/slnx.sqlite
new file mode 100644
index 000000000..c06abf4a1
Binary files /dev/null and b/auth0_flutter/example/windows/runner/.vs/slnx.sqlite differ
diff --git a/auth0_flutter/example/windows/runner/flutter_window.cpp b/auth0_flutter/example/windows/runner/flutter_window.cpp
index b25e363ef..955ee3038 100644
--- a/auth0_flutter/example/windows/runner/flutter_window.cpp
+++ b/auth0_flutter/example/windows/runner/flutter_window.cpp
@@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() {
this->Show();
});
+ // Flutter can complete the first frame before the "show window" callback is
+ // registered. The following call ensures a frame is pending to ensure the
+ // window is shown. It is a no-op if the first frame hasn't completed yet.
+ flutter_controller_->ForceRedraw();
+
return true;
}
diff --git a/auth0_flutter/example/windows/runner/main.cpp b/auth0_flutter/example/windows/runner/main.cpp
index a61bf80d3..41fb2fb62 100644
--- a/auth0_flutter/example/windows/runner/main.cpp
+++ b/auth0_flutter/example/windows/runner/main.cpp
@@ -1,27 +1,130 @@
#include
#include
#include
+#include
+#include
#include "flutter_window.h"
#include "utils.h"
-int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
- _In_ wchar_t *command_line, _In_ int show_command) {
- // Attach to console when present (e.g., 'flutter run') or create a
- // new console when running with a debugger.
+const wchar_t* kSingleInstanceMutex = L"auth0flutter_single_instance_mutex";
+const wchar_t* kRedirectPipeName = L"\\\\.\\pipe\\auth0flutter_pipe";
+
+// Forward URI to first instance (pipe client)
+void ForwardToFirstInstance(const wchar_t* uri) {
+ HANDLE hPipe = CreateFileW(
+ kRedirectPipeName, GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
+
+ if (hPipe != INVALID_HANDLE_VALUE) {
+ DWORD written = 0;
+ size_t len = (wcslen(uri) + 1) * sizeof(wchar_t);
+ WriteFile(hPipe, uri, (DWORD)len, &written, NULL);
+ CloseHandle(hPipe);
+ }
+}
+
+// Bring first instance window to foreground
+void BringExistingWindowToFront() {
+ HWND hwnd = FindWindowW(L"FLUTTER_RUNNER_WIN32_WINDOW", NULL);
+ if (hwnd) {
+ ShowWindow(hwnd, SW_RESTORE);
+ SetForegroundWindow(hwnd);
+ }
+}
+
+// Pipe server (runs in first instance)
+void StartPipeServer() {
+ std::thread([] {
+ while (true) {
+ HANDLE hPipe = CreateNamedPipeW(
+ kRedirectPipeName,
+ PIPE_ACCESS_INBOUND,
+ PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
+ 1, 0, 0, 0, NULL);
+
+ if (hPipe == INVALID_HANDLE_VALUE) {
+ return;
+ }
+
+ if (ConnectNamedPipe(hPipe, NULL)) {
+ wchar_t buffer[2048];
+ DWORD read = 0;
+ if (ReadFile(hPipe, buffer, sizeof(buffer), &read, NULL)) {
+ buffer[read / sizeof(wchar_t)] = L'\0';
+
+ // Expose to plugin
+ SetEnvironmentVariableW(L"PLUGIN_STARTUP_URL", buffer);
+
+ // Bring app to front when redirect arrives
+ BringExistingWindowToFront();
+ }
+ }
+ DisconnectNamedPipe(hPipe);
+ CloseHandle(hPipe);
+ }
+ }).detach();
+}
+
+int APIENTRY wWinMain(
+ _In_ HINSTANCE instance,
+ _In_opt_ HINSTANCE prev,
+ _In_ wchar_t* /*command_line*/,
+ _In_ int show_command) {
+
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
CreateAndAttachConsole();
}
- // Initialize COM, so that it is available for use in the library and/or
- // plugins.
::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
+ // -----------------------------
+ // Parse command line properly
+ // -----------------------------
+ int argc = 0;
+ LPWSTR* argv = CommandLineToArgvW(GetCommandLineW(), &argc);
+
+ std::wstring startupUri;
+ if (argv && argc > 1) {
+ // argv[1] is already de-quoted by Windows
+ startupUri = argv[1];
+ }
+
+ if (argv) {
+ LocalFree(argv);
+ }
+
+ // -----------------------------
+ // Ensure single instance
+ // -----------------------------
+ bool hasUri = !startupUri.empty();
+
+HANDLE hMutex = CreateMutexW(NULL, TRUE, kSingleInstanceMutex);
+bool alreadyRunning = (hMutex && GetLastError() == ERROR_ALREADY_EXISTS);
+
+if (alreadyRunning && hasUri) {
+ // This is a protocol activation β forward and exit
+ ForwardToFirstInstance(startupUri.c_str());
+ return 0;
+}
+
+ // -----------------------------
+ // First instance: store startup URI
+ // -----------------------------
+ if (!startupUri.empty()) {
+ SetEnvironmentVariableW(L"PLUGIN_STARTUP_URL", startupUri.c_str());
+ } else {
+ SetEnvironmentVariableW(L"PLUGIN_STARTUP_URL", L"");
+ }
+
+ StartPipeServer();
+
+ // -----------------------------
+ // Flutter bootstrap
+ // -----------------------------
flutter::DartProject project(L"data");
std::vector command_line_arguments =
GetCommandLineArguments();
-
project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
FlutterWindow window(project);
diff --git a/auth0_flutter/lib/src/mobile/web_authentication.dart b/auth0_flutter/lib/src/mobile/web_authentication.dart
index d5f68133a..36588e03f 100644
--- a/auth0_flutter/lib/src/mobile/web_authentication.dart
+++ b/auth0_flutter/lib/src/mobile/web_authentication.dart
@@ -1,6 +1,7 @@
import 'package:auth0_flutter_platform_interface/auth0_flutter_platform_interface.dart';
import '../../auth0_flutter.dart';
+import 'dart:io' show Platform;
/// An interface for authenticating users using the [Auth0 Universal Login page](https://auth0.com/docs/authenticate/login/auth0-universal-login).
///
@@ -12,11 +13,12 @@ import '../../auth0_flutter.dart';
/// It is not intended for you to instantiate this class yourself, as an
/// instance of it is already exposed as [Auth0.webAuthentication].
///
+///
/// Usage example:
///
/// ```dart
/// final auth0 = Auth0('DOMAIN', 'CLIENT_ID');
-/// final result = await auth0.webAuthentication.login();
+/// final result = await auth0.webAuthentication().login();
/// final accessToken = result.accessToken;
/// ```
class WebAuthentication {
@@ -26,7 +28,11 @@ class WebAuthentication {
final CredentialsManager? _credentialsManager;
WebAuthentication(
- this._account, this._userAgent, this._scheme, this._credentialsManager);
+ this._account,
+ this._userAgent,
+ this._scheme,
+ this._credentialsManager,
+ );
/// Redirects the user to the [Auth0 Universal Login page](https://auth0.com/docs/authenticate/login/auth0-universal-login) for authentication. If successful, it returns
/// a set of tokens, as well as the user's profile (constructed from ID token
@@ -85,7 +91,7 @@ class WebAuthentication {
'openid',
'profile',
'email',
- 'offline_access'
+ 'offline_access',
},
final String? redirectUrl,
final String? organizationId,
@@ -98,24 +104,35 @@ class WebAuthentication {
const IdTokenValidationConfig(),
final SafariViewController? safariViewController,
final bool useDPoP = false}) async {
- final credentials = await Auth0FlutterWebAuthPlatform.instance.login(
- _createWebAuthRequest(WebAuthLoginOptions(
- audience: audience,
- scopes: scopes,
- redirectUrl: redirectUrl,
- organizationId: organizationId,
- invitationUrl: invitationUrl,
- parameters: parameters,
- idTokenValidationConfig: idTokenValidationConfig,
- scheme: _scheme,
- useHTTPS: useHTTPS,
- useEphemeralSession: useEphemeralSession,
- safariViewController: safariViewController,
- allowedBrowsers: allowedBrowsers,
- useDPoP: useDPoP)));
+ // Merge custom callback parameters into the parameters map for Windows
+ final mergedParameters = {
+ ...parameters,
+ 'actualCallbackUrl': 'auth0flutter://callback',
+ };
- await _credentialsManager?.storeCredentials(credentials);
+ final credentials = await Auth0FlutterWebAuthPlatform.instance.login(
+ _createWebAuthRequest(
+ WebAuthLoginOptions(
+ audience: audience,
+ scopes: scopes,
+ redirectUrl: redirectUrl,
+ organizationId: organizationId,
+ invitationUrl: invitationUrl,
+ parameters: mergedParameters,
+ idTokenValidationConfig: idTokenValidationConfig,
+ scheme: _scheme,
+ useHTTPS: useHTTPS,
+ useEphemeralSession: useEphemeralSession,
+ safariViewController: safariViewController,
+ allowedBrowsers: allowedBrowsers,
+ useDPoP: useDPoP,
+ ),
+ ),
+ );
+ if (!Platform.isWindows) {
+ await _credentialsManager?.storeCredentials(credentials);
+ }
return credentials;
}
@@ -156,7 +173,10 @@ class WebAuthentication {
federated: federated,
allowedBrowsers: allowedBrowsers),
));
- await _credentialsManager?.clearCredentials();
+
+ if (!Platform.isWindows) {
+ await _credentialsManager?.clearCredentials();
+ }
}
/// Terminates the ongoing web-based operation and reports back that it was
@@ -171,5 +191,8 @@ class WebAuthentication {
_createWebAuthRequest(
final TOptions options) =>
WebAuthRequest(
- account: _account, options: options, userAgent: _userAgent);
+ account: _account,
+ options: options,
+ userAgent: _userAgent,
+ );
}
diff --git a/auth0_flutter/pubspec.yaml b/auth0_flutter/pubspec.yaml
index b16705c92..b2beb7db5 100644
--- a/auth0_flutter/pubspec.yaml
+++ b/auth0_flutter/pubspec.yaml
@@ -54,6 +54,8 @@ flutter:
web:
pluginClass: Auth0FlutterPlugin
fileName: src/web.dart
+ windows:
+ pluginClass: Auth0FlutterPluginCApi
# To add assets to your plugin package, add an assets section, like this:
# assets:
diff --git a/auth0_flutter/windows/.gitignore b/auth0_flutter/windows/.gitignore
new file mode 100644
index 000000000..b3eb2be16
--- /dev/null
+++ b/auth0_flutter/windows/.gitignore
@@ -0,0 +1,17 @@
+flutter/
+
+# Visual Studio user-specific files.
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# Visual Studio build-related files.
+x64/
+x86/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
diff --git a/auth0_flutter/windows/.vs/CMake Overview b/auth0_flutter/windows/.vs/CMake Overview
new file mode 100644
index 000000000..e69de29bb
diff --git a/auth0_flutter/windows/.vs/ProjectSettings.json b/auth0_flutter/windows/.vs/ProjectSettings.json
new file mode 100644
index 000000000..8f0d73346
--- /dev/null
+++ b/auth0_flutter/windows/.vs/ProjectSettings.json
@@ -0,0 +1,3 @@
+{
+ "CurrentProjectSetting": "x64-Debug"
+}
\ No newline at end of file
diff --git a/auth0_flutter/windows/.vs/VSWorkspaceState.json b/auth0_flutter/windows/.vs/VSWorkspaceState.json
new file mode 100644
index 000000000..287f4fc17
--- /dev/null
+++ b/auth0_flutter/windows/.vs/VSWorkspaceState.json
@@ -0,0 +1,12 @@
+{
+ "OutputFoldersPerTargetSystem": {
+ "Local Machine": [
+ "out\\build\\x64-Debug",
+ "out\\install\\x64-Debug"
+ ]
+ },
+ "ExpandedNodes": [
+ ""
+ ],
+ "PreviewInSolutionExplorer": false
+}
\ No newline at end of file
diff --git a/auth0_flutter/windows/.vs/slnx.sqlite b/auth0_flutter/windows/.vs/slnx.sqlite
new file mode 100644
index 000000000..5e3038ecd
Binary files /dev/null and b/auth0_flutter/windows/.vs/slnx.sqlite differ
diff --git a/auth0_flutter/windows/.vs/windows/FileContentIndex/f80ae7cd-876d-45a7-b522-5aa5be927f84.vsidx b/auth0_flutter/windows/.vs/windows/FileContentIndex/f80ae7cd-876d-45a7-b522-5aa5be927f84.vsidx
new file mode 100644
index 000000000..27bc5077f
Binary files /dev/null and b/auth0_flutter/windows/.vs/windows/FileContentIndex/f80ae7cd-876d-45a7-b522-5aa5be927f84.vsidx differ
diff --git a/auth0_flutter/windows/.vs/windows/v17/.wsuo b/auth0_flutter/windows/.vs/windows/v17/.wsuo
new file mode 100644
index 000000000..8288cb9ba
Binary files /dev/null and b/auth0_flutter/windows/.vs/windows/v17/.wsuo differ
diff --git a/auth0_flutter/windows/.vs/windows/v17/Browse.VC.db b/auth0_flutter/windows/.vs/windows/v17/Browse.VC.db
new file mode 100644
index 000000000..6fbf1a7b3
Binary files /dev/null and b/auth0_flutter/windows/.vs/windows/v17/Browse.VC.db differ
diff --git a/auth0_flutter/windows/.vs/windows/v17/DocumentLayout.json b/auth0_flutter/windows/.vs/windows/v17/DocumentLayout.json
new file mode 100644
index 000000000..c70c0e147
--- /dev/null
+++ b/auth0_flutter/windows/.vs/windows/v17/DocumentLayout.json
@@ -0,0 +1,12 @@
+{
+ "Version": 1,
+ "WorkspaceRootPath": "C:\\Users\\Administrator\\Documents\\auth0-flutter\\auth0_flutter\\windows\\",
+ "Documents": [],
+ "DocumentGroupContainers": [
+ {
+ "Orientation": 0,
+ "VerticalTabListWidth": 256,
+ "DocumentGroups": []
+ }
+ ]
+}
\ No newline at end of file
diff --git a/auth0_flutter/windows/Auth0FlutterWebAuthMethodCallHandler.cpp b/auth0_flutter/windows/Auth0FlutterWebAuthMethodCallHandler.cpp
new file mode 100644
index 000000000..fa4a25ac8
--- /dev/null
+++ b/auth0_flutter/windows/Auth0FlutterWebAuthMethodCallHandler.cpp
@@ -0,0 +1,67 @@
+/**
+ * @file Auth0FlutterWebAuthMethodCallHandler.cpp
+ * @brief Implementation of Auth0FlutterWebAuthMethodCallHandler
+ */
+
+#include "Auth0FlutterWebAuthMethodCallHandler.h"
+
+namespace auth0_flutter
+{
+
+ /**
+ * @brief Constructor - initializes the handler with a list of WebAuth handlers
+ *
+ * @param handlers Vector of WebAuthRequestHandler implementations
+ * Each handler is responsible for a specific WebAuth operation
+ */
+ Auth0FlutterWebAuthMethodCallHandler::Auth0FlutterWebAuthMethodCallHandler(
+ std::vector> handlers)
+ : handlers_(std::move(handlers))
+ {
+ }
+
+ /**
+ * @brief Routes method calls to the appropriate handler
+ *
+ * This method implements the routing logic:
+ * 1. Extract the method name from the method call
+ * 2. Iterate through registered handlers to find a match
+ * 3. If a handler matches, delegate to it
+ * 4. If no handler matches, return NotImplemented
+ *
+ * The method also validates that arguments are provided as a map,
+ * which is required by all WebAuth operations.
+ *
+ * @param method_call The method call from Flutter containing method name and arguments
+ * @param result The result callback to return response to Flutter
+ */
+ void Auth0FlutterWebAuthMethodCallHandler::HandleMethodCall(
+ const flutter::MethodCall &method_call,
+ std::unique_ptr> result)
+ {
+ // Get the method name from the call
+ const std::string &method = method_call.method_name();
+
+ // All WebAuth methods require arguments to be a map
+ const auto *args = std::get_if(method_call.arguments());
+ if (!args)
+ {
+ result->Error("bad_args", "Expected a map as arguments");
+ return;
+ }
+
+ // Find and execute the matching handler
+ for (const auto &handler : handlers_)
+ {
+ if (handler->method() == method)
+ {
+ handler->handle(args, std::move(result));
+ return;
+ }
+ }
+
+ // No handler found for this method
+ result->NotImplemented();
+ }
+
+} // namespace auth0_flutter
diff --git a/auth0_flutter/windows/Auth0FlutterWebAuthMethodCallHandler.h b/auth0_flutter/windows/Auth0FlutterWebAuthMethodCallHandler.h
new file mode 100644
index 000000000..67cfe729d
--- /dev/null
+++ b/auth0_flutter/windows/Auth0FlutterWebAuthMethodCallHandler.h
@@ -0,0 +1,85 @@
+/**
+ * @file Auth0FlutterWebAuthMethodCallHandler.h
+ * @brief Method call handler for WebAuth channel
+ *
+ * This class manages WebAuth method calls from Flutter by routing them to
+ * appropriate specialized handlers based on the method name.
+ *
+ * Pattern: Chain of Responsibility / Strategy pattern
+ * - Maintains a list of WebAuthRequestHandler implementations
+ * - Routes incoming method calls to the handler that matches the method name
+ * - Returns NotImplemented if no handler matches
+ */
+
+#ifndef FLUTTER_PLUGIN_AUTH0_FLUTTER_WEB_AUTH_METHOD_CALL_HANDLER_H_
+#define FLUTTER_PLUGIN_AUTH0_FLUTTER_WEB_AUTH_METHOD_CALL_HANDLER_H_
+
+#include
+#include
+#include
+#include
+#include "request_handlers/web_auth/WebAuthRequestHandler.h"
+
+namespace auth0_flutter
+{
+
+ /**
+ * @class Auth0FlutterWebAuthMethodCallHandler
+ * @brief Routes WebAuth method calls to appropriate handlers
+ *
+ * This class implements the Strategy pattern for handling different WebAuth
+ * operations. Each operation (login, logout) is implemented by a separate
+ * handler class, making the code modular and testable.
+ *
+ * Usage:
+ * 1. Create handler instance with list of WebAuthRequestHandlers
+ * 2. Call HandleMethodCall() when a method is invoked from Flutter
+ * 3. The handler will route to the appropriate WebAuthRequestHandler
+ *
+ * Example:
+ * @code
+ * auto handler = std::make_unique(
+ * std::vector>{
+ * std::make_unique(),
+ * std::make_unique()
+ * }
+ * );
+ * handler->HandleMethodCall(method_call, std::move(result));
+ * @endcode
+ */
+ class Auth0FlutterWebAuthMethodCallHandler
+ {
+ public:
+ /**
+ * @brief Constructs the handler with a list of WebAuth request handlers
+ * @param handlers Vector of WebAuthRequestHandler implementations
+ */
+ explicit Auth0FlutterWebAuthMethodCallHandler(
+ std::vector> handlers);
+
+ ~Auth0FlutterWebAuthMethodCallHandler() = default;
+
+ // Disallow copy and assign
+ Auth0FlutterWebAuthMethodCallHandler(const Auth0FlutterWebAuthMethodCallHandler &) = delete;
+ Auth0FlutterWebAuthMethodCallHandler &operator=(const Auth0FlutterWebAuthMethodCallHandler &) = delete;
+
+ /**
+ * @brief Handles a method call from Flutter
+ *
+ * Routes the method call to the appropriate WebAuthRequestHandler based on
+ * the method name. If no handler matches, returns NotImplemented.
+ *
+ * @param method_call The method call from Flutter
+ * @param result The result callback to send response back to Flutter
+ */
+ void HandleMethodCall(
+ const flutter::MethodCall &method_call,
+ std::unique_ptr> result);
+
+ private:
+ std::vector> handlers_;
+ };
+
+} // namespace auth0_flutter
+
+#endif // FLUTTER_PLUGIN_AUTH0_FLUTTER_WEB_AUTH_METHOD_CALL_HANDLER_H_
diff --git a/auth0_flutter/windows/CMakeLists.txt b/auth0_flutter/windows/CMakeLists.txt
new file mode 100644
index 000000000..ad3f7c551
--- /dev/null
+++ b/auth0_flutter/windows/CMakeLists.txt
@@ -0,0 +1,204 @@
+# The Flutter tooling requires that developers have a version of Visual Studio
+# installed that includes CMake 3.14 or later. You should not increase this
+# version, as doing so will cause the plugin to fail to compile for some
+# customers of the plugin.
+cmake_minimum_required(VERSION 3.15)
+cmake_policy(SET CMP0167 NEW)
+project(auth0_flutter LANGUAGES CXX)
+
+#if (DEFINED ENV{VCPKG_ROOT} AND EXISTS "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
+# set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
+# CACHE STRING "Vcpkg toolchain file")
+#endif()
+
+# Project-level configuration.
+set(PROJECT_NAME "auth0_flutter")
+project(${PROJECT_NAME} LANGUAGES CXX)
+
+# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
+# versions of CMake.
+cmake_policy(VERSION 3.14...3.25)
+
+# This value is used when generating builds using this plugin, so it must
+# not be changed
+set(PLUGIN_NAME "auth0_flutter_plugin")
+# Any new source files that you add to the plugin should be added here.
+list(APPEND PLUGIN_SOURCES
+ "auth0_flutter_plugin.cpp"
+ "auth0_flutter_plugin.h"
+ "Auth0FlutterWebAuthMethodCallHandler.cpp"
+ "Auth0FlutterWebAuthMethodCallHandler.h"
+ "request_handlers/web_auth/WebAuthRequestHandler.h"
+ "request_handlers/web_auth/LoginWebAuthRequestHandler.cpp"
+ "request_handlers/web_auth/LoginWebAuthRequestHandler.h"
+ "request_handlers/web_auth/LogoutWebAuthRequestHandler.cpp"
+ "request_handlers/web_auth/LogoutWebAuthRequestHandler.h"
+)
+
+# === vcpkg dependencies ===
+# These are resolved via vcpkg.json automatically (cpprestsdk, boost)
+find_package(cpprestsdk CONFIG REQUIRED)
+find_package(OpenSSL REQUIRED)
+find_package(Boost REQUIRED COMPONENTS system date_time regex)
+
+# Define the plugin library target only if flutter targets are available
+if(TARGET flutter)
+ # Define the plugin library target. Its name must not be changed (see comment
+ # on PLUGIN_NAME above).
+ add_library(${PLUGIN_NAME} SHARED
+ "include/auth0_flutter/auth0_flutter_plugin_c_api.h"
+ "auth0_flutter_plugin_c_api.cpp"
+ "auth0_client.cpp"
+ "token_decoder.cpp"
+ "time_util.cpp"
+ "user_profile.cpp"
+ "user_identity.cpp"
+ "jwt_util.cpp"
+ "oauth_helpers.cpp"
+ "oauth_helpers.h"
+ "url_utils.cpp"
+ "url_utils.h"
+ "windows_utils.cpp"
+ "windows_utils.h"
+ ${PLUGIN_SOURCES}
+ )
+
+ # Apply a standard set of build settings that are configured in the
+ # application-level CMakeLists.txt. This can be removed for plugins that want
+ # full control over build settings.
+ if(COMMAND apply_standard_settings)
+ apply_standard_settings(${PLUGIN_NAME})
+ endif()
+
+ # Symbols are hidden by default to reduce the chance of accidental conflicts
+ # between plugins. This should not be removed; any symbols that should be
+ # exported should be explicitly exported with the FLUTTER_PLUGIN_EXPORT macro.
+ set_target_properties(${PLUGIN_NAME} PROPERTIES
+ CXX_VISIBILITY_PRESET hidden)
+ target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL)
+ target_compile_definitions(${PLUGIN_NAME}
+ PRIVATE
+ _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
+ )
+ # Source include directories and library dependencies.
+ target_include_directories(${PLUGIN_NAME} INTERFACE
+ "${CMAKE_CURRENT_SOURCE_DIR}/include")
+
+ #list(APPEND CMAKE_MODULE_PATH "$ENV{VCPKG_ROOT}/installed/x64-windows/share")
+
+ # Link Flutter + vcpkg dependencies
+ target_link_libraries(${PLUGIN_NAME} PRIVATE
+ flutter
+ flutter_wrapper_plugin
+ cpprestsdk::cpprest
+ OpenSSL::SSL
+ OpenSSL::Crypto
+ Boost::system
+ Boost::date_time
+ Boost::regex
+ )
+
+ # List of absolute paths to libraries that should be bundled with the plugin.
+ set(auth0_flutter_bundled_libraries
+ ""
+ PARENT_SCOPE
+ )
+endif()
+
+# === Tests ===
+option(AUTH0_FLUTTER_ENABLE_TESTS "Build auth0_flutter unit tests" ON)
+
+if (AUTH0_FLUTTER_ENABLE_TESTS)
+ enable_testing()
+
+ set(TEST_RUNNER auth0_flutter_tests)
+
+ include(FetchContent)
+ FetchContent_Declare(
+ googletest
+ URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.zip
+ )
+ set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
+ set(INSTALL_GTEST OFF CACHE BOOL "" FORCE)
+ FetchContent_MakeAvailable(googletest)
+
+ add_executable(${TEST_RUNNER}
+ test/time_util_test.cpp
+ test/url_utils_test.cpp
+ test/windows_utils_test.cpp
+ test/oauth_helpers_test.cpp
+
+ # Reuse plugin sources directly (NO flutter plugin entrypoints)
+ time_util.cpp
+ url_utils.cpp
+ windows_utils.cpp
+ oauth_helpers.cpp
+ )
+
+ target_compile_features(${TEST_RUNNER} PRIVATE cxx_std_17)
+
+ target_include_directories(${TEST_RUNNER} PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}
+ ${CMAKE_CURRENT_SOURCE_DIR}/include
+ )
+
+ target_compile_definitions(${TEST_RUNNER} PRIVATE
+ _SILENCE_STDEXT_ARR_ITERS_DEPRECATION_WARNING
+ )
+
+ target_link_libraries(${TEST_RUNNER} PRIVATE
+ gtest_main
+ gmock
+ cpprestsdk::cpprest
+ OpenSSL::SSL
+ OpenSSL::Crypto
+ Boost::system
+ Boost::date_time
+ Boost::regex
+ )
+
+ # Link flutter_wrapper_plugin if available (when building as part of Flutter app)
+ if(TARGET flutter_wrapper_plugin)
+ target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin)
+ endif()
+
+ # Register tests with CTest
+ include(GoogleTest)
+ gtest_discover_tests(${TEST_RUNNER}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ DISCOVERY_TIMEOUT 60
+ )
+
+ # Fallback: Manual test registration (if gtest_discover_tests fails)
+ # Uncomment the following lines if CTest cannot discover tests automatically:
+ # add_test(NAME auth0_flutter_all_tests COMMAND ${TEST_RUNNER})
+ # set_tests_properties(auth0_flutter_all_tests PROPERTIES
+ # WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ # )
+
+ # Add a custom target to run tests directly
+ add_custom_target(run_tests
+ COMMAND ${TEST_RUNNER}
+ DEPENDS ${TEST_RUNNER}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ COMMENT "Running unit tests..."
+ )
+
+ # Add a custom target to run tests with coverage (requires OpenCppCoverage)
+ add_custom_target(coverage
+ COMMAND OpenCppCoverage.exe
+ --sources ${CMAKE_CURRENT_SOURCE_DIR}
+ --excluded_sources ${CMAKE_CURRENT_SOURCE_DIR}\\test
+ --export_type cobertura:coverage.xml
+ --export_type html:coverage_html
+ -- $
+ DEPENDS ${TEST_RUNNER}
+ WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+ COMMENT "Running unit tests with coverage (requires OpenCppCoverage)..."
+ )
+
+ message(STATUS "Auth0 Flutter tests enabled. Test executable: ${TEST_RUNNER}")
+ message(STATUS "Run tests with: cmake --build build --target run_tests")
+ message(STATUS "Run tests with coverage: cmake --build build --target coverage")
+ message(STATUS "Or run directly: ./build/Debug/${TEST_RUNNER}.exe")
+endif()
\ No newline at end of file
diff --git a/auth0_flutter/windows/auth0_client.cpp b/auth0_flutter/windows/auth0_client.cpp
new file mode 100644
index 000000000..dfcf0c4fe
--- /dev/null
+++ b/auth0_flutter/windows/auth0_client.cpp
@@ -0,0 +1,63 @@
+#include "auth0_client.h"
+
+#include
+#include
+
+#include "token_decoder.h"
+using namespace web;
+using namespace web::http;
+using namespace web::http::client;
+
+static std::string GetJsonString(
+ const web::json::value &json,
+ const utility::string_t &key)
+{
+ if (json.has_field(key) && json.at(key).is_string())
+ {
+ return utility::conversions::to_utf8string(json.at(key).as_string());
+ }
+ return {};
+}
+
+Auth0Client::Auth0Client(std::string domain, std::string clientId)
+ : domain_(std::move(domain)),
+ clientId_(std::move(clientId)) {}
+
+Credentials Auth0Client::ExchangeCodeForTokens(
+ const std::string &redirectUri,
+ const std::string &code,
+ const std::string &codeVerifier)
+{
+
+ http_client client(
+ U("https://" + utility::conversions::to_string_t(domain_)));
+
+ http_request request(methods::POST);
+ request.set_request_uri(U("/oauth/token"));
+ request.headers().set_content_type(U("application/json"));
+
+ web::json::value body;
+ body[U("grant_type")] = web::json::value::string(U("authorization_code"));
+ body[U("client_id")] =
+ web::json::value::string(utility::conversions::to_string_t(clientId_));
+ body[U("code")] =
+ web::json::value::string(utility::conversions::to_string_t(code));
+ body[U("redirect_uri")] =
+ web::json::value::string(utility::conversions::to_string_t(redirectUri));
+ body[U("code_verifier")] =
+ web::json::value::string(utility::conversions::to_string_t(codeVerifier));
+
+ request.set_body(body);
+
+ auto response = client.request(request).get();
+ auto json = response.extract_json().get();
+
+ if (response.status_code() != status_codes::OK)
+ {
+ throw std::runtime_error(
+ "Token request failed: " +
+ GetJsonString(json, U("error_description")));
+ }
+
+ return DecodeTokenResponse(json);
+}
\ No newline at end of file
diff --git a/auth0_flutter/windows/auth0_client.h b/auth0_flutter/windows/auth0_client.h
new file mode 100644
index 000000000..213110fc5
--- /dev/null
+++ b/auth0_flutter/windows/auth0_client.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include
+#include "credentials.h"
+
+class Auth0Client
+{
+public:
+ Auth0Client(std::string domain, std::string clientId);
+
+ Credentials ExchangeCodeForTokens(
+ const std::string &redirectUri,
+ const std::string &code,
+ const std::string &codeVerifier);
+
+private:
+ std::string domain_;
+ std::string clientId_;
+};
diff --git a/auth0_flutter/windows/auth0_flutter_plugin.cpp b/auth0_flutter/windows/auth0_flutter_plugin.cpp
new file mode 100644
index 000000000..7c3b64748
--- /dev/null
+++ b/auth0_flutter/windows/auth0_flutter_plugin.cpp
@@ -0,0 +1,116 @@
+/**
+ * @file auth0_flutter_plugin.cpp
+ * @brief Main plugin implementation for Auth0 Flutter Windows
+ *
+ * This file contains the main plugin class for Auth0 Flutter Windows.
+ * The plugin follows a handler pattern similar to the Android and iOS implementations,
+ * delegating method calls to specialized handlers.
+ *
+ * Architecture:
+ * - Auth0FlutterPlugin: Main plugin class, registers with Flutter engine
+ * - Auth0FlutterWebAuthMethodCallHandler: Routes method calls to appropriate handlers
+ * - LoginWebAuthRequestHandler: Handles webAuth#login
+ * - LogoutWebAuthRequestHandler: Handles webAuth#logout
+ *
+ * Helper utilities are now in separate files:
+ * - oauth_helpers.h: PKCE functions, OAuth callback handling
+ * - url_utils.h: URL encoding/decoding
+ * - windows_utils.h: Windows-specific utilities
+ */
+
+#include "auth0_flutter_plugin.h"
+
+#include
+#include
+#include
+
+#include
+
+// Utility headers
+#include "oauth_helpers.h"
+#include "url_utils.h"
+#include "windows_utils.h"
+
+// WebAuth handlers
+#include "Auth0FlutterWebAuthMethodCallHandler.h"
+#include "request_handlers/web_auth/LoginWebAuthRequestHandler.h"
+#include "request_handlers/web_auth/LogoutWebAuthRequestHandler.h"
+
+namespace auth0_flutter
+{
+
+ /**
+ * @brief Registers the plugin with the Flutter engine
+ *
+ * Sets up the WebAuth method channel and initializes the plugin with
+ * all required handlers. This follows the same channel name and architecture
+ * as Android and iOS implementations.
+ *
+ * Channel: "auth0.com/auth0_flutter/web_auth"
+ * Methods supported:
+ * - webAuth#login: Handled by LoginWebAuthRequestHandler
+ * - webAuth#logout: Handled by LogoutWebAuthRequestHandler
+ *
+ * @param registrar The Flutter plugin registrar
+ */
+ void Auth0FlutterPlugin::RegisterWithRegistrar(
+ flutter::PluginRegistrarWindows *registrar)
+ {
+ auto channel =
+ std::make_unique>(
+ registrar->messenger(), "auth0.com/auth0_flutter/web_auth",
+ &flutter::StandardMethodCodec::GetInstance());
+
+ auto plugin = std::make_unique();
+
+ channel->SetMethodCallHandler(
+ [plugin_pointer = plugin.get()](const auto &call, auto result)
+ {
+ plugin_pointer->HandleMethodCall(call, std::move(result));
+ });
+
+ registrar->AddPlugin(std::move(plugin));
+ }
+
+ /**
+ * @brief Constructor - initializes the plugin with WebAuth handlers
+ *
+ * Creates and registers all WebAuth request handlers following the
+ * strategy pattern used in Android and iOS implementations.
+ */
+ Auth0FlutterPlugin::Auth0FlutterPlugin()
+ {
+ // Initialize WebAuth method call handler with all request handlers
+ std::vector> handlers;
+ handlers.push_back(std::make_unique());
+ handlers.push_back(std::make_unique());
+
+ webAuthCallHandler_ = std::make_unique(
+ std::move(handlers));
+ }
+
+ Auth0FlutterPlugin::~Auth0FlutterPlugin() {}
+
+ /**
+ * @brief Handles method calls from Flutter
+ *
+ * Delegates all method calls to the appropriate handler. This implementation
+ * follows the same pattern as Android and iOS, using a handler-based architecture
+ * for clean separation of concerns.
+ *
+ * All WebAuth methods (login, logout) are handled by webAuthCallHandler_,
+ * which routes to specialized handlers based on the method name.
+ *
+ * @param method_call The method call from Flutter
+ * @param result Callback to return results to Flutter
+ */
+ void Auth0FlutterPlugin::HandleMethodCall(
+ const flutter::MethodCall &method_call,
+ std::unique_ptr> result)
+ {
+ // Delegate all method calls to the WebAuth handler
+ // The handler will route to appropriate specialized handlers based on method name
+ webAuthCallHandler_->HandleMethodCall(method_call, std::move(result));
+ }
+
+} // namespace auth0_flutter
diff --git a/auth0_flutter/windows/auth0_flutter_plugin.h b/auth0_flutter/windows/auth0_flutter_plugin.h
new file mode 100644
index 000000000..2819cbfcc
--- /dev/null
+++ b/auth0_flutter/windows/auth0_flutter_plugin.h
@@ -0,0 +1,84 @@
+/**
+ * @file auth0_flutter_plugin.h
+ * @brief Main plugin header for Auth0 Flutter Windows
+ *
+ * Defines the Auth0FlutterPlugin class which serves as the entry point
+ * for the Flutter plugin on Windows. This plugin handles WebAuth operations
+ * by delegating to specialized handler classes.
+ */
+
+#ifndef FLUTTER_PLUGIN_AUTH0_FLUTTER_PLUGIN_H_
+#define FLUTTER_PLUGIN_AUTH0_FLUTTER_PLUGIN_H_
+
+#include
+#include
+
+#include
+
+namespace auth0_flutter
+{
+ // Forward declaration
+ class Auth0FlutterWebAuthMethodCallHandler;
+
+ /**
+ * @class Auth0FlutterPlugin
+ * @brief Main plugin class for Auth0 Flutter on Windows
+ *
+ * This class follows the same architectural pattern as Android and iOS:
+ * - Registers with Flutter engine
+ * - Creates method channel for WebAuth operations
+ * - Delegates method calls to specialized handlers
+ *
+ * The plugin uses a handler-based architecture where each WebAuth operation
+ * (login, logout) is implemented by a separate handler class, making the
+ * code modular, testable, and consistent with other platforms.
+ */
+ class Auth0FlutterPlugin : public flutter::Plugin
+ {
+ public:
+ /**
+ * @brief Registers the plugin with the Flutter Windows engine
+ * @param registrar The plugin registrar provided by Flutter
+ */
+ static void RegisterWithRegistrar(flutter::PluginRegistrarWindows *registrar);
+
+ /**
+ * @brief Constructor - initializes WebAuth handlers
+ */
+ Auth0FlutterPlugin();
+
+ /**
+ * @brief Destructor
+ */
+ virtual ~Auth0FlutterPlugin();
+
+ // Disallow copy and assign.
+ Auth0FlutterPlugin(const Auth0FlutterPlugin &) = delete;
+ Auth0FlutterPlugin &operator=(const Auth0FlutterPlugin &) = delete;
+
+ /**
+ * @brief Handles method calls from Flutter
+ *
+ * Routes method calls to the appropriate handler. All WebAuth methods
+ * are handled by webAuthCallHandler_.
+ *
+ * @param method_call The method call from Flutter
+ * @param result Callback to return results to Flutter
+ */
+ void HandleMethodCall(
+ const flutter::MethodCall &method_call,
+ std::unique_ptr> result);
+
+ private:
+ /**
+ * @brief Handler for all WebAuth method calls
+ *
+ * This handler manages login, logout, and other WebAuth operations
+ * by routing to appropriate specialized handlers.
+ */
+ std::unique_ptr webAuthCallHandler_;
+ };
+
+} // namespace auth0_flutter
+
+#endif // FLUTTER_PLUGIN_AUTH0_FLUTTER_PLUGIN_H_
diff --git a/auth0_flutter/windows/auth0_flutter_plugin_c_api.cpp b/auth0_flutter/windows/auth0_flutter_plugin_c_api.cpp
new file mode 100644
index 000000000..c095fa23d
--- /dev/null
+++ b/auth0_flutter/windows/auth0_flutter_plugin_c_api.cpp
@@ -0,0 +1,12 @@
+#include "include/auth0_flutter/auth0_flutter_plugin_c_api.h"
+
+#include
+
+#include "auth0_flutter_plugin.h"
+
+void Auth0FlutterPluginCApiRegisterWithRegistrar(
+ FlutterDesktopPluginRegistrarRef registrar) {
+ auth0_flutter::Auth0FlutterPlugin::RegisterWithRegistrar(
+ flutter::PluginRegistrarManager::GetInstance()
+ ->GetRegistrar(registrar));
+}
diff --git a/auth0_flutter/windows/auth0_models.h b/auth0_flutter/windows/auth0_models.h
new file mode 100644
index 000000000..e69de29bb
diff --git a/auth0_flutter/windows/credentials.h b/auth0_flutter/windows/credentials.h
new file mode 100644
index 000000000..26ed58f6c
--- /dev/null
+++ b/auth0_flutter/windows/credentials.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+
+#include
+
+#include "user_profile.h"
+
+class Credentials {
+ public:
+ // ===== Raw credential fields =====
+ std::string accessToken;
+ std::string idToken;
+ std::string tokenType;
+
+ std::optional refreshToken;
+ std::optional expiresIn; // seconds
+ std::optional expiresAt;
+
+ std::vector scope;
+};
diff --git a/auth0_flutter/windows/include/auth0_flutter/auth0_flutter_plugin_c_api.h b/auth0_flutter/windows/include/auth0_flutter/auth0_flutter_plugin_c_api.h
new file mode 100644
index 000000000..cef4a62cc
--- /dev/null
+++ b/auth0_flutter/windows/include/auth0_flutter/auth0_flutter_plugin_c_api.h
@@ -0,0 +1,23 @@
+#ifndef FLUTTER_PLUGIN_AUTH0_FLUTTER_PLUGIN_C_API_H_
+#define FLUTTER_PLUGIN_AUTH0_FLUTTER_PLUGIN_C_API_H_
+
+#include
+
+#ifdef FLUTTER_PLUGIN_IMPL
+#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport)
+#else
+#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport)
+#endif
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+FLUTTER_PLUGIN_EXPORT void Auth0FlutterPluginCApiRegisterWithRegistrar(
+ FlutterDesktopPluginRegistrarRef registrar);
+
+#if defined(__cplusplus)
+} // extern "C"
+#endif
+
+#endif // FLUTTER_PLUGIN_AUTH0_FLUTTER_PLUGIN_C_API_H_
diff --git a/auth0_flutter/windows/jwt_util.cpp b/auth0_flutter/windows/jwt_util.cpp
new file mode 100644
index 000000000..cb7989d35
--- /dev/null
+++ b/auth0_flutter/windows/jwt_util.cpp
@@ -0,0 +1,122 @@
+#include "jwt_util.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#pragma comment(lib, "Crypt32.lib")
+
+static std::string Base64UrlDecode(const std::string &input)
+{
+ std::string padded = input;
+ std::replace(padded.begin(), padded.end(), '-', '+');
+ std::replace(padded.begin(), padded.end(), '_', '/');
+ while (padded.size() % 4 != 0)
+ padded.push_back('=');
+
+ // First call: determine required buffer size
+ DWORD out_len = 0;
+ if (!CryptStringToBinaryA(
+ padded.c_str(),
+ static_cast(padded.size()),
+ CRYPT_STRING_BASE64,
+ nullptr,
+ &out_len,
+ nullptr,
+ nullptr))
+ {
+ throw std::runtime_error("Base64 decode failed: unable to determine output size");
+ }
+
+ // Second call: perform actual decoding
+ std::string output(out_len, '\0');
+ if (!CryptStringToBinaryA(
+ padded.c_str(),
+ static_cast(padded.size()),
+ CRYPT_STRING_BASE64,
+ reinterpret_cast(&output[0]),
+ &out_len,
+ nullptr,
+ nullptr))
+ {
+ throw std::runtime_error("Base64 decode failed: decoding error");
+ }
+
+ return output;
+}
+
+JwtParts SplitJwt(const std::string &token)
+{
+ std::stringstream ss(token);
+ std::string part;
+ std::vector parts;
+
+ while (std::getline(ss, part, '.'))
+ {
+ parts.push_back(part);
+ }
+
+ if (parts.size() == 2 && !token.empty() && token.back() == '.')
+ {
+ parts.push_back("");
+ }
+
+ if (parts.size() != 3)
+ {
+ throw std::runtime_error("JWT must have exactly 3 parts");
+ }
+
+ return {parts[0], parts[1], parts[2]};
+}
+
+web::json::value DecodeJwtPayload(const std::string &token)
+{
+ auto parts = SplitJwt(token);
+ auto decoded = Base64UrlDecode(parts.payload);
+ return web::json::value::parse(decoded);
+}
+
+flutter::EncodableValue JsonToEncodable(const web::json::value &v)
+{
+ if (v.is_null())
+ return flutter::EncodableValue();
+
+ if (v.is_boolean())
+ return flutter::EncodableValue(v.as_bool());
+ if (v.is_number())
+ return flutter::EncodableValue(v.as_double());
+ if (v.is_string())
+ return flutter::EncodableValue(utility::conversions::to_utf8string(v.as_string()));
+
+ if (v.is_array())
+ {
+ flutter::EncodableList list;
+ for (const auto &item : v.as_array())
+ {
+ list.push_back(JsonToEncodable(item));
+ }
+ return flutter::EncodableValue(list);
+ }
+
+ if (v.is_object())
+ {
+ flutter::EncodableMap map;
+ for (const auto &kv : v.as_object())
+ {
+ map[flutter::EncodableValue(utility::conversions::to_utf8string(kv.first))] =
+ JsonToEncodable(kv.second);
+ }
+ return flutter::EncodableValue(map);
+ }
+
+ return flutter::EncodableValue();
+}
+
+flutter::EncodableMap ParseJsonToEncodableMap(const std::string &json)
+{
+ auto parsed = web::json::value::parse(json);
+ auto ev = JsonToEncodable(parsed);
+ return std::get(ev);
+}
\ No newline at end of file
diff --git a/auth0_flutter/windows/jwt_util.h b/auth0_flutter/windows/jwt_util.h
new file mode 100644
index 000000000..981cd338b
--- /dev/null
+++ b/auth0_flutter/windows/jwt_util.h
@@ -0,0 +1,19 @@
+#pragma once
+
+#include
+#include
+#include
+
+struct JwtParts
+{
+ std::string header;
+ std::string payload;
+ std::string signature;
+};
+
+JwtParts SplitJwt(const std::string &token);
+web::json::value DecodeJwtPayload(const std::string &token);
+
+// SAFE conversion
+flutter::EncodableMap ParseJsonToEncodableMap(const std::string &json);
+flutter::EncodableValue JsonToEncodable(const web::json::value &v);
\ No newline at end of file
diff --git a/auth0_flutter/windows/oauth_helpers.cpp b/auth0_flutter/windows/oauth_helpers.cpp
new file mode 100644
index 000000000..773194f19
--- /dev/null
+++ b/auth0_flutter/windows/oauth_helpers.cpp
@@ -0,0 +1,212 @@
+/**
+ * @file oauth_helpers.cpp
+ * @brief Implementation of OAuth 2.0 and PKCE helper functions
+ */
+
+#include "oauth_helpers.h"
+#include "url_utils.h"
+#include "windows_utils.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+// OpenSSL for PKCE
+#include
+#include
+
+// cpprestsdk for HTTP listener and client
+#include
+#include
+#include
+
+using namespace web;
+using namespace web::http;
+using namespace web::http::client;
+using namespace web::http::experimental::listener;
+
+namespace auth0_flutter
+{
+
+ std::string base64UrlEncode(const std::vector &data)
+ {
+ static const char *b64chars =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
+ std::string result;
+ size_t i = 0;
+ unsigned char a3[3];
+ unsigned char a4[4];
+
+ for (size_t pos = 0; pos < data.size();)
+ {
+ int len = 0;
+ for (i = 0; i < 3; i++)
+ {
+ if (pos < data.size())
+ {
+ a3[i] = data[pos++];
+ len++;
+ }
+ else
+ {
+ a3[i] = 0;
+ }
+ }
+
+ a4[0] = (a3[0] & 0xfc) >> 2;
+ a4[1] = ((a3[0] & 0x03) << 4) + ((a3[1] & 0xf0) >> 4);
+ a4[2] = ((a3[1] & 0x0f) << 2) + ((a3[2] & 0xc0) >> 6);
+ a4[3] = a3[2] & 0x3f;
+
+ for (i = 0; i < 4; i++)
+ {
+ if (i <= (size_t)(len + 0))
+ {
+ result += b64chars[a4[i]];
+ }
+ else
+ {
+ result += '=';
+ }
+ }
+ }
+
+ // Make it URL-safe
+ for (auto &c : result)
+ {
+ if (c == '+')
+ c = '-';
+ if (c == '/')
+ c = '_';
+ }
+
+ // Strip padding '='
+ while (!result.empty() && result.back() == '=')
+ {
+ result.pop_back();
+ }
+
+ return result;
+ }
+
+ std::string generateCodeVerifier()
+ {
+ std::vector buffer(32);
+ if (RAND_bytes(buffer.data(), static_cast(buffer.size())) != 1)
+ {
+ throw std::runtime_error("Failed to generate random bytes");
+ }
+ return base64UrlEncode(buffer);
+ }
+
+ std::string generateCodeChallenge(const std::string &verifier)
+ {
+ unsigned char hash[SHA256_DIGEST_LENGTH];
+ SHA256(reinterpret_cast(verifier.data()),
+ verifier.size(),
+ hash);
+
+ std::vector digest(hash, hash + SHA256_DIGEST_LENGTH);
+ return base64UrlEncode(digest);
+ }
+
+ std::string waitForAuthCode_CustomScheme(
+ const std::string &expectedRedirectBase,
+ int timeoutSeconds,
+ const std::string &expectedState)
+ {
+ DebugPrint("Waiting for custom scheme callback: " + expectedRedirectBase);
+ const int sleepMs = 200;
+ int elapsed = 0;
+ auto readAndClearEnv = []() -> std::string
+ {
+ // Ask Windows how many wchar_t characters are needed (including null)
+ DWORD bufSize = GetEnvironmentVariableW(L"PLUGIN_STARTUP_URL", NULL, 0);
+ if (bufSize == 0)
+ return std::string();
+
+ std::vector buf(bufSize);
+ DWORD ret = GetEnvironmentVariableW(L"PLUGIN_STARTUP_URL", buf.data(), bufSize);
+ if (ret == 0 || ret >= bufSize)
+ {
+ return std::string();
+ }
+
+ // Clear it so it's not consumed twice
+ SetEnvironmentVariableW(L"PLUGIN_STARTUP_URL", L"");
+
+ // Convert wide -> UTF-8 safely
+ std::wstring wstr(buf.data(), ret);
+ return WideToUtf8(wstr);
+ };
+
+ while (elapsed < timeoutSeconds * 1000)
+ {
+ std::string uri = readAndClearEnv();
+ if (!uri.empty())
+ {
+ DebugPrint("Received callback URI: " + uri);
+
+ // Optionally: verify prefix matches expectedRedirectBase
+ if (!expectedRedirectBase.empty())
+ {
+ if (uri.rfind(expectedRedirectBase, 0) != 0)
+ {
+ DebugPrint("WARNING: URI does not match expected base. Expected: " +
+ expectedRedirectBase + ", Received: " + uri);
+ // continue β but still try to parse if present
+ }
+ }
+ // find query
+ auto qpos = uri.find('?');
+ if (qpos == std::string::npos)
+ {
+ DebugPrint("ERROR: No query parameters in callback URI");
+ return std::string(); // no query params
+ }
+ std::string query = uri.substr(qpos + 1);
+ auto params = SafeParseQuery(query);
+
+ // Validate state parameter if expected state is provided (CSRF protection)
+ if (!expectedState.empty())
+ {
+ auto stateIt = params.find("state");
+ if (stateIt == params.end() || stateIt->second != expectedState)
+ {
+ DebugPrint("State validation failed: expected '" + expectedState +
+ "', received '" + (stateIt != params.end() ? stateIt->second : "(missing)") + "'");
+ return std::string(); // State mismatch - potential CSRF attack
+ }
+ DebugPrint("State validation passed");
+ }
+
+ auto it = params.find("code");
+ if (it != params.end())
+ {
+ DebugPrint("Authorization code extracted successfully");
+ return it->second;
+ }
+ else
+ {
+ // maybe error param present
+ if (params.find("error") != params.end())
+ {
+ DebugPrint("ERROR: OAuth error in callback");
+ return std::string();
+ }
+ DebugPrint("ERROR: No code parameter in callback");
+ }
+ }
+ std::this_thread::sleep_for(std::chrono::milliseconds(sleepMs));
+ elapsed += sleepMs;
+ }
+
+ // timeout
+ DebugPrint("Timeout waiting for callback");
+ return std::string();
+ }
+
+} // namespace auth0_flutter
\ No newline at end of file
diff --git a/auth0_flutter/windows/oauth_helpers.h b/auth0_flutter/windows/oauth_helpers.h
new file mode 100644
index 000000000..4124f1f6a
--- /dev/null
+++ b/auth0_flutter/windows/oauth_helpers.h
@@ -0,0 +1,54 @@
+/**
+ * @file oauth_helpers.h
+ * @brief OAuth 2.0 and PKCE helper functions
+ */
+
+#pragma once
+
+#include
+#include
+
+namespace auth0_flutter
+{
+
+ /**
+ * @brief Base64 URL-safe encode without padding
+ *
+ * Encodes binary data to base64 URL-safe format as required by OAuth 2.0 PKCE.
+ */
+ std::string base64UrlEncode(const std::vector &data);
+
+ /**
+ * @brief Generate PKCE code verifier
+ *
+ * Creates a cryptographically random 32-byte value and encodes it as a
+ * base64 URL-safe string.
+ */
+ std::string generateCodeVerifier();
+
+ /**
+ * @brief Generate code challenge from verifier for PKCE flow
+ *
+ * Creates the code challenge by hashing the verifier with SHA256 and
+ * encoding the result as base64 URL-safe.
+ */
+ std::string generateCodeChallenge(const std::string &verifier);
+
+ /**
+ * @brief Wait for OAuth callback with authorization code (custom scheme flow)
+ *
+ * Polls the PLUGIN_STARTUP_URL environment variable for the OAuth redirect URI.
+ * This is used when an intermediary server receives the Auth0 callback and forwards
+ * it to the app via a custom protocol scheme.
+ *
+ * @param expectedRedirectBase Expected redirect URI base for validation
+ * @param timeoutSeconds Maximum time to wait for callback (default: 180 seconds)
+ * @param expectedState Expected state parameter for CSRF validation (optional)
+ * @return Authorization code if successful, empty string on timeout or error
+ */
+ std::string waitForAuthCode_CustomScheme(
+ const std::string &expectedRedirectBase,
+ int timeoutSeconds = 180,
+ const std::string &expectedState = "");
+
+} // namespace auth0_flutter
diff --git a/auth0_flutter/windows/request_handlers/web_auth/LoginWebAuthRequestHandler.cpp b/auth0_flutter/windows/request_handlers/web_auth/LoginWebAuthRequestHandler.cpp
new file mode 100644
index 000000000..b8196d6ee
--- /dev/null
+++ b/auth0_flutter/windows/request_handlers/web_auth/LoginWebAuthRequestHandler.cpp
@@ -0,0 +1,377 @@
+/**
+ * @file LoginWebAuthRequestHandler.cpp
+ * @brief Implementation of LoginWebAuthRequestHandler
+ */
+
+#include "LoginWebAuthRequestHandler.h"
+#include "../../auth0_client.h"
+#include "../../credentials.h"
+#include "../../user_profile.h"
+#include "../../jwt_util.h"
+#include "../../time_util.h"
+#include "../../oauth_helpers.h"
+#include "../../windows_utils.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include