From 07cdd67daedcd8e1122291a0acf0468a60ebad1c Mon Sep 17 00:00:00 2001 From: Nico Labarre Date: Tue, 16 Dec 2025 22:57:00 -0500 Subject: [PATCH] feat: add patreon social provider (#6245) Co-authored-by: benkingcode Co-authored-by: Kinfe123 --- docs/content/docs/plugins/generic-oauth.mdx | 8 +- .../plugins/generic-oauth/providers/index.ts | 1 + .../generic-oauth/providers/patreon.ts | 83 +++++++++++++++++++ 3 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 packages/better-auth/src/plugins/generic-oauth/providers/patreon.ts diff --git a/docs/content/docs/plugins/generic-oauth.mdx b/docs/content/docs/plugins/generic-oauth.mdx index 7baa151751..cc87da0c8e 100644 --- a/docs/content/docs/plugins/generic-oauth.mdx +++ b/docs/content/docs/plugins/generic-oauth.mdx @@ -138,12 +138,13 @@ Better Auth provides pre-configured helper functions for popular OAuth providers - **Microsoft Entra ID (Azure AD)** - `microsoftEntraId(options)` - **Okta** - `okta(options)` - **Slack** - `slack(options)` +- **Patreon** - `patreon(options)` ### Example: Using Pre-configured Providers ```ts title="auth.ts" import { betterAuth } from "better-auth" -import { genericOAuth, auth0, hubspot, keycloak, line, microsoftEntraId, okta, slack } from "better-auth/plugins" +import { genericOAuth, auth0, hubspot, keycloak, line, microsoftEntraId, okta, slack, patreon } from "better-auth/plugins" export const auth = betterAuth({ plugins: [ @@ -189,6 +190,10 @@ export const auth = betterAuth({ clientId: process.env.SLACK_CLIENT_ID, clientSecret: process.env.SLACK_CLIENT_SECRET, }), + patreon({ + clientId: process.env.PATREON_CLIENT_ID, + clientSecret: process.env.PATREON_CLIENT_SECRET, + }), ], }), ], @@ -204,6 +209,7 @@ Each provider helper accepts common OAuth options (extending `BaseOAuthProviderO - **Microsoft Entra ID**: Requires `tenantId` (can be a GUID, `"common"`, `"organizations"`, or `"consumers"`) - **Okta**: Requires `issuer` (e.g., `https://dev-xxxxx.okta.com/oauth2/default`) - **Slack**: No additional required fields +- **Patreon**: No additional required fields All providers support the same optional fields: - `scopes?: string[]` - Array of OAuth scopes to request diff --git a/packages/better-auth/src/plugins/generic-oauth/providers/index.ts b/packages/better-auth/src/plugins/generic-oauth/providers/index.ts index 2b32e68a06..ed8b7a6cc4 100644 --- a/packages/better-auth/src/plugins/generic-oauth/providers/index.ts +++ b/packages/better-auth/src/plugins/generic-oauth/providers/index.ts @@ -32,4 +32,5 @@ export { microsoftEntraId, } from "./microsoft-entra-id"; export { type OktaOptions, okta } from "./okta"; +export { type PatreonOptions, patreon } from "./patreon"; export { type SlackOptions, slack } from "./slack"; diff --git a/packages/better-auth/src/plugins/generic-oauth/providers/patreon.ts b/packages/better-auth/src/plugins/generic-oauth/providers/patreon.ts new file mode 100644 index 0000000000..af14ca4d00 --- /dev/null +++ b/packages/better-auth/src/plugins/generic-oauth/providers/patreon.ts @@ -0,0 +1,83 @@ +import type { OAuth2Tokens, OAuth2UserInfo } from "@better-auth/core/oauth2"; +import { betterFetch } from "@better-fetch/fetch"; +import type { BaseOAuthProviderOptions, GenericOAuthConfig } from "../index"; + +export interface PatreonOptions extends BaseOAuthProviderOptions {} + +interface PatreonProfile { + data: { + id: string; + attributes: { + full_name: string; + email: string; + image_url: string; + is_email_verified: boolean; + }; + }; +} + +/** + * Patreon OAuth provider helper + * + * @example + * ```ts + * import { genericOAuth, patreon } from "better-auth/plugins/generic-oauth"; + * + * export const auth = betterAuth({ + * plugins: [ + * genericOAuth({ + * config: [ + * patreon({ + * clientId: process.env.PATREON_CLIENT_ID, + * clientSecret: process.env.PATREON_CLIENT_SECRET, + * }), + * ], + * }), + * ], + * }); + * ``` + */ +export function patreon(options: PatreonOptions): GenericOAuthConfig { + const defaultScopes = ["identity[email]"]; + + const getUserInfo = async ( + tokens: OAuth2Tokens, + ): Promise => { + const { data: profile, error } = await betterFetch( + "https://www.patreon.com/api/oauth2/v2/identity?fields[user]=email,full_name,image_url,is_email_verified", + { + method: "GET", + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }, + ); + + if (error || !profile) { + return null; + } + + return { + id: profile.data.id, + name: profile.data.attributes.full_name, + email: profile.data.attributes.email, + image: profile.data.attributes.image_url, + emailVerified: profile.data.attributes.is_email_verified, + }; + }; + + return { + providerId: "patreon", + authorizationUrl: "https://www.patreon.com/oauth2/authorize", + tokenUrl: "https://www.patreon.com/api/oauth2/token", + clientId: options.clientId, + clientSecret: options.clientSecret, + scopes: options.scopes ?? defaultScopes, + redirectURI: options.redirectURI, + pkce: options.pkce, + disableImplicitSignUp: options.disableImplicitSignUp, + disableSignUp: options.disableSignUp, + overrideUserInfo: options.overrideUserInfo, + getUserInfo, + }; +}