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:
- Login via SSO - Users authenticate through Google, Microsoft, or GitHub OAuth
- Token Exchange - Exchange the NextAuth session for an API JWT token
- API Requests - Include the JWT in all subsequent API requests
- 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:
| Claim | Description |
|---|---|
sub | User ID (UUID) |
email | User's email address |
username | User's username |
site_role | Site-wide role (e.g., "admin", "user") |
exp | Token expiration timestamp |
iat | Token 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
Authorizationheader - 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:
- Invalidate the current access token
- Invalidate the associated refresh token
- Clear any server-side session data
Security Considerations
- Always use HTTPS - Never send tokens over unencrypted connections
- Validate tokens server-side - Don't trust client-provided token claims
- Implement token rotation - Refresh tokens should be single-use
- Set appropriate expiry times - Balance security with user experience
- Log authentication events - Monitor for suspicious activity
- Handle token leakage - Have a process to revoke compromised tokens
Related Endpoints
- Exchange Token - Exchange session for JWT
- Refresh Token - Refresh access token
- Logout - Invalidate tokens
- Get Current User - Get authenticated user info