mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 12:27:43 +00:00
feat: oAuth proxy plugin (#438)
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
passkey,
|
||||
twoFactor,
|
||||
oneTap,
|
||||
oAuthProxy,
|
||||
} from "better-auth/plugins";
|
||||
import { reactInvitationEmail } from "./email/invitation";
|
||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||
@@ -122,5 +123,6 @@ export const auth = betterAuth({
|
||||
admin(),
|
||||
multiSession(),
|
||||
oneTap(),
|
||||
oAuthProxy(),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -840,6 +840,24 @@ export const contents: Content[] = [
|
||||
),
|
||||
href: "/docs/plugins/multi-session",
|
||||
},
|
||||
{
|
||||
title: "OAuth Proxy",
|
||||
href: "/docs/plugins/oauth-proxy",
|
||||
icon: () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16 2a8 8 0 1 0 8 8a8.01 8.01 0 0 0-8-8m5.91 7h-2.438a15.3 15.3 0 0 0-.791-4.36A6.01 6.01 0 0 1 21.91 9m-5.888 6.999h-.008c-.38-.12-1.309-1.821-1.479-4.999h2.93c-.17 3.176-1.094 4.877-1.443 4.999M14.535 9c.17-3.176 1.094-4.877 1.443-4.999h.008c.38.12 1.309 1.821 1.479 4.999zM13.32 4.64A15.3 15.3 0 0 0 12.528 9H10.09a6.01 6.01 0 0 1 3.23-4.36M10.09 11h2.437a15.3 15.3 0 0 0 .792 4.36A6.01 6.01 0 0 1 10.09 11m8.59 4.36a15.3 15.3 0 0 0 .792-4.36h2.438a6.01 6.01 0 0 1-3.23 4.36M28 30H4a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h24a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2M4 22v6h24v-6z"
|
||||
></path>
|
||||
<circle cx="7" cy="25" r="1" fill="currentColor"></circle>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "JWT",
|
||||
icon: () => (
|
||||
|
||||
66
docs/content/docs/plugins/oauth-proxy.mdx
Normal file
66
docs/content/docs/plugins/oauth-proxy.mdx
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
title: OAuth Proxy
|
||||
description: OAuth Proxy plugin for Better Auth
|
||||
---
|
||||
|
||||
A proxy plugin, that allows you to proxy OAuth requests.Useful for development and preview deployments where the redirect URL can't be known in advance to add to the OAuth provider.
|
||||
|
||||
## Installation
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Add the plugin to your **auth** config
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
import { oAuthProxy } from "better-auth/plugins"
|
||||
|
||||
export const auth = betterAuth({
|
||||
plugins: [ // [!code highlight]
|
||||
oAuthProxy(), // [!code highlight]
|
||||
] // [!code highlight]
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Add redirect URL to your OAuth provider
|
||||
|
||||
For the proxy server to work properly, you’ll need to pass the redirect URL of your main production app registered with the OAuth provider in your social provider config. This needs to be done for each social provider you want to proxy requests for.
|
||||
|
||||
```ts
|
||||
export const auth = betterAuth({
|
||||
plugins: [
|
||||
oAuthProxy(),
|
||||
],
|
||||
socialProviders: {
|
||||
github: {
|
||||
clientId: "your-client-id",
|
||||
clientSecret: "your-client-secret",
|
||||
redirectURL: "https://my-main-app.com/api/auth/github/callback". // [!code highlight]
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
|
||||
## How it works
|
||||
|
||||
The plugin adds an endpoint to your server that proxies OAuth requests. When you initiate a social sign-in, it sets the redirect URL to this proxy endpoint. After the OAuth provider redirects back to your server, the plugin then forwards the user to the original callback URL.
|
||||
|
||||
```ts
|
||||
await authClient.signIn.social({
|
||||
provider: "github",
|
||||
redirectURL: "/dashboard" // the plugin will override this to something like "http://localhost:3000/api/auth/oauth-proxy?callbackURL=/dashboard"
|
||||
})
|
||||
```
|
||||
|
||||
When the OAuth provider returns the user to your server, the plugin automatically redirects them to the intended callback URL.
|
||||
|
||||
<Callout>
|
||||
To share cookies between the proxy server and your main server it uses url query parameters to pass the cookies encrypted in the URL. This is secure as the cookies are encrypted and can only be decrypted by the server.
|
||||
</Callout>
|
||||
|
||||
## Options
|
||||
|
||||
**currentURL**: The application's current URL is automatically determined by the plugin. It first it check for the request URL if invoked by a client, then it checks the base URL from popular hosting providers,and finally falls back to the `baseURL` in your auth config. If the URL isn’t inferred correctly, you can specify it manually here.
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "better-auth",
|
||||
"version": "0.7.3",
|
||||
"version": "0.7.4-beta.1",
|
||||
"description": "The most comprehensive authentication library for TypeScript.",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
|
||||
@@ -15,3 +15,4 @@ export * from "./jwt";
|
||||
export * from "./multi-session";
|
||||
export * from "./email-otp";
|
||||
export * from "./one-tap";
|
||||
export * from "./oauth-proxy";
|
||||
|
||||
133
packages/better-auth/src/plugins/oauth-proxy/index.ts
Normal file
133
packages/better-auth/src/plugins/oauth-proxy/index.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { z } from "zod";
|
||||
import { createAuthEndpoint, createAuthMiddleware } from "../../api";
|
||||
import { symmetricDecrypt, symmetricEncrypt } from "../../crypto";
|
||||
import type { BetterAuthPlugin } from "../../types";
|
||||
import { env } from "../../utils/env";
|
||||
|
||||
function getVenderBaseURL() {
|
||||
const vercel = env.VERCEL_URL;
|
||||
const netlify = env.NETLIFY_URL;
|
||||
const render = env.RENDER_URL;
|
||||
const aws = env.AWS_LAMBDA_FUNCTION_NAME;
|
||||
const google = env.GOOGLE_CLOUD_FUNCTION_NAME;
|
||||
const azure = env.AZURE_FUNCTION_NAME;
|
||||
|
||||
return vercel || netlify || render || aws || google || azure;
|
||||
}
|
||||
|
||||
interface OAuthProxyOptions {
|
||||
/**
|
||||
* The current URL of the application.
|
||||
* The plugin will attempt to infer the current URL from your environment
|
||||
* by checking the base URL from popular hosting providers,
|
||||
* from the request URL if invoked by a client,
|
||||
* or as a fallback, from the `baseURL` in your auth config.
|
||||
* If the URL is not inferred correctly, you can provide a value here."
|
||||
*/
|
||||
currentURL?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A proxy plugin, that allows you to proxy OAuth requests.
|
||||
* Useful for development and preview deployments where
|
||||
* the redirect URL can't be known in advance to add to the OAuth provider.
|
||||
*/
|
||||
export const oAuthProxy = (opts?: OAuthProxyOptions) => {
|
||||
return {
|
||||
id: "oauth-proxy",
|
||||
endpoints: {
|
||||
oAuthProxy: createAuthEndpoint(
|
||||
"/oauth-proxy-callback",
|
||||
{
|
||||
method: "GET",
|
||||
query: z.object({
|
||||
callbackURL: z.string(),
|
||||
cookies: z.string(),
|
||||
}),
|
||||
},
|
||||
async (ctx) => {
|
||||
const cookies = ctx.query.cookies;
|
||||
const decryptedCookies = await symmetricDecrypt({
|
||||
key: ctx.context.secret,
|
||||
data: cookies,
|
||||
});
|
||||
ctx.setHeader("set-cookie", decryptedCookies);
|
||||
/**
|
||||
* Here the callback url will be already validated in against trusted origins
|
||||
* so we don't need to do that here
|
||||
*/
|
||||
throw ctx.redirect(ctx.query.callbackURL);
|
||||
},
|
||||
),
|
||||
},
|
||||
hooks: {
|
||||
after: [
|
||||
{
|
||||
matcher(context) {
|
||||
return context.path?.startsWith("/callback");
|
||||
},
|
||||
handler: createAuthMiddleware(async (ctx) => {
|
||||
const response = ctx.context.returned;
|
||||
if (!response) {
|
||||
return;
|
||||
}
|
||||
const location = response.headers.get("location");
|
||||
if (location?.includes("/oauth-proxy-callback?callbackURL")) {
|
||||
if (!location.startsWith("http")) {
|
||||
return;
|
||||
}
|
||||
const origin = new URL(location).origin;
|
||||
|
||||
/**
|
||||
* We don't want to redirect to the proxy URL if the origin is the same
|
||||
* as the current URL
|
||||
*/
|
||||
if (origin === ctx.context.baseURL) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setCookies = response.headers.get("set-cookie");
|
||||
if (!setCookies) {
|
||||
return;
|
||||
}
|
||||
const encryptedCookies = await symmetricEncrypt({
|
||||
key: ctx.context.secret,
|
||||
data: setCookies,
|
||||
});
|
||||
const locationWithCookies = `${location}&cookies=${encodeURIComponent(
|
||||
encryptedCookies,
|
||||
)}`;
|
||||
response.headers.set("location", locationWithCookies);
|
||||
return {
|
||||
response,
|
||||
};
|
||||
}
|
||||
}),
|
||||
},
|
||||
],
|
||||
before: [
|
||||
{
|
||||
matcher(context) {
|
||||
return context.path?.startsWith("/sign-in/social");
|
||||
},
|
||||
async handler(ctx) {
|
||||
const url = new URL(
|
||||
opts?.currentURL ||
|
||||
ctx.request?.url ||
|
||||
getVenderBaseURL() ||
|
||||
ctx.context.baseURL,
|
||||
);
|
||||
ctx.body.callbackURL = `${url.origin}${
|
||||
ctx.context.options.basePath || "/api/auth"
|
||||
}/oauth-proxy-callback?callbackURL=${encodeURIComponent(
|
||||
ctx.body.callbackURL || ctx.context.baseURL,
|
||||
)}`;
|
||||
return {
|
||||
context: ctx,
|
||||
};
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies BetterAuthPlugin;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@better-auth/cli",
|
||||
"version": "0.7.3",
|
||||
"version": "0.7.4-beta.1",
|
||||
"description": "The CLI for Better Auth",
|
||||
"module": "dist/index.mjs",
|
||||
"repository": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@better-auth/expo",
|
||||
"version": "0.7.3",
|
||||
"version": "0.7.4-beta.1",
|
||||
"description": "",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.mjs",
|
||||
|
||||
Reference in New Issue
Block a user