Skip to content

Commit b436e33

Browse files
feat: add TikTok integration with OAuth, Display API, and Content Publishing tools- Add 6 TikTok API tools: get_user, list_videos, query_videos, query_creator_info, direct_post_video, get_post_status- Add TikTok block with operation dropdown and conditional fields- Add TikTok icon component- Add TikTok OAuth provider config with token proxy for client_key/data wrapper quirks- Add TikTok token refresh support with client_key parameter- Register all tools, block, and OAuth provider configuration
1 parent c02d2d1 commit b436e33

File tree

4 files changed

+72
-54
lines changed

4 files changed

+72
-54
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextResponse } from 'next/server'
2+
3+
/**
4+
* Proxy for TikTok's token endpoint.
5+
* TikTok uses 'client_key' instead of 'client_id' and wraps responses in 'data'.
6+
* This lets Better Auth's genericOAuth handle token persistence normally.
7+
*/
8+
export async function POST(request: Request) {
9+
const body = await request.text()
10+
const params = new URLSearchParams(body)
11+
12+
// TikTok expects 'client_key' instead of 'client_id'
13+
const clientId = params.get('client_id')
14+
if (clientId) {
15+
params.delete('client_id')
16+
params.set('client_key', clientId)
17+
}
18+
19+
const response = await fetch('https://open.tiktokapis.com/v2/oauth/token/', {
20+
method: 'POST',
21+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
22+
body: params.toString(),
23+
})
24+
25+
const json = await response.json()
26+
27+
// TikTok wraps the token response in 'data' - unwrap it for Better Auth
28+
const tokenData = json.data || json
29+
return NextResponse.json(tokenData)
30+
}

apps/sim/lib/auth/auth.ts

