OAuth Plugin
Learn to integrate OAuth 2.0 authentication in Tauri v2 desktop apps using the tauri-plugin-oauth plugin. Covers installation, the complete authorization code flow with PKCE, and security best practices.
Introduction
The OAuth plugin for Tauri v2 solves a fundamental problem for desktop applications — many OAuth providers, including Google and GitHub, do not allow custom URI schemes (like myapp://callback) as redirect URLs. Desktop apps cannot host a public HTTPS endpoint to receive the redirect, so a different approach is needed. The OAuth Plugin introduction expands on why a local server is the usual desktop answer.
This plugin spawns a temporary local web server on localhost that listens for the OAuth callback. The user authenticates in their browser, the provider redirects to http://localhost:<port>/callback, the plugin captures the authorization code, and your app can exchange it for tokens. The entire flow stays within the user’s machine; no third-party redirect service is required.
The plugin is provider-agnostic. It works with any OAuth 2.0 or OpenID Connect provider that supports the authorization code grant with a localhost redirect URI. It does not handle token exchange or storage — you remain in control of that logic.
Desktop-only:
This plugin is designed for desktop platforms (Windows, macOS, Linux). For mobile OAuth that uses platform-native in-app browsers, consider tauri-plugin-auth-session instead.
Authentication Flow
OAuth 2.0 is a framework that lets a user grant a third-party application limited access to their resources without sharing their password. The most secure and widely recommended grant type for native and single-page applications is the Authorization Code Grant with PKCE (Proof Key for Code Exchange). Authentication Flow walks through the same sequence with React code.
From the user’s perspective the process feels simple — they click “Sign in with …,” a browser opens, they approve the request, and the app is authenticated. Under the hood, four parties are involved: your app, the user’s browser, the OAuth provider, and the local server created by this plugin.
The plugin’s role in that flow can be visualized in plain text:
App → starts local server → opens browser with auth URL
User logs in and approves
OAuth provider redirects to http://localhost:<port>/callback?code=...&state=...
Local server receives the URL → plugin forwards it to your app
App exchanges the code for tokens (using its own backend logic)
This local server removes the need for a custom URI scheme on desktop and works even with providers that reject anything other than http://localhost as a redirect URI.
Implementing the Plugin
The steps below walk through a complete, production-ready integration of the OAuth plugin with a React + Vite frontend. Each step depends on the previous one, so follow them in order.
Step 1: Install the plugin
Add the Rust crate to src-tauri/Cargo.toml:
[dependencies]
tauri-plugin-oauth = "2"
reqwest = { version = "0.12", features = ["json"] }
serde = { version = "1", features = ["derive"] }
Note:
The reqwest and serde crates are added here because you will later exchange the authorization code for tokens from the Rust side, which is the secure approach.
Install the JavaScript API package in your project root:
npm install @fabianlars/tauri-plugin-oauth@2
Step 2: Register the plugin and configure permissions
Register the plugin in your Tauri app’s src-tauri/src/lib.rs:
use tauri::{command, Emitter, Window};
use tauri_plugin_oauth::start;
#[command]
async fn start_oauth_server(window: Window) -> Result<u16, String> {
start(move |url| {
let _ = window.emit("oauth-redirect", url);
})
.map_err(|e| e.to_string())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_oauth::init())
.invoke_handler(tauri::generate_handler![start_oauth_server])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
The command start_oauth_server starts the local server and emits the full redirect URL to the frontend whenever a callback arrives.
Add the required permission to src-tauri/capabilities/default.json:
{
"identifier": "default",
"description": "Default capability for the main window",
"windows": ["main"],
"permissions": [
"oauth:default"
]
}
Step 3: Start the OAuth server from the frontend
In your React app, import the plugin’s functions and start the server. The following component manages the entire flow:
import { useState } from "react";
import {
start,
cancel,
onUrl,
} from "@fabianlars/tauri-plugin-oauth";
import { invoke } from "@tauri-apps/api/core";
function generateStateAndPKCE() {
const array = new Uint32Array(32);
crypto.getRandomValues(array);
const state = Array.from(array, (b) => b.toString(16).padStart(8, "0")).join("");
const codeVerifier = Array.from(crypto.getRandomValues(new Uint8Array(32)))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
// SHA-256 hash of codeVerifier for the challenge
const encoder = new TextEncoder();
const data = encoder.encode(codeVerifier);
const hashPromise = crypto.subtle.digest("SHA-256", data);
return { state, codeVerifier, hashPromise };
}
function base64UrlEncode(buffer: ArrayBuffer) {
return btoa(String.fromCharCode(...new Uint8Array(buffer)))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
export default function App() {
const [token, setToken] = useState<string | null>(null);
const handleLogin = async () => {
const { state, codeVerifier, hashPromise } = generateStateAndPKCE();
const codeChallenge = base64UrlEncode(await hashPromise);
const port = await start(); // plugin starts local server
const authUrl = new URL("https://your-provider.com/authorize");
authUrl.searchParams.set("client_id", "YOUR_CLIENT_ID");
authUrl.searchParams.set("redirect_uri", `http://localhost:${port}/callback`);
authUrl.searchParams.set("response_type", "code");
authUrl.searchParams.set("scope", "openid profile email");
authUrl.searchParams.set("state", state);
authUrl.searchParams.set("code_challenge", codeChallenge);
authUrl.searchParams.set("code_challenge_method", "S256");
// Listen for the redirect URL from the plugin
const stopListener = await onUrl(async (callbackUrl) => {
stopListener();
// Exchange code for tokens via a Rust command (secure)
const url = new URL(callbackUrl);
const returnedState = url.searchParams.get("state");
const code = url.searchParams.get("code");
if (returnedState !== state) {
console.error("CSRF state mismatch!");
return;
}
if (!code) {
console.error("No authorization code received.");
return;
}
try {
const accessToken: string = await invoke("exchange_code", {
code,
codeVerifier,
});
setToken(accessToken);
} catch (err) {
console.error("Token exchange failed:", err);
}
// Optionally stop the server (port is captured from start())
await cancel(port);
});
// Open the provider's authorization page
window.open(authUrl.toString(), "_blank");
};
const handleLogout = () => {
setToken(null);
};
return (
<div>
{token ? (
<div>
<p>Authenticated!</p>
<button onClick={handleLogout}>Log out</button>
</div>
) : (
<button onClick={handleLogin}>Sign in with Provider</button>
)}
</div>
);
}
State validation is mandatory:
Always validate the state parameter on the callback. Skipping this makes your app vulnerable to CSRF attacks. The code above demonstrates a correct check.
Step 4: Exchange the authorization code for tokens on the Rust side
To keep secrets like the client secret safe, perform the token exchange in Rust. Add a new command to src-tauri/src/lib.rs:
use serde::Deserialize;
#[derive(Deserialize)]
struct TokenResponse {
access_token: String,
// Optionally include refresh_token, id_token, etc.
}
#[command]
async fn exchange_code(
code: String,
code_verifier: String,
) -> Result<String, String> {
let client = reqwest::Client::new();
let params = [
("grant_type", "authorization_code"),
("code", &code),
("redirect_uri", "http://localhost"), // must match the redirect URI registered with the provider
("client_id", "YOUR_CLIENT_ID"),
("client_secret", "YOUR_CLIENT_SECRET"), // only stored in the binary, not in frontend
("code_verifier", &code_verifier),
];
let resp = client
.post("https://your-provider.com/token")
.form(¶ms)
.send()
.await
.map_err(|e| e.to_string())?
.json::<TokenResponse>()
.await
.map_err(|e| e.to_string())?;
Ok(resp.access_token)
}
Register the new command in the run function:
.invoke_handler(tauri::generate_handler![start_oauth_server, exchange_code])
Never expose client secrets to the frontend:
The client_secret must only appear in the Rust code (or an environment variable read at compile time). Putting it in JavaScript would make it visible to anyone who inspects your app’s source.
What a successful flow looks like:
If everything is configured correctly, after clicking the sign-in button, a browser opens, the user approves, the browser redirects to localhost, the plugin emits the URL, the frontend exchanges the code, and the app stores the token. You should see “Authenticated!” on the screen.
Best Practices
Secure the entire chain
The OAuth plugin only handles the redirect capture. Every other part of the flow is your responsibility, and small oversights can create security holes. The OAuth best practices page focuses on token storage, refresh, and logout.
- Always use PKCE. Even though the authorization code is exchanged on the same machine, PKCE prevents the code from being intercepted by a malicious local process. The code example above includes a full PKCE implementation.
- Validate the
stateparameter. This prevents CSRF attacks where an attacker tricks a user into completing an authorization with the attacker’s credentials. - Do not expose the client secret in frontend code. If your provider requires a client secret for the token endpoint, perform the exchange on the Rust side, never in JavaScript.
- Compare redirect URIs exactly. When registering the redirect URI with your provider, use
http://localhostwith the exact port the plugin returns. Do not use wildcards or fuzzy matching. - Use short-lived server instances. Stop the local server (
cancel) as soon as the callback is received to minimize the window an attacker could exploit an open port. - Store tokens securely. The plugin does not store tokens. Use the Store Plugin or platform-specific keychain access to persist refresh tokens and access tokens.
A subtle error: mismatched redirect URI:
If the redirect_uri in the authorization request does not exactly match what is registered with the provider, the flow will fail with a redirect mismatch error. Make sure the dynamic port is included correctly.
Handle errors gracefully
The plugin may fail to start a server (all ports busy) or the browser may close before completion. Your frontend code should handle these scenarios:
- If
start()throws, display a friendly message and let the user retry. - If
onUrlnever fires (user closes the browser), implement a timeout or provide a manual cancel button that callscancel(port). - If the token exchange fails, inform the user and clean up any pending listeners.
Platform considerations
The plugin works uniformly across macOS, Windows, and Linux. No platform-specific configuration is required. The local server binds to 127.0.0.1 on a dynamic port. Some firewalls or antivirus software may block the connection; if users report issues, ensure the port is accessible locally.
Summary
The tauri-plugin-oauth plugin removes the biggest obstacle to adding OAuth sign-in to a Tauri v2 desktop app — the lack of a public redirect endpoint. By spawning a temporary local server, it gives you the same authorization code flow that web apps use, with full control over token exchange and storage.
To secure the token lifecycle, the Tauri Store plugin can help persist tokens.
For mobile-specific OAuth with native in-app browser sessions, explore the tauri-plugin-auth-session plugin, which uses ASWebAuthenticationSession on Apple platforms and Chrome Custom Tabs on Android.
Introduction to the OAuth Plugin
Understand the Tauri OAuth plugin, how it solves authentication challenges in desktop apps, and the basic authentication flow it enables
Authentication Flow
Understand the complete OAuth authentication flow in Tauri v2 apps using the plugin, from initiating login to securely handling tokens
Best Practices for OAuth Authentication
Learn how to securely store tokens implement refresh and logout and follow OAuth security guidelines in Tauri v2 applications