Files
better-auth/docs/content/docs/integrations/electron.mdx
T
2026-02-04 20:37:05 +00:00

684 lines
17 KiB
Plaintext

---
title: Electron Integration
description: Integrate Better Auth with Electron.
---
Electron is a popular framework for building cross-platform desktop applications using web technologies.
Better Auth can be integrated into Electron apps to provide secure authentication flows, leveraging the system browser.
## Installation
<Steps>
<Step>
### Configure a Better Auth front- & back-end
Before integrating with Electron, ensure you have a Better Auth server and client set up.
To get started, check out our [installation](/docs/installation) guide for setting up Better Auth.
</Step>
<Step>
### Install the required packages
Install the Better Auth server package and the Electron integration package in your server, Electron app, and web client projects.
**In each project, run:**
```package-install
better-auth @better-auth/electron
```
</Step>
<Step>
### Add the Electron plugin to your Better Auth server
Add the Electron plugin to your Better Auth server.
```ts title="web/lib/auth.ts"
import { betterAuth } from "better-auth";
import { electron } from "@better-auth/electron";
export const auth = betterAuth({
plugins: [electron()],
emailAndPassword: {
enabled: true,
},
social: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
});
```
</Step>
<Step>
### Add the proxy plugin to your web client
On your frontend, add the proxy plugin to handle redirects back into the Electron app.
```ts title="web/lib/auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { electronProxyClient } from "@better-auth/electron/proxy";
export const authClient = createAuthClient({
baseURL: "http://localhost:8081",
plugins: [
electronProxyClient({
protocol: {
scheme: "com.example.app"
},
}),
],
});
```
</Step>
<Step>
### Initialize the Electron client
```ts title="electron/lib/auth-client.ts"
import { createAuthClient } from "better-auth/client";
import { electronClient } from "@better-auth/electron/client";
export const authClient = createAuthClient({
baseURL: "http://localhost:8081", // Base URL of your Better Auth frontend
plugins: [
electronClient({
signInURL: "https://app.example.com/sign-in", // The URL to redirect to for authentication
protocol: {
scheme: "com.example.app" // The custom protocol scheme registered by your Electron app
},
storage: {
getItem: async (key) => {
// retrieve entry from storage
},
setItem: async (key, value) => {
// set entry in storage
},
},
}),
],
});
```
If you'd rather not implement your own storage solution, we offer a default option:
1. Make sure to install the `conf` package:
```package-install
conf
```
2. Then, you can use it as follows:
```ts title="electron/lib/auth-client.ts"
import { storage } from "@better-auth/electron/storage";
electronClient({
storage: storage(),
});
```
<Callout type="warning">
You should never expose the authClient directly
to the renderer process. Instead, [create an IPC bridge](#creating-ipc-bridges) to securely communicate between the main and renderer processes.
</Callout>
</Step>
<Step>
### Scheme and Trusted Origins
The Electron plugin uses deep links to redirect users back to your app after authentication.
To enable this, you need to add your app's protocol scheme to the `trustedOrigins` on your **Better Auth server**.
First, make sure you have a custom protocol scheme registered in your build configuration.
<Tabs items={["electron-forge", "electron-builder"]}>
<Tab value="electron-forge">
```ts title="forge.config.js"
module.exports = {
packagerConfig: {
protocols: [{
name: "MyApp Protocol",
schemes: ["com.example.app"],
}],
},
// ...other config options
};
```
</Tab>
<Tab value="electron-builder">
```ts title="electron-builder.json5"
{
"protocols": [
{
"name": "MyApp Protocol",
"schemes": ["com.example.app"]
}
],
// ...other config options
}
```
</Tab>
</Tabs>
Then, update your Better Auth config to include the scheme in `trustedOrigins`:
```ts title="web/lib/auth.ts"
export const auth = betterAuth({
trustedOrigins: ["com.example.app:/"],
});
```
</Step>
<Step>
### Configure the BrowserWindow
Ensure that your `BrowserWindow` is configured with `nodeIntegration` set to `false` and `contextIsolation` set to `true`, to ensure NodeJS APIs aren't exposed to any JavaScript process running in the browser.
```ts title="electron/main.ts"
import { BrowserWindow } from "electron";
import { join } from "node:path";
const win = new BrowserWindow({
webPreferences: {
preload: join(__dirname, "preload.mjs"),
nodeIntegration: false,
contextIsolation: true,
},
});
```
</Step>
<Step>
### Setup the Main Process
In your Electron main process, use the `setupMain()` method from the Electron auth client to handle necessary configurations.
This will:
- Register the protocol handler for deep links.
- Set up content security policies.
- Set up IPC bridges for communication between main and renderer processes.
```ts title="electron/main.ts"
import { authClient } from "./lib/auth-client";
authClient.setupMain();
```
Note that this must be called before the app is ready.
</Step>
<Step>
### Setup the Renderer Process
In your preload script, use the `setupRenderer()` method from the Electron auth client to expose safe IPC bridges to the renderer process.
```ts title="electron/preload.ts"
import { authClient } from "./lib/auth-client";
authClient.setupRenderer();
```
Note that this must also be called before the app is ready.
<Callout type="info">
To infer the types of the exposed bridges, you can use `authClient.$Infer.Bridges` to extend the `Window` interface.
```ts title="electron/preload.d.ts"
import type { authClient } from "./lib/auth-client";
declare global {
type Bridges = typeof authClient.$Infer.Bridges;
interface Window extends Bridges {}
}
```
</Callout>
</Step>
</Steps>
## Usage
### Handling Authorization in the Browser
In order to redirect users back to your Electron application, you need to call the `ensureElectronRedirect()` method on your web sign-in callback page. Also make sure to preserve any PKCE and state parameters during the sign-in initiation, by passing them via `fetchOptions.query`.
The following example uses React, but the same logic applies to any framework:
```tsx title="web/pages/sign-in.tsx"
import { useEffect, use } from "react";
import { authClient } from "../auth-client";
function SignIn({
searchParams,
}: {
searchParams: Promise<{
client_id?: string | undefined;
state?: string | undefined;
code_challenge?: string | undefined;
code_challenge_method?: string | undefined;
}>;
}) {
const query = use(searchParams);
useEffect(() => {
const id = authClient.ensureElectronRedirect();
return () => {
clearTimeout(id);
}
}, []);
return (
<button
onClick={() =>
authClient.signIn.social({
provider: "google",
fetchOptions: { query }, // preserve PKCE/state
});
}
>
Sign in with Google
</button>
);
}
```
### Handling Authentication in Electron
In your Electron renderer process, you can use the IPC bridges exposed by the preload script.
```tsx title="electron/App.tsx"
function Auth() {
useEffect(() => {
const unsubscribeAuthenticated =
window.onAuthenticated((user) => {
console.log("Authenticated user:", user);
});
const unsubscribeAuthError =
window.onAuthError((ctx) => {
toast.error(`Authentication error: ${ctx.message}`);
});
return () => {
unsubscribeAuthenticated();
unsubscribeAuthError();
};
}, []);
return (
<>
<button onClick={() => window.requestAuth()}>
Sign in with Browser
</button>
<button
onClick={() =>
window.requestAuth({
provider: "google", // sign in with social provider, redirecting directly to the provider
});
}
>
Sign in with Google
</button>
</>
);
}
```
In the main process, you can call from the auth client directly:
```ts title="electron/main.ts"
import { authClient } from "./lib/auth-client";
authClient.requestAuth();
```
### Sign Out
To sign out the user inside the renderer process, you can use the `signOut` bridge in the renderer process:
```ts title="electron/App.tsx"
<button onClick={() => window.signOut()}>Sign out</button>
```
### Subscribing to User Updates
You can listen for user changes via the `onUserUpdated` bridge in the renderer process:
```ts title="electron/App.tsx"
useEffect(() => {
const unsubscribe = window.onUserUpdated((user) => {
console.log("User updated:", user);
});
return () => unsubscribe();
}, []);
```
### Handling Errors
Listen for authentication errors via the `onAuthError` bridge in the renderer process. The bridge will receive error context forwarded from fetch hooks.
```ts title="electron/error-listener.tsx"
useEffect(() => {
const unsubscribe = window.onAuthError((ctx) => {
console.error("Authentication error:", ctx);
});
return () => unsubscribe();
}, []);
```
### Creating IPC bridges
You should create IPC bridges to extend the functionality exposed to your renderer process. This ensures a minimal, safe API surface.
First, create an IPC handler in the main process that uses the `authClient` to perform the desired action:
```ts title="electron/main.ts"
import { authClient } from "./lib/auth-client";
import { ipcMain } from "electron";
ipcMain.handle("myBridge", async (_event, data) => {
const cookie = authClient.getCookie();
return await authClient.someEndpoint({
data,
fetchOptions: {
headers: { cookie },
},
});
});
```
Next, expose the bridge in your preload script using `contextBridge`:
```ts title="electron/preload.ts"
import { contextBridge, ipcRenderer } from "electron";
contextBridge.exposeInMainWorld("myBridge", (data: Record<string, any>) => {
return ipcRenderer.invoke("myBridge", data);
});
```
<Callout type="info">
To infer the types of your bridge, extend the `Window` interface:
```ts title="electron/preload.d.ts"
import type { authClient } from "./lib/auth-client";
declare global {
type Bridges = typeof authClient.$Infer.Bridges;
interface Window extends Bridges {
myBridge: (data: Record<string, any>) => Promise<any>;
}
}
```
</Callout>
Now you can call your custom bridge from anywhere in the renderer process:
```ts title="electron/App.tsx"
useEffect(() => {
window
.myBridge({
foo: "bar",
})
.then((res) => {
console.log("Bridge response:", res);
});
}, []);
```
For more details, check out Electron's [Inter-Process Communication](https://www.electronjs.org/docs/latest/tutorial/ipc) tutorial.
## Options
### Server plugin
#### `codeExpiresIn?`
The duration, in seconds, for which the authorization code is valid. (defaults to `300`)
Note that the authorization code will be refreshed during active endpoint usage.
```ts title="web/auth.ts"
electron({
codeExpiresIn: 300, // 5 minutes
});
```
#### `redirectCookieExpiresIn?`
The duration, in seconds, for which the redirect cookie remains valid. (defaults to `120`)
```ts title="web/auth.ts"
electron({
redirectCookieExpiresIn: 120, // 2 minutes
});
```
The redirect cookie name is derived by the `clientID`.
#### `cookiePrefix?`
The prefix to use for cookies set by the plugin. (defaults to `better-auth`)
```ts title="web/auth.ts"
electron({
cookiePrefix: "better-auth",
});
```
#### `clientID?`
The client id to use for identifying the Electron client during authorization. (defaults to `electron`)
Make sure this matches the clientID provided to both the proxy and electron client plugin.
```ts title="web/auth.ts"
electron({
clientID: "electron",
});
```
#### `disableOriginOverride?`
Override the origin for Electron API routes. (defaults to `false`)
Enable this if you're facing cors origin issues with Electron API routes.
```ts title="web/auth.ts"
electron({
disableOriginOverride: true,
});
```
### Proxy client
#### `protocol`
The protocol scheme to use for deep linking in Electron.
Should follow the [reverse domain name notation](https://datatracker.ietf.org/doc/html/rfc8252#section-7.1) to ensure uniqueness.
Make sure this matches the protocol scheme provided to the Electron client and `trustedOrigins`.
```ts title="web/auth-client.ts"
electronProxyClient({
protocol: "com.example.app",
});
```
```ts title="web/auth-client.ts"
electronProxyClient({
protocol: {
scheme: "com.example.app",
},
});
```
#### `callbackPath?`
The callback path to use for authentication redirects. (defaults to `/auth/callback`)
Make sure this matches the path provided to the electron client plugin.
```ts title="web/auth-client.ts"
electronProxyClient({
callbackPath: "/auth/callback",
});
```
#### `clientID?`
The client id to use for identifying the Electron client during authorization. (defaults to `electron`)
Make sure this matches the clientID provided to both the server and electron client plugin.
```ts title="web/auth-client.ts"
electronProxyClient({
clientID: "electron",
});
```
#### `cookiePrefix?`
The prefix to use for cookies set by the plugin. (defaults to `better-auth`)
```ts title="web/auth-client.ts"
electronProxyClient({
cookiePrefix: "better-auth",
});
```
### Client plugin
#### `signInURL`
The URL to redirect to for authentication.
```ts title="electron/auth-client.ts"
electronClient({
signInURL: "http://localhost:3000/sign-in",
});
```
#### `protocol`
The protocol scheme to use for deep linking in Electron.
Should follow the [reverse domain name notation](https://datatracker.ietf.org/doc/html/rfc8252#section-7.1) to ensure uniqueness.
Make sure this matches the protocol scheme provided to the proxy client and `trustedOrigins`.
```ts title="web/auth-client.ts"
electronProxyClient({
protocol: "com.example.app",
});
```
```ts title="web/auth-client.ts"
electronProxyClient({
protocol: {
scheme: "com.example.app",
},
});
```
#### `callbackPath?`
The callback path to use for authentication redirects. (defaults to `/auth/callback`)
Make sure this matches the path provided to the proxy client plugin.
```ts title="electron/auth-client.ts"
electronClient({
callbackPath: "/auth/callback",
});
```
#### `storage`
Storage solution to use to store session and cookie data.
By default a storage file is generated in the `userData` directory. The name is derived by the project name.
```ts title="electron/auth-client.ts"
electronClient({
storage: {
getItem: (key) => {
// get entry from storage
},
setItem: (key, value) => {
// set entry in storage
},
},
});
```
#### `storagePrefix?`
Prefix for local storage keys. (defaults to `better-auth`)
```ts title="electron/auth-client.ts"
electronClient({
storagePrefix: "better-auth",
});
```
#### `cookiePrefix?`
Prefix(es) for server cookie names to filter. (defaults to `better-auth`)
This is used to identify which cookies belong to better-auth to
prevent infinite refetching when third-party cookies are set.
```ts title="electron/auth-client.ts"
electronClient({
cookiePrefix: "better-auth",
});
```
```ts title="electron/auth-client.ts"
electronClient({
cookiePrefix: ["better-auth", "my-app"],
});
```
#### `channelPrefix?`
Channel prefix for IPC bridges. (defaults to `better-auth`)
```ts title="electron/auth-client.ts"
electronClient({
channelPrefix: "myapp",
});
```
#### `clientID?`
The client id to use for identifying the Electron client during authorization. (defaults to `electron`)
Make sure this matches the clientID provided to both the server and proxy client plugin.
```ts title="electron/auth-client.ts"
electronClient({
clientID: "electron",
});
```
#### `disableCache?`
Whether to disable caching the session data locally. (defaults to `false`)
```ts title="electron/auth-client.ts"
electronClient({
disableCache: true,
});
```