mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-26 21:45:40 -05:00
684 lines
17 KiB
Plaintext
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,
|
|
});
|
|
```
|