Files
Sahaj Jain bb8df47a8c OAuth 2.0 API for external app integrations (#267)
* 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>
2026-03-06 22:05:20 +05:30

207 lines
4.5 KiB
Markdown

# 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:
```bash
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
```bash
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:
```json
{
"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:
```bash
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:
```bash
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
```bash
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:
```typescript
// 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"],
},
],
}),
],
});
```
```typescript
// 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.
```json
{
"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.
```json
{
"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.
```json
{
"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:
```json
{
"error": "invalid_token",
"error_description": "Invalid or expired access token"
}
```