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 be code
  • client_id — Your application's client ID
  • redirect_uri — URL to receive the authorization code (must match registered URI)
  • scope — Space-separated list of requested permissions
  • state — 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 information
  • write:profile — Update user profile
  • read:email — Access user's email address

Project Scopes

  • read:projects — List and view projects
  • write:projects — Create and update projects
  • delete:projects — Delete projects

Admin Scopes

  • admin:read — Read organization-wide data
  • admin:write — Manage organization settings
  • admin: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 parameter
  • invalid_client — Client authentication failed
  • invalid_grant — Authorization code or refresh token is invalid or expired
  • unauthorized_client — Client not authorized for this grant type
  • unsupported_grant_type — Grant type not supported
  • invalid_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 exp claim is in the future
  • Verify iss matches https://auth.example.com
  • Verify aud matches 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.