tauri-plugin-oauth
A Tauri plugin that spawns a temporary localhost server to capture browser-based OAuth redirects in desktop apps.
Repository Health
Technical Analysis
tauri-plugin-oauth solves a specific problem for Tauri desktop applications: many OAuth providers (Google, GitHub, X, and others) refuse to accept custom URI schemes as redirect URLs, which is the usual way a native app receives an OAuth callback. Instead of relying on deep links, this plugin binds a short-lived TCP listener on 127.0.0.1, hands the OAuth provider a normal http://127.0.0.1:<port> redirect target, and forwards whatever URL the provider hits back into the Rust side of the app as an event.
The crate ships both a plain Rust library (start/start_with_config/cancel) usable outside of Tauri’s command system, and a Tauri plugin wrapper that exposes start/cancel as invokable commands with a matching TypeScript client (@fabianlars/tauri-plugin-oauth) for the frontend. Configuration covers fixed vs. OS-assigned ports, a custom HTML response served to the browser after the redirect, and an optional redirect_uri that issues a 302 instead of an inline page — useful when the app wants the browser to land on one of its own hosted routes once the callback fires.
Because the listener binds an unauthenticated localhost port, the library is intentionally minimal and pushes URL verification back onto the consuming application rather than trying to guess what’s safe.
What You Get
- A
start/start_with_configRust API that binds a localhost TCP listener and returns the port it is listening on - A Tauri plugin (
init()) exposingstartandcancelas JS-invokable commands with an event bridge (oauth://url,oauth://invalid-url) - A TypeScript client package (
@fabianlars/tauri-plugin-oauth) withstart,cancel,onUrl, andonInvalidUrlhelpers - Configurable behavior: fixed candidate ports, a custom HTML response page, or a
redirect_urithat 302s the browser to an app-hosted page instead - An inline script fallback that resolves the browser’s full URL (including hash fragments) back to the local server via a
Full-Urlheader fetch
Common Use Cases
- Adding ‘Login with GitHub/Google/X’ to a Tauri desktop app whose OAuth provider rejects custom URI schemes
- Capturing an OAuth authorization code or token server-side in Rust without exposing it to the app’s webview directly
- Redirecting the user’s browser back to a real hosted page after the OAuth callback via the
redirect_urioption - Building a Rust-only (non-Tauri) CLI or desktop tool that still needs a local OAuth redirect catcher, using the plugin’s underlying library functions directly
Under The Hood
Architecture
Single-file plugin crate (src/lib.rs, ~280 lines) built around a blocking TCP listener spawned on its own thread; start_with_config binds a TcpListener, spawns a thread that loops over listener.incoming(), and hands each connection to handle_connection, which does hand-rolled HTTP parsing via httparse to extract the path and a custom Full-Url header, replying with either a raw 200 HTML page (with an injected script that fetches the browser’s full URL back to the server for fragment-token recovery) or a 302 to a configured redirect_uri. The plugin_impl inner module is a thin Tauri-command wrapper around the same start_with_config/cancel functions, forwarding results as oauth://url / oauth://invalid-url window events. Shutdown is cooperative: a caller can call cancel(port) (opens a TCP connection and writes a 4-byte EXIT sentinel) or hit /exit over HTTP, either of which breaks the accept loop. There’s no framework abstraction beyond this — a single thread, single accept loop, one connection handled at a time — which keeps the surface small but means a flow that never completes leaves the thread parked.
Tech Stack
Cargo.toml pulls tauri = "2" (plugin host), httparse = "1" for zero-copy HTTP request parsing, url = "2" for validating the returned redirect, log = "0.4" for tracing, serde = "1" for deserializing OauthConfig from the Tauri plugin config, and thiserror = "2" for error typing though lib.rs mostly returns std::io::Error directly today. build.rs uses tauri-plugin (build feature) to declare the exposed commands (start, cancel) and generate the ACL/permission scaffolding under permissions/. The JS side is a small Rollup-built TypeScript package (@fabianlars/tauri-plugin-oauth) wrapping @tauri-apps/api’s invoke/listen, versioned alongside the crate. An example Tauri app under examples/vanilla exercises the plugin end-to-end with a plain HTML/TS frontend and its own src-tauri.
Code Quality
No test files exist anywhere in the repository — no #[cfg(test)] modules, no tests/ directory, and the npm pretest script just runs the build rather than a test runner — so the HTTP parsing and redirect logic have no automated coverage. Error handling is present but coarse: handle_connection logs and returns None on write/flush failures rather than propagating a typed error, and the fixed 4048-byte read buffer is compared via an unchecked buffer[..4] == EXIT slice that would panic on a short read. CI (lint.yml, format.yml) runs cargo clippy and formatting checks for both Rust and JS, but there is no cargo test or build-verification job. The public API surface (start, start_with_config, cancel, OauthConfig) is small, named clearly, and documented with doc comments, but the absence of tests is a real gap for a security-adjacent local server.
API Design
The API is intentionally minimal — one function to start (with or without config), one to cancel, and a config struct of three optional fields. Rust callers get a plain closure-based callback; JS callers get an async start()/cancel() pair plus onUrl/onInvalidUrl event subscriptions mirroring Tauri’s usual invoke/listen idiom. The library deliberately punts on convenience work: it does not verify the OAuth response’s trustworthiness or parse query params, and both code comments and the README repeatedly flag that callers must validate the URL themselves — a scope boundary rather than an oversight. That keeps the dependency footprint and attack surface small at the cost of leaving state/PKCE validation entirely to the consumer.