mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 04:19:32 +00:00
feat(social): add Kakao, Naver provider (#3287)
This commit is contained in:
@@ -230,6 +230,42 @@ export const socialProviders = {
|
||||
/>
|
||||
</svg>`,
|
||||
},
|
||||
kakao: {
|
||||
Icon: (props: SVGProps<any>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 512 512"
|
||||
{...props}
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M 511.5,203.5 C 511.5,215.5 511.5,227.5 511.5,239.5C 504.002,286.989 482.002,326.489 445.5,358C 390.216,402.375 326.882,424.209 255.5,423.5C 239.751,423.476 224.085,422.643 208.5,421C 174.34,444.581 140.006,467.914 105.5,491C 95.6667,493.167 91.8333,489.333 94,479.5C 101.833,450.667 109.667,421.833 117.5,393C 85.5639,376.077 58.0639,353.577 35,325.5C 15.8353,299.834 4.00193,271.167 -0.5,239.5C -0.5,227.5 -0.5,215.5 -0.5,203.5C 7.09119,155.407 29.4245,115.574 66.5,84C 121.53,39.9708 184.53,18.4708 255.5,19.5C 326.47,18.4708 389.47,39.9708 444.5,84C 481.575,115.574 503.909,155.407 511.5,203.5 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
stringIcon: `<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 512 512"><g><path fill="currentColor" d="M 511.5,203.5 C 511.5,215.5 511.5,227.5 511.5,239.5C 504.002,286.989 482.002,326.489 445.5,358C 390.216,402.375 326.882,424.209 255.5,423.5C 239.751,423.476 224.085,422.643 208.5,421C 174.34,444.581 140.006,467.914 105.5,491C 95.6667,493.167 91.8333,489.333 94,479.5C 101.833,450.667 109.667,421.833 117.5,393C 85.5639,376.077 58.0639,353.577 35,325.5C 15.8353,299.834 4.00193,271.167 -0.5,239.5C -0.5,227.5 -0.5,215.5 -0.5,203.5C 7.09119,155.407 29.4245,115.574 66.5,84C 121.53,39.9708 184.53,18.4708 255.5,19.5C 326.47,18.4708 389.47,39.9708 444.5,84C 481.575,115.574 503.909,155.407 511.5,203.5 Z"/></g></svg>`,
|
||||
},
|
||||
naver: {
|
||||
Icon: (props: SVGProps<any>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.273 12.845 7.376 0H0v24h7.726V11.156L16.624 24H24V0h-7.727v12.845Z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
stringIcon: `<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path fill="currentColor" d="M16.273 12.845 7.376 0H0v24h7.726V11.156L16.624 24H24V0h-7.727v12.845Z"/></svg>`,
|
||||
},
|
||||
linkedin: {
|
||||
Icon: (props: SVGProps<any>) => (
|
||||
<svg
|
||||
|
||||
@@ -637,6 +637,26 @@ export const contents: Content[] = [
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Kakao",
|
||||
isNew: true,
|
||||
href: "/docs/authentication/kakao",
|
||||
icon: (props?: SVGProps<any>) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M 511.5,203.5 C 511.5,215.5 511.5,227.5 511.5,239.5C 504.002,286.989 482.002,326.489 445.5,358C 390.216,402.375 326.882,424.209 255.5,423.5C 239.751,423.476 224.085,422.643 208.5,421C 174.34,444.581 140.006,467.914 105.5,491C 95.6667,493.167 91.8333,489.333 94,479.5C 101.833,450.667 109.667,421.833 117.5,393C 85.5639,376.077 58.0639,353.577 35,325.5C 15.8353,299.834 4.00193,271.167 -0.5,239.5C -0.5,227.5 -0.5,215.5 -0.5,203.5C 7.09119,155.407 29.4245,115.574 66.5,84C 121.53,39.9708 184.53,18.4708 255.5,19.5C 326.47,18.4708 389.47,39.9708 444.5,84C 481.575,115.574 503.909,155.407 511.5,203.5 Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Kick",
|
||||
href: "/docs/authentication/kick",
|
||||
@@ -754,6 +774,24 @@ export const contents: Content[] = [
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Naver",
|
||||
href: "/docs/authentication/naver",
|
||||
isNew: true,
|
||||
icon: () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M16.273 12.845 7.376 0H0v24h7.726V11.156L16.624 24H24V0h-7.727v12.845Z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Tiktok",
|
||||
href: "/docs/authentication/tiktok",
|
||||
|
||||
47
docs/content/docs/authentication/kakao.mdx
Normal file
47
docs/content/docs/authentication/kakao.mdx
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Kakao
|
||||
description: Kakao provider setup and usage.
|
||||
---
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Get your Kakao Credentials
|
||||
To use Kakao sign in, you need a client ID and client secret. You can get them from the [Kakao Developer Portal](https://developers.kakao.com).
|
||||
|
||||
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/kakao` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
|
||||
</Step>
|
||||
<Step>
|
||||
### 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: {
|
||||
kakao: { // [!code highlight]
|
||||
clientId: process.env.KAKAO_CLIENT_ID as string, // [!code highlight]
|
||||
clientSecret: process.env.KAKAO_CLIENT_SECRET as string, // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
}
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Sign In with Kakao
|
||||
To sign in with Kakao, 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 `kakao`.
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
const authClient = createAuthClient()
|
||||
|
||||
const signIn = async () => {
|
||||
const data = await authClient.signIn.social({
|
||||
provider: "kakao"
|
||||
})
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
47
docs/content/docs/authentication/naver.mdx
Normal file
47
docs/content/docs/authentication/naver.mdx
Normal file
@@ -0,0 +1,47 @@
|
||||
---
|
||||
title: Naver
|
||||
description: Naver provider setup and usage.
|
||||
---
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Get your Naver Credentials
|
||||
To use Naver sign in, you need a client ID and client secret. You can get them from the [Naver Developers](https://developers.naver.com/).
|
||||
|
||||
Make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/naver` for local development. For production, you should set it to the URL of your application. If you change the base path of the auth routes, you should update the redirect URL accordingly.
|
||||
</Step>
|
||||
<Step>
|
||||
### 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: {
|
||||
naver: { // [!code highlight]
|
||||
clientId: process.env.NAVER_CLIENT_ID as string, // [!code highlight]
|
||||
clientSecret: process.env.NAVER_CLIENT_SECRET as string, // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
}
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
<Step>
|
||||
### Sign In with Naver
|
||||
To sign in with Naver, 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 `naver`.
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
const authClient = createAuthClient()
|
||||
|
||||
const signIn = async () => {
|
||||
const data = await authClient.signIn.social({
|
||||
provider: "naver"
|
||||
})
|
||||
}
|
||||
```
|
||||
</Step>
|
||||
|
||||
</Steps>
|
||||
@@ -25,8 +25,11 @@ import { roblox } from "./roblox";
|
||||
import { salesforce } from "./salesforce";
|
||||
import { vk } from "./vk";
|
||||
import { zoom } from "./zoom";
|
||||
import { kakao } from "./kakao";
|
||||
import { naver } from "./naver";
|
||||
import { line } from "./line";
|
||||
import { paypal } from "./paypal";
|
||||
|
||||
export const socialProviders = {
|
||||
apple,
|
||||
atlassian,
|
||||
@@ -53,6 +56,8 @@ export const socialProviders = {
|
||||
vk,
|
||||
zoom,
|
||||
notion,
|
||||
kakao,
|
||||
naver,
|
||||
line,
|
||||
paypal,
|
||||
};
|
||||
@@ -103,6 +108,8 @@ export * from "./zoom";
|
||||
export * from "./kick";
|
||||
export * from "./huggingface";
|
||||
export * from "./slack";
|
||||
export * from "./kakao";
|
||||
export * from "./naver";
|
||||
export * from "./line";
|
||||
export * from "./paypal";
|
||||
|
||||
|
||||
176
packages/better-auth/src/social-providers/kakao.ts
Normal file
176
packages/better-auth/src/social-providers/kakao.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { betterFetch } from "@better-fetch/fetch";
|
||||
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
||||
import {
|
||||
createAuthorizationURL,
|
||||
validateAuthorizationCode,
|
||||
refreshAccessToken,
|
||||
} from "../oauth2";
|
||||
|
||||
interface Partner {
|
||||
/** Partner-specific ID (consent required: kakaotalk_message) */
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
/** Nickname (consent required: profile/nickname) */
|
||||
nickname?: string;
|
||||
/** Thumbnail image URL (consent required: profile/profile image) */
|
||||
thumbnail_image_url?: string;
|
||||
/** Profile image URL (consent required: profile/profile image) */
|
||||
profile_image_url?: string;
|
||||
/** Whether the profile image is the default */
|
||||
is_default_image?: boolean;
|
||||
/** Whether the nickname is the default */
|
||||
is_default_nickname?: boolean;
|
||||
}
|
||||
|
||||
interface KakaoAccount {
|
||||
/** Consent required: profile info (nickname/profile image) */
|
||||
profile_needs_agreement?: boolean;
|
||||
/** Consent required: nickname */
|
||||
profile_nickname_needs_agreement?: boolean;
|
||||
/** Consent required: profile image */
|
||||
profile_image_needs_agreement?: boolean;
|
||||
/** Profile info */
|
||||
profile?: Profile;
|
||||
/** Consent required: name */
|
||||
name_needs_agreement?: boolean;
|
||||
/** Name */
|
||||
name?: string;
|
||||
/** Consent required: email */
|
||||
email_needs_agreement?: boolean;
|
||||
/** Email valid */
|
||||
is_email_valid?: boolean;
|
||||
/** Email verified */
|
||||
is_email_verified?: boolean;
|
||||
/** Email */
|
||||
email?: string;
|
||||
/** Consent required: age range */
|
||||
age_range_needs_agreement?: boolean;
|
||||
/** Age range */
|
||||
age_range?: string;
|
||||
/** Consent required: birth year */
|
||||
birthyear_needs_agreement?: boolean;
|
||||
/** Birth year (YYYY) */
|
||||
birthyear?: string;
|
||||
/** Consent required: birthday */
|
||||
birthday_needs_agreement?: boolean;
|
||||
/** Birthday (MMDD) */
|
||||
birthday?: string;
|
||||
/** Birthday type (SOLAR/LUNAR) */
|
||||
birthday_type?: string;
|
||||
/** Whether birthday is in a leap month */
|
||||
is_leap_month?: boolean;
|
||||
/** Consent required: gender */
|
||||
gender_needs_agreement?: boolean;
|
||||
/** Gender (male/female) */
|
||||
gender?: string;
|
||||
/** Consent required: phone number */
|
||||
phone_number_needs_agreement?: boolean;
|
||||
/** Phone number */
|
||||
phone_number?: string;
|
||||
/** Consent required: CI */
|
||||
ci_needs_agreement?: boolean;
|
||||
/** CI (unique identifier) */
|
||||
ci?: string;
|
||||
/** CI authentication time (UTC) */
|
||||
ci_authenticated_at?: string;
|
||||
}
|
||||
|
||||
export interface KakaoProfile {
|
||||
/** Kakao user ID */
|
||||
id: number;
|
||||
/**
|
||||
* Whether the user has signed up (only present if auto-connection is disabled)
|
||||
* false: preregistered, true: registered
|
||||
*/
|
||||
has_signed_up?: boolean;
|
||||
/** UTC datetime when the user connected the service */
|
||||
connected_at?: string;
|
||||
/** UTC datetime when the user signed up via Kakao Sync */
|
||||
synched_at?: string;
|
||||
/** Custom user properties */
|
||||
properties?: Record<string, any>;
|
||||
/** Kakao account info */
|
||||
kakao_account: KakaoAccount;
|
||||
/** Partner info */
|
||||
for_partner?: Partner;
|
||||
}
|
||||
|
||||
export interface KakaoOptions extends ProviderOptions<KakaoProfile> {}
|
||||
|
||||
export const kakao = (options: KakaoOptions) => {
|
||||
return {
|
||||
id: "kakao",
|
||||
name: "Kakao",
|
||||
createAuthorizationURL({ state, scopes, redirectURI }) {
|
||||
const _scopes = options.disableDefaultScope
|
||||
? []
|
||||
: ["account_email", "profile_image", "profile_nickname"];
|
||||
options.scope && _scopes.push(...options.scope);
|
||||
scopes && _scopes.push(...scopes);
|
||||
return createAuthorizationURL({
|
||||
id: "kakao",
|
||||
options,
|
||||
authorizationEndpoint: "https://kauth.kakao.com/oauth/authorize",
|
||||
scopes: _scopes,
|
||||
state,
|
||||
redirectURI,
|
||||
});
|
||||
},
|
||||
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
||||
return validateAuthorizationCode({
|
||||
code,
|
||||
redirectURI,
|
||||
options,
|
||||
tokenEndpoint: "https://kauth.kakao.com/oauth/token",
|
||||
});
|
||||
},
|
||||
refreshAccessToken: options.refreshAccessToken
|
||||
? options.refreshAccessToken
|
||||
: async (refreshToken) => {
|
||||
return refreshAccessToken({
|
||||
refreshToken,
|
||||
options: {
|
||||
clientId: options.clientId,
|
||||
clientKey: options.clientKey,
|
||||
clientSecret: options.clientSecret,
|
||||
},
|
||||
tokenEndpoint: "https://kauth.kakao.com/oauth/token",
|
||||
});
|
||||
},
|
||||
async getUserInfo(token) {
|
||||
if (options.getUserInfo) {
|
||||
return options.getUserInfo(token);
|
||||
}
|
||||
const { data: profile, error } = await betterFetch<KakaoProfile>(
|
||||
"https://kapi.kakao.com/v2/user/me",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (error || !profile) {
|
||||
return null;
|
||||
}
|
||||
const userMap = await options.mapProfileToUser?.(profile);
|
||||
const account = profile.kakao_account || {};
|
||||
const kakaoProfile = account.profile || {};
|
||||
const user = {
|
||||
id: String(profile.id),
|
||||
name: kakaoProfile.nickname || account.name || undefined,
|
||||
email: account.email,
|
||||
image:
|
||||
kakaoProfile.profile_image_url || kakaoProfile.thumbnail_image_url,
|
||||
emailVerified: !!account.is_email_valid && !!account.is_email_verified,
|
||||
...userMap,
|
||||
};
|
||||
return {
|
||||
user,
|
||||
data: profile,
|
||||
};
|
||||
},
|
||||
options,
|
||||
} satisfies OAuthProvider<KakaoProfile>;
|
||||
};
|
||||
110
packages/better-auth/src/social-providers/naver.ts
Normal file
110
packages/better-auth/src/social-providers/naver.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { betterFetch } from "@better-fetch/fetch";
|
||||
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
||||
import {
|
||||
createAuthorizationURL,
|
||||
validateAuthorizationCode,
|
||||
refreshAccessToken,
|
||||
} from "../oauth2";
|
||||
|
||||
export interface NaverProfile {
|
||||
/** API response result code */
|
||||
resultcode: string;
|
||||
/** API response message */
|
||||
message: string;
|
||||
response: {
|
||||
/** Unique Naver user identifier */
|
||||
id: string;
|
||||
/** User nickname */
|
||||
nickname: string;
|
||||
/** User real name */
|
||||
name: string;
|
||||
/** User email address */
|
||||
email: string;
|
||||
/** Gender (F: female, M: male, U: unknown) */
|
||||
gender: string;
|
||||
/** Age range */
|
||||
age: string;
|
||||
/** Birthday (MM-DD format) */
|
||||
birthday: string;
|
||||
/** Birth year */
|
||||
birthyear: string;
|
||||
/** Profile image URL */
|
||||
profile_image: string;
|
||||
/** Mobile phone number */
|
||||
mobile: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NaverOptions extends ProviderOptions<NaverProfile> {}
|
||||
|
||||
export const naver = (options: NaverOptions) => {
|
||||
return {
|
||||
id: "naver",
|
||||
name: "Naver",
|
||||
createAuthorizationURL({ state, scopes, redirectURI }) {
|
||||
const _scopes = options.disableDefaultScope ? [] : ["profile", "email"];
|
||||
options.scope && _scopes.push(...options.scope);
|
||||
scopes && _scopes.push(...scopes);
|
||||
return createAuthorizationURL({
|
||||
id: "naver",
|
||||
options,
|
||||
authorizationEndpoint: "https://nid.naver.com/oauth2.0/authorize",
|
||||
scopes: _scopes,
|
||||
state,
|
||||
redirectURI,
|
||||
});
|
||||
},
|
||||
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
||||
return validateAuthorizationCode({
|
||||
code,
|
||||
redirectURI,
|
||||
options,
|
||||
tokenEndpoint: "https://nid.naver.com/oauth2.0/token",
|
||||
});
|
||||
},
|
||||
refreshAccessToken: options.refreshAccessToken
|
||||
? options.refreshAccessToken
|
||||
: async (refreshToken) => {
|
||||
return refreshAccessToken({
|
||||
refreshToken,
|
||||
options: {
|
||||
clientId: options.clientId,
|
||||
clientKey: options.clientKey,
|
||||
clientSecret: options.clientSecret,
|
||||
},
|
||||
tokenEndpoint: "https://nid.naver.com/oauth2.0/token",
|
||||
});
|
||||
},
|
||||
async getUserInfo(token) {
|
||||
if (options.getUserInfo) {
|
||||
return options.getUserInfo(token);
|
||||
}
|
||||
const { data: profile, error } = await betterFetch<NaverProfile>(
|
||||
"https://openapi.naver.com/v1/nid/me",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (error || !profile || profile.resultcode !== "00") {
|
||||
return null;
|
||||
}
|
||||
const userMap = await options.mapProfileToUser?.(profile);
|
||||
const res = profile.response || {};
|
||||
const user = {
|
||||
id: res.id,
|
||||
name: res.name || res.nickname,
|
||||
email: res.email,
|
||||
image: res.profile_image,
|
||||
emailVerified: false,
|
||||
...userMap,
|
||||
};
|
||||
return {
|
||||
user,
|
||||
data: profile,
|
||||
};
|
||||
},
|
||||
options,
|
||||
} satisfies OAuthProvider<NaverProfile>;
|
||||
};
|
||||
Reference in New Issue
Block a user