diff --git a/docs/components/sidebar-content.tsx b/docs/components/sidebar-content.tsx index 04f29cc6..e59d6a4b 100644 --- a/docs/components/sidebar-content.tsx +++ b/docs/components/sidebar-content.tsx @@ -601,6 +601,23 @@ export const contents: Content[] = [ ), }, + { + title: "Reddit", + href: "/docs/authentication/reddit", + icon: () => ( + + + + ), + }, ], }, { diff --git a/docs/content/docs/authentication/reddit.mdx b/docs/content/docs/authentication/reddit.mdx new file mode 100644 index 00000000..f4afdc60 --- /dev/null +++ b/docs/content/docs/authentication/reddit.mdx @@ -0,0 +1,81 @@ +--- +title: Reddit +description: Reddit provider setup and usage. +--- + + + + ### Get your Reddit Credentials + To use Reddit sign in, you need a client ID and client secret. You can get them from the [Reddit Developer Portal](https://www.reddit.com/prefs/apps). + + 1. Click "Create App" or "Create Another App" + 2. Select "web app" as the application type + 3. Set the redirect URL to `http://localhost:3000/api/auth/callback/reddit` for local development + 4. For production, set it to your application's domain (e.g. `https://example.com/api/auth/callback/reddit`) + 5. After creating the app, you'll get the client ID (under the app name) and client secret + + If you change the base path of the auth routes, make sure to update the redirect URL accordingly. + + + + ### Configure the provider + To configure the provider, you need to import the provider and pass it to the `socialProviders` option of the auth instance. + + ```ts title="auth.ts" + import { betterAuth } from "better-auth" + + export const auth = betterAuth({ + socialProviders: { + reddit: { + clientId: process.env.REDDIT_CLIENT_ID as string, + clientSecret: process.env.REDDIT_CLIENT_SECRET as string, + }, + }, + }) + ``` + + + + ### Sign In with Reddit + To sign in with Reddit, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties: + - `provider`: The provider to use. It should be set to `reddit`. + + ```ts title="auth-client.ts" + import { createAuthClient } from "better-auth/client" + const authClient = createAuthClient() + + const signIn = async () => { + const data = await authClient.signIn.social({ + provider: "reddit" + }) + } + ``` + + + +## Additional Configuration + +### Scopes +By default, Reddit provides basic user information. If you need additional permissions, you can specify scopes in your auth configuration: + +```ts title="auth.ts" +export const auth = betterAuth({ + socialProviders: { + reddit: { + clientId: process.env.REDDIT_CLIENT_ID as string, + clientSecret: process.env.REDDIT_CLIENT_SECRET as string, + duration: "permanent", + scopes: ["identity", "read", "submit"] // Add required scopes + }, + }, +}) +``` + +Common Reddit scopes include: +- `identity`: Access basic account information +- `read`: Access posts and comments +- `submit`: Submit posts and comments +- `subscribe`: Manage subreddit subscriptions +- `history`: Access voting history + +For a complete list of available scopes, refer to the [Reddit OAuth2 documentation](https://www.reddit.com/dev/api/oauth). diff --git a/packages/better-auth/src/oauth2/create-authorization-url.ts b/packages/better-auth/src/oauth2/create-authorization-url.ts index 387c508c..9baf5539 100644 --- a/packages/better-auth/src/oauth2/create-authorization-url.ts +++ b/packages/better-auth/src/oauth2/create-authorization-url.ts @@ -10,6 +10,7 @@ export async function createAuthorizationURL({ scopes, claims, redirectURI, + duration, }: { id: string; options: ProviderOptions; @@ -19,6 +20,7 @@ export async function createAuthorizationURL({ codeVerifier?: string; scopes: string[]; claims?: string[]; + duration?: string; }) { const url = new URL(authorizationEndpoint); url.searchParams.set("response_type", "code"); @@ -47,5 +49,9 @@ export async function createAuthorizationURL({ }), ); } + if (duration) { + url.searchParams.set("duration", duration); + } + return url; } diff --git a/packages/better-auth/src/social-providers/index.ts b/packages/better-auth/src/social-providers/index.ts index 12231214..e8ab1a5b 100644 --- a/packages/better-auth/src/social-providers/index.ts +++ b/packages/better-auth/src/social-providers/index.ts @@ -11,7 +11,7 @@ import { twitter } from "./twitter"; import { dropbox } from "./dropbox"; import { linkedin } from "./linkedin"; import { gitlab } from "./gitlab"; - +import { reddit } from "./reddit"; export const socialProviders = { apple, discord, @@ -25,6 +25,7 @@ export const socialProviders = { dropbox, linkedin, gitlab, + reddit, }; export const socialProviderList = Object.keys(socialProviders) as [ @@ -54,3 +55,4 @@ export * from "./twitter"; export * from "./dropbox"; export * from "./linkedin"; export * from "./gitlab"; +export * from "./reddit"; diff --git a/packages/better-auth/src/social-providers/reddit.ts b/packages/better-auth/src/social-providers/reddit.ts new file mode 100644 index 00000000..fb690f97 --- /dev/null +++ b/packages/better-auth/src/social-providers/reddit.ts @@ -0,0 +1,104 @@ +import { betterFetch } from "@better-fetch/fetch"; +import type { OAuthProvider, ProviderOptions } from "../oauth2"; +import { + createAuthorizationURL, + getOAuth2Tokens, + validateAuthorizationCode, +} from "../oauth2"; + +export interface RedditProfile { + id: string; + name: string; + icon_img: string | null; + has_verified_email: boolean; + oauth_client_id: string; + verified: boolean; +} + +export interface RedditOptions extends ProviderOptions { + duration?: string; +} + +export const reddit = (options: RedditOptions) => { + return { + id: "reddit", + name: "Reddit", + createAuthorizationURL({ state, scopes, redirectURI }) { + const _scopes = scopes || ["identity"]; + options.scope && _scopes.push(...options.scope); + + return createAuthorizationURL({ + id: "reddit", + options, + authorizationEndpoint: "https://www.reddit.com/api/v1/authorize", + scopes: _scopes, + state, + redirectURI, + duration: options.duration, + }); + }, + validateAuthorizationCode: async ({ code, redirectURI }) => { + const body = new URLSearchParams({ + grant_type: "authorization_code", + code, + redirect_uri: options.redirectURI || redirectURI, + }); + const headers = { + "content-type": "application/x-www-form-urlencoded", + accept: "text/plain", + "user-agent": "better-auth", + Authorization: `Basic ${Buffer.from( + `${options.clientId}:${options.clientSecret}`, + ).toString("base64")}`, + }; + + const { data, error } = await betterFetch( + "https://www.reddit.com/api/v1/access_token", + { + method: "POST", + headers, + body: body.toString(), + }, + ); + + if (error) { + throw error; + } + + return getOAuth2Tokens(data); + }, + async getUserInfo(token) { + if (options.getUserInfo) { + return options.getUserInfo(token); + } + + const { data: profile, error } = await betterFetch( + "https://oauth.reddit.com/api/v1/me", + { + headers: { + Authorization: `Bearer ${token.accessToken}`, + "User-Agent": "better-auth", + }, + }, + ); + + if (error) { + return null; + } + + const userMap = await options.mapProfileToUser?.(profile); + + return { + user: { + id: profile.id, + name: profile.name, + email: profile.oauth_client_id, + emailVerified: profile.has_verified_email, + image: profile.icon_img?.split("?")[0], + ...userMap, + }, + data: profile, + }; + }, + } satisfies OAuthProvider; +};