* OAuth 2.0 API for external app integrations Implements the Authorization Code flow so external apps can authenticate tweakcn users and access their themes/profile via a REST API. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: simplify OAuth API routes and improve efficiency - Extract `requireAuth` helper to eliminate duplicated auth+scope boilerplate across all v1 API routes - Narrow SELECT queries to only fetch needed columns in hot paths (resolveUserFromBearerToken, authenticateClient, authorize, token) - Reuse `generateSecureToken`/`hashSecret` in create-oauth-app script instead of duplicating crypto logic - Unify sign-in handlers and loading state in OAuth authorize page - Use `oauthError` consistently for 404 responses Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * replace demo app with OAuth API documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * add /api/oauth/userinfo endpoint for genericOAuth compatibility Returns flat OIDC-style fields (sub, name, email, picture) so Better Auth's genericOAuth plugin and similar clients work out of the box without custom getUserInfo mapping. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
4.5 KiB
OAuth 2.0 API
tweakcn exposes an OAuth 2.0 API so external apps can authenticate users and access their data.
Registering an app
Register an OAuth app via the CLI script:
npx tsx scripts/create-oauth-app.ts \
--name "My App" \
--redirect-uris "https://myapp.com/callback" \
--scopes "themes:read,profile:read" \
--description "Optional description"
This outputs a client_id and client_secret. The secret is shown once and cannot be retrieved later.
Authorization flow
Standard OAuth 2.0 Authorization Code flow. PKCE is supported but optional.
1. Redirect the user to authorize
GET https://tweakcn.com/api/oauth/authorize
?client_id=CLIENT_ID
&redirect_uri=https://myapp.com/callback
&response_type=code
&scope=themes:read profile:read
&state=RANDOM_STRING
If the user is not signed in, they'll be shown a sign-in page. After signing in, they're redirected to your redirect_uri with an authorization code:
https://myapp.com/callback?code=AUTH_CODE&state=RANDOM_STRING
2. Exchange the code for tokens
curl -X POST https://tweakcn.com/api/oauth/token \
-d grant_type=authorization_code \
-d client_id=CLIENT_ID \
-d client_secret=CLIENT_SECRET \
-d code=AUTH_CODE \
-d redirect_uri=https://myapp.com/callback
Response:
{
"access_token": "...",
"refresh_token": "...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "themes:read profile:read"
}
3. Call the API
Pass the access token as a Bearer token:
curl https://tweakcn.com/api/v1/themes \
-H "Authorization: Bearer ACCESS_TOKEN"
4. Refresh tokens
Access tokens expire after 1 hour. Use the refresh token to get a new pair:
curl -X POST https://tweakcn.com/api/oauth/token \
-d grant_type=refresh_token \
-d client_id=CLIENT_ID \
-d client_secret=CLIENT_SECRET \
-d refresh_token=REFRESH_TOKEN
5. Revoke tokens
curl -X POST https://tweakcn.com/api/oauth/revoke \
-d token=ACCESS_OR_REFRESH_TOKEN
Using with Better Auth's genericOAuth
tweakcn works as a provider with Better Auth's genericOAuth plugin:
// server
import { genericOAuth } from "better-auth/plugins";
export const auth = betterAuth({
plugins: [
genericOAuth({
config: [
{
providerId: "tweakcn",
clientId: process.env.TWEAKCN_CLIENT_ID,
clientSecret: process.env.TWEAKCN_CLIENT_SECRET,
authorizationUrl: "https://tweakcn.com/api/oauth/authorize",
tokenUrl: "https://tweakcn.com/api/oauth/token",
userInfoUrl: "https://tweakcn.com/api/oauth/userinfo",
scopes: ["themes:read", "profile:read"],
},
],
}),
],
});
// client
import { genericOAuthClient } from "better-auth/client/plugins";
const authClient = createAuthClient({
plugins: [genericOAuthClient()],
});
await authClient.signIn.oauth2({
providerId: "tweakcn",
callbackURL: "/dashboard",
});
API endpoints
All endpoints require Authorization: Bearer <access_token>.
GET /api/oauth/userinfo
OIDC-compatible userinfo endpoint. Returns flat user fields. Requires profile:read scope.
{
"sub": "user_123",
"name": "Jane Doe",
"email": "jane@example.com",
"picture": "https://..."
}
GET /api/v1/me
Returns the authenticated user's profile. Requires profile:read scope.
{
"data": {
"id": "...",
"name": "Jane Doe",
"email": "jane@example.com",
"image": "https://..."
}
}
GET /api/v1/themes
Returns all themes owned by the authenticated user. Requires themes:read scope.
{
"data": [
{
"id": "...",
"name": "My Theme",
"styles": { ... },
"createdAt": "2025-01-01T00:00:00.000Z",
"updatedAt": "2025-01-01T00:00:00.000Z"
}
]
}
GET /api/v1/themes/:themeId
Returns a single theme by ID. Only returns themes owned by the authenticated user. Requires themes:read scope.
Scopes
| Scope | Description |
|---|---|
themes:read |
Read the user's saved themes |
profile:read |
Read the user's profile (name, email, avatar) |
PKCE support
For public clients (e.g. SPAs, mobile apps), use PKCE by adding code_challenge and code_challenge_method=S256 to the authorize request, then code_verifier when exchanging the code.
Error responses
All error responses follow the OAuth 2.0 spec:
{
"error": "invalid_token",
"error_description": "Invalid or expired access token"
}