Authentication Guide
OAuth 2.0 Authentication
This guide covers implementing OAuth 2.0 authentication: authorization flows, token management, scopes, and security best practices.
Overview
Our API supports OAuth 2.0 for secure, delegated access. Choose the appropriate flow based on your application type:
- Authorization Code — Web applications with a backend server
- Authorization Code + PKCE — Mobile apps and single-page applications
- Client Credentials — Server-to-server communication
Quick Reference
Authorization endpoint: https://auth.example.com/oauth/authorize
Token endpoint: https://auth.example.com/oauth/token
Revocation endpoint: https://auth.example.com/oauth/revoke
JWKS endpoint: https://auth.example.com/.well-known/jwks.json
Authorization Code Flow
Best for web applications with a secure backend. This flow keeps your client secret confidential.
Step 1: Redirect to Authorization
Redirect the user to the authorization endpoint:
GET https://auth.example.com/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=read:projects write:projects&
state=random_state_string
Parameters
response_type— Must becodeclient_id— Your application's client IDredirect_uri— URL to receive the authorization code (must match registered URI)scope— Space-separated list of requested permissionsstate— Random string to prevent CSRF attacks (store and verify on callback)
Step 2: User Authorizes
The user sees a consent screen and approves or denies access. On approval, they're redirected to your redirect_uri:
https://yourapp.com/callback?code=AUTH_CODE&state=random_state_string
Step 3: Exchange Code for Tokens
Exchange the authorization code for access and refresh tokens:
curl -X POST https://auth.example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://yourapp.com/callback" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "read:projects write:projects"
}
Authorization Code + PKCE
Required for mobile apps and SPAs where the client secret cannot be kept confidential.
Step 1: Generate Code Verifier and Challenge
// Generate a random code verifier (43-128 characters)
const codeVerifier = generateRandomString(64);
// Create the code challenge (SHA-256 hash, base64url encoded)
const codeChallenge = base64url(sha256(codeVerifier));
Step 2: Authorization Request with Challenge
GET https://auth.example.com/oauth/authorize?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=read:projects&
state=random_state_string&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256
Step 3: Exchange with Verifier
curl -X POST https://auth.example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=AUTH_CODE" \
-d "redirect_uri=https://yourapp.com/callback" \
-d "client_id=YOUR_CLIENT_ID" \
-d "code_verifier=ORIGINAL_CODE_VERIFIER"
Client Credentials Flow
For server-to-server authentication where no user context is needed.
curl -X POST https://auth.example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET" \
-d "scope=admin:read"
Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "admin:read"
}
Note: Client credentials flow does not return a refresh token.
Using Access Tokens
Include the access token in the Authorization header:
curl -X GET https://api.example.com/v1/projects \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..."
Refreshing Tokens
Access tokens expire after 1 hour. Use the refresh token to obtain new tokens without user interaction:
curl -X POST https://auth.example.com/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=YOUR_REFRESH_TOKEN" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Response
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4..."
}
Refresh tokens are rotated on each use. Always store and use the new refresh token from the response.
Scopes
Request only the scopes your application needs:
User Scopes
read:profile— Read user profile informationwrite:profile— Update user profileread:email— Access user's email address
Project Scopes
read:projects— List and view projectswrite:projects— Create and update projectsdelete:projects— Delete projects
Admin Scopes
admin:read— Read organization-wide dataadmin:write— Manage organization settingsadmin:users— Manage organization users
Revoking Tokens
Revoke tokens when a user logs out or disconnects your app:
curl -X POST https://auth.example.com/oauth/revoke \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "token=TOKEN_TO_REVOKE" \
-d "client_id=YOUR_CLIENT_ID" \
-d "client_secret=YOUR_CLIENT_SECRET"
Revoking a refresh token also invalidates all associated access tokens.
Error Responses
OAuth errors follow the RFC 6749 specification:
{
"error": "invalid_grant",
"error_description": "The authorization code has expired"
}
Common Error Codes
invalid_request— Missing or invalid parameterinvalid_client— Client authentication failedinvalid_grant— Authorization code or refresh token is invalid or expiredunauthorized_client— Client not authorized for this grant typeunsupported_grant_type— Grant type not supportedinvalid_scope— Requested scope is invalid or exceeds granted scope
Security Best Practices
For All Applications
- Always use HTTPS — Never send tokens over unencrypted connections
- Validate the state parameter — Compare with stored value to prevent CSRF
- Request minimal scopes — Only ask for permissions you need
- Store tokens securely — Never expose tokens in URLs, logs, or client-side storage
- Handle token expiration — Implement proper refresh logic before tokens expire
For Web Applications
- Store client secrets in environment variables, never in code
- Use HTTP-only, secure cookies for session management
- Implement CSRF protection on your callback endpoint
For Mobile/SPA Applications
- Always use PKCE — never use implicit flow
- Use secure storage (Keychain on iOS, Keystore on Android)
- Consider using a Backend-for-Frontend (BFF) pattern
- Implement certificate pinning for API requests
JWT Token Structure
Access tokens are JWTs containing claims about the user and granted permissions:
// Decoded JWT payload
{
"iss": "https://auth.example.com",
"sub": "user_123",
"aud": "https://api.example.com",
"exp": 1705330200,
"iat": 1705326600,
"scope": "read:projects write:projects",
"client_id": "abc123"
}
Validating Tokens
If validating tokens locally, ensure you:
- Verify the signature using keys from the JWKS endpoint
- Check
expclaim is in the future - Verify
issmatcheshttps://auth.example.com - Verify
audmatches your API identifier
Code Examples
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
// Step 1: Redirect to authorization
app.get('/login', (req, res) => {
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const params = new URLSearchParams({
response_type: 'code',
client_id: process.env.CLIENT_ID,
redirect_uri: 'https://yourapp.com/callback',
scope: 'read:projects write:projects',
state: state
});
res.redirect(`https://auth.example.com/oauth/authorize?${params}`);
});
// Step 2: Handle callback
app.get('/callback', async (req, res) => {
// Verify state
if (req.query.state !== req.session.oauthState) {
return res.status(400).send('State mismatch');
}
// Exchange code for tokens
const response = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: req.query.code,
redirect_uri: 'https://yourapp.com/callback',
client_id: process.env.CLIENT_ID,
client_secret: process.env.CLIENT_SECRET
})
});
const tokens = await response.json();
// Store tokens securely and create session
});
Python (Flask)
import os
import secrets
from urllib.parse import urlencode
from flask import Flask, redirect, request, session
import requests
app = Flask(__name__)
@app.route('/login')
def login():
state = secrets.token_hex(16)
session['oauth_state'] = state
params = {
'response_type': 'code',
'client_id': os.environ['CLIENT_ID'],
'redirect_uri': 'https://yourapp.com/callback',
'scope': 'read:projects write:projects',
'state': state
}
return redirect(f"https://auth.example.com/oauth/authorize?{urlencode(params)}")
@app.route('/callback')
def callback():
if request.args.get('state') != session.get('oauth_state'):
return 'State mismatch', 400
response = requests.post('https://auth.example.com/oauth/token', data={
'grant_type': 'authorization_code',
'code': request.args.get('code'),
'redirect_uri': 'https://yourapp.com/callback',
'client_id': os.environ['CLIENT_ID'],
'client_secret': os.environ['CLIENT_SECRET']
})
tokens = response.json()
# Store tokens securely and create session
Related Samples
This is a sample article to demonstrate how I write.