Skip to main content

Authentication

Pinboard API uses JWT (JSON Web Token) authentication. All API endpoints (except health checks) require a valid JWT token in the Authorization header.

Overview

The authentication flow works as follows:

  1. Login via SSO - Users authenticate through Google, Microsoft, or GitHub OAuth
  2. Token Exchange - Exchange the NextAuth session for an API JWT token
  3. API Requests - Include the JWT in all subsequent API requests
  4. Token Refresh - Refresh tokens before they expire
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│ Browser │───▶│ Next.js │───▶│ OAuth │
│ │◀───│ NextAuth │◀───│ Provider │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ JWT Token │ Creates JWT
▼ │
┌─────────────┐ │
│ FastAPI │◀──────────┘
│ Backend │ Validates JWT
└─────────────┘

Getting a Token

Step 1: Exchange Session for JWT

After logging in via SSO, exchange your session for an API token:

curl -X POST "https://api.pinboard.ai/api/v1/auth/token/exchange" \
-H "Content-Type: application/json" \
-d '{
"session_token": "your-nextauth-session-token"
}'

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 86400
}

Step 2: Using the Token

Include the access token in the Authorization header for all API requests:

curl "https://api.pinboard.ai/api/v1/users/me" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Token Lifecycle

Access Token

  • Duration: 24 hours (86400 seconds)
  • Usage: Include in Authorization: Bearer <token> header
  • Content: Contains user ID, email, username, and site role

Refresh Token

  • Duration: 30 days
  • Usage: Exchange for a new access token before expiry
  • Storage: Store securely, never expose in client-side code

Refreshing Tokens

When your access token is about to expire, use the refresh token to get a new one:

curl -X POST "https://api.pinboard.ai/api/v1/auth/token/refresh" \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "your-refresh-token"
}'

Response:

{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 86400
}

JWT Token Structure

The JWT access token contains the following claims:

ClaimDescription
subUser ID (UUID)
emailUser's email address
usernameUser's username
site_roleSite-wide role (e.g., "admin", "user")
expToken expiration timestamp
iatToken issued-at timestamp

Example decoded token:

{
"sub": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"username": "johndoe",
"site_role": "user",
"exp": 1705449600,
"iat": 1705363200
}

Code Examples

TypeScript/JavaScript

// Token exchange
async function getApiToken(sessionToken: string): Promise<TokenResponse> {
const response = await fetch('https://api.pinboard.ai/api/v1/auth/token/exchange', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ session_token: sessionToken }),
});

if (!response.ok) {
throw new Error('Token exchange failed');
}

return response.json();
}

// API request with token
async function getCurrentUser(accessToken: string) {
const response = await fetch('https://api.pinboard.ai/api/v1/users/me', {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
});

if (!response.ok) {
throw new Error('Failed to get user');
}

return response.json();
}

Python

import requests

API_URL = "https://api.pinboard.ai"

def get_api_token(session_token: str) -> dict:
"""Exchange session token for API JWT."""
response = requests.post(
f"{API_URL}/api/v1/auth/token/exchange",
json={"session_token": session_token}
)
response.raise_for_status()
return response.json()

def get_current_user(access_token: str) -> dict:
"""Get current user info."""
response = requests.get(
f"{API_URL}/api/v1/users/me",
headers={"Authorization": f"Bearer {access_token}"}
)
response.raise_for_status()
return response.json()

# Usage
tokens = get_api_token("your-session-token")
user = get_current_user(tokens["access_token"])
print(f"Logged in as: {user['email']}")

cURL

# Get token
TOKEN=$(curl -s -X POST "https://api.pinboard.ai/api/v1/auth/token/exchange" \
-H "Content-Type: application/json" \
-d '{"session_token": "your-session-token"}' \
| jq -r '.access_token')

# Use token
curl "https://api.pinboard.ai/api/v1/users/me" \
-H "Authorization: Bearer $TOKEN"

Error Handling

401 Unauthorized

Returned when authentication fails:

{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired token",
"details": null
}
}

Common causes:

  • Missing Authorization header
  • Invalid token format
  • Expired token
  • Token was revoked

Solution: Obtain a new token using the refresh token or re-authenticate.

403 Forbidden

Returned when the user lacks permission:

{
"error": {
"code": "FORBIDDEN",
"message": "You do not have permission to access this resource",
"details": {
"required_permission": "project.tasks.create"
}
}
}

Common causes:

  • User is not a member of the team/project
  • User's role lacks the required permission
  • Resource belongs to a different organization

Best Practices

Token Storage

  • Browser: Store in memory or secure HTTP-only cookies (not localStorage)
  • Server: Store in environment variables or secure secret management
  • Mobile: Use secure storage (Keychain on iOS, Keystore on Android)

Token Refresh Strategy

Implement proactive token refresh:

const TOKEN_REFRESH_BUFFER = 5 * 60 * 1000; // 5 minutes before expiry

function shouldRefreshToken(expiresAt: number): boolean {
return Date.now() >= expiresAt - TOKEN_REFRESH_BUFFER;
}

async function ensureValidToken(): Promise<string> {
if (shouldRefreshToken(tokenExpiresAt)) {
const tokens = await refreshToken(currentRefreshToken);
// Update stored tokens
return tokens.access_token;
}
return currentAccessToken;
}

Error Handling

Always handle authentication errors gracefully:

async function apiRequest(path: string, options?: RequestInit) {
const response = await fetch(`${API_URL}${path}`, {
...options,
headers: {
...options?.headers,
'Authorization': `Bearer ${accessToken}`,
},
});

if (response.status === 401) {
// Try to refresh token
const newTokens = await refreshToken(refreshToken);
// Retry request with new token
return apiRequest(path, options);
}

if (!response.ok) {
const error = await response.json();
throw new ApiError(error);
}

return response.json();
}

Logout

To log out and invalidate tokens:

curl -X POST "https://api.pinboard.ai/api/v1/auth/logout" \
-H "Authorization: Bearer your-access-token"

This will:

  1. Invalidate the current access token
  2. Invalidate the associated refresh token
  3. Clear any server-side session data

Security Considerations

  1. Always use HTTPS - Never send tokens over unencrypted connections
  2. Validate tokens server-side - Don't trust client-provided token claims
  3. Implement token rotation - Refresh tokens should be single-use
  4. Set appropriate expiry times - Balance security with user experience
  5. Log authentication events - Monitor for suspicious activity
  6. Handle token leakage - Have a process to revoke compromised tokens