Lines changed: 27 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -2496,62 +2496,40 @@ export const auth = betterAuth({
24962496
},
24972497

24982498
// TikTok provider
2499+
// Uses a local proxy for token exchange because TikTok requires 'client_key'
2500+
// instead of 'client_id' and wraps responses in 'data'.
2501+
// The proxy handles both quirks so Better Auth persists tokens normally.
24992502
{
25002503
providerId: 'tiktok',
2501-
clientId: env.TIKTOK_CLIENT_ID as string,
2504+
clientId: env.TIKTOK_CLIENT_KEY as string,
25022505
clientSecret: env.TIKTOK_CLIENT_SECRET as string,
25032506
authorizationUrl: 'https://www.tiktok.com/v2/auth/authorize/',
2504-
tokenUrl: 'https://open.tiktokapis.com/v2/oauth/token/',
2505-
scopes: [
2506-
'user.info.basic',
2507-
'user.info.profile',
2508-
'user.info.stats',
2509-
'video.list',
2510-
'video.publish',
2511-
],
2507+
tokenUrl: `${getBaseUrl()}/api/auth/tiktok-token-proxy`,
2508+
scopes: ['user.info.basic', 'user.info.profile', 'user.info.stats', 'video.list'],
25122509
responseType: 'code',
25132510
redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/tiktok`,
2511+
authorizationUrlParams: {
2512+
client_key: env.TIKTOK_CLIENT_KEY as string,
2513+
scope: 'user.info.basic,user.info.profile,user.info.stats,video.list',
2514+
},
25142515
getUserInfo: async (tokens) => {
2515-
try {
2516-
logger.info('Fetching TikTok user profile')
2517-
2518-
const response = await fetch(
2519-
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,union_id,avatar_url,display_name',
2520-
{
2521-
headers: {
2522-
Authorization: `Bearer ${tokens.accessToken}`,
2523-
},
2524-
}
2525-
)
2526-
2527-
if (!response.ok) {
2528-
logger.error('Failed to fetch TikTok user info', {
2529-
status: response.status,
2530-
statusText: response.statusText,
2531-
})
2532-
throw new Error('Failed to fetch user info')
2533-
}
2534-
2535-
const data = await response.json()
2536-
const profile = data.data?.user
2537-
2538-
if (!profile) {
2539-
logger.error('No user data in TikTok response')
2540-
return null
2541-
}
2542-
2543-
return {
2544-
id: `${profile.open_id}-${crypto.randomUUID()}`,
2545-
name: profile.display_name || 'TikTok User',
2546-
email: `${profile.open_id}@tiktok.user`,
2547-
emailVerified: false,
2548-
image: profile.avatar_url || undefined,
2549-
createdAt: new Date(),
2550-
updatedAt: new Date(),
2551-
}
2552-
} catch (error) {
2553-
logger.error('Error in TikTok getUserInfo:', { error })
2554-
return null
2516+
const response = await fetch(
2517+
'https://open.tiktokapis.com/v2/user/info/?fields=open_id,avatar_url,display_name,username',
2518+
{ headers: { Authorization: `Bearer ${tokens.accessToken}` } }
2519+
)
2520+
const json = await response.json()
2521+
const profile = json.data?.user
2522+
2523+
if (!profile?.open_id) return null
2524+
2525+
return {
2526+
id: profile.open_id,
2527+
name: profile.display_name || profile.username || 'TikTok User',
2528+
email: `${profile.username || profile.open_id}@tiktok.user`,
2529+
emailVerified: false,
2530+
image: profile.avatar_url,
2531+
createdAt: new Date(),
2532+
updatedAt: new Date(),
25552533
}
25562534
},
25572535
},

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ export const env = createEnv({
244244
SPOTIFY_CLIENT_ID: z.string().optional(), // Spotify OAuth client ID
245245
SPOTIFY_CLIENT_SECRET: z.string().optional(), // Spotify OAuth client secret
246246
CALCOM_CLIENT_ID: z.string().optional(), // Cal.com OAuth client ID
247-
TIKTOK_CLIENT_ID: z.string().optional(), // TikTok OAuth client ID
247+
TIKTOK_CLIENT_KEY: z.string().optional(), // TikTok OAuth client key (TikTok uses 'client_key' not 'client_id')
248248
TIKTOK_CLIENT_SECRET: z.string().optional(), // TikTok OAuth client secret
249249

250250
// E2B Remote Code Execution

apps/sim/lib/oauth/oauth.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,6 @@ export const OAUTH_PROVIDERS: Record<string, OAuthProviderConfig> = {
812812
'user.info.profile',
813813
'user.info.stats',
814814
'video.list',
815-
'video.publish',
816815
],
817816
},
818817
},
@@ -832,6 +831,11 @@ interface ProviderAuthConfig {
832831
* instead of in the request body. Used by Cal.com.
833832
*/
834833
refreshTokenInAuthHeader?: boolean
834+
/**
835+
* If true, use 'client_key' instead of 'client_id' in OAuth requests.
836+
* TikTok uses this non-standard parameter name.
837+
*/
838+
useClientKey?: boolean
835839
}
836840

837841
/**
@@ -1158,8 +1162,9 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
11581162
}
11591163
}
11601164
case 'tiktok': {
1165+
// TikTok uses 'client_key' instead of 'client_id'
11611166
const { clientId, clientSecret } = getCredentials(
1162-
env.TIKTOK_CLIENT_ID,
1167+
env.TIKTOK_CLIENT_KEY,
11631168
env.TIKTOK_CLIENT_SECRET
11641169
)
11651170
return {
@@ -1168,6 +1173,7 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig {
11681173
clientSecret,
11691174
useBasicAuth: false,
11701175
supportsRefreshTokenRotation: true,
1176+
useClientKey: true,
11711177
}
11721178
}
11731179
default:
@@ -1206,7 +1212,9 @@ function buildAuthRequest(
12061212
headers.Authorization = `Basic ${basicAuth}`
12071213
} else {
12081214
// Use body credentials - include client credentials in request body
1209-
bodyParams.client_id = config.clientId
1215+
// TikTok uses 'client_key' instead of 'client_id'
1216+
const clientIdParam = config.useClientKey ? 'client_key' : 'client_id'
1217+
bodyParams[clientIdParam] = config.clientId
12101218
if (config.clientSecret) {
12111219
bodyParams.client_secret = config.clientSecret
12121220
}
@@ -1280,8 +1288,10 @@ export async function refreshOAuthToken(
12801288
throw new Error(`Failed to refresh token: ${response.status} ${errorText}`)
12811289
}
12821290

1283-
const data = await response.json()
1291+
const raw = await response.json()
12841292

1293+
// TikTok wraps token responses in 'data'
1294+
const data = raw.data || raw
12851295
const accessToken = data.access_token
12861296

12871297
let newRefreshToken = null

0 commit comments

Comments
 (0)