---
title: "OAuth Flows"
description: "Detailed implementation guides for all OAuth 2.0 flows supported by TraceRTM"
---

# OAuth Flows

This guide provides detailed implementation examples for each OAuth 2.0 flow supported by TraceRTM, including complete code samples for various platforms.

## Authorization Code Flow (Web Apps)

The recommended flow for traditional web applications with a backend server.

### Flask Implementation

```python
# app.py
from flask import Flask, redirect, request, session, url_for
import httpx
import secrets
import os

app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET_KEY"]

# OAuth configuration
CLIENT_ID = os.environ["TRACERTM_CLIENT_ID"]
CLIENT_SECRET = os.environ["TRACERTM_CLIENT_SECRET"]
AUTH_URL = "https://auth.tracertm.dev/oauth/authorize"
TOKEN_URL = "https://auth.tracertm.dev/oauth/token"
REDIRECT_URI = "http://localhost:5000/callback"

@app.route("/login")
def login():
    """Initiate OAuth login flow."""
    state = secrets.token_urlsafe(32)
    session["oauth_state"] = state

    params = {
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "response_type": "code",
        "scope": "read:projects write:items read:links write:links",
        "state": state
    }

    query = "&".join(f"{k}={v}" for k, v in params.items())
    return redirect(f"{AUTH_URL}?{query}")

@app.route("/callback")
async def callback():
    """Handle OAuth callback."""
    # Verify state to prevent CSRF
    if request.args.get("state") != session.pop("oauth_state", None):
        return "Invalid state parameter", 400

    # Check for errors
    error = request.args.get("error")
    if error:
        error_desc = request.args.get("error_description", "Unknown error")
        return f"OAuth error: {error} - {error_desc}", 400

    # Exchange authorization code for tokens
    code = request.args.get("code")

    async with httpx.AsyncClient() as client:
        response = await client.post(TOKEN_URL, data={
            "grant_type": "authorization_code",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "code": code,
            "redirect_uri": REDIRECT_URI
        })

    if response.status_code != 200:
        return f"Token exchange failed: {response.text}", 400

    tokens = response.json()

    # Store tokens securely
    session["access_token"] = tokens["access_token"]
    session["refresh_token"] = tokens["refresh_token"]
    session["token_expires"] = tokens["expires_in"]

    return redirect(url_for("dashboard"))

@app.route("/dashboard")
async def dashboard():
    """Protected dashboard route."""
    access_token = session.get("access_token")
    if not access_token:
        return redirect(url_for("login"))

    from tracertm import TraceRTMClient

    client = TraceRTMClient(access_token=access_token)

    try:
        projects = await client.projects.list()
        return render_template("dashboard.html", projects=projects)
    except AuthenticationError:
        # Token expired, try refresh
        return redirect(url_for("refresh"))

@app.route("/refresh")
async def refresh():
    """Refresh access token."""
    refresh_token = session.get("refresh_token")
    if not refresh_token:
        return redirect(url_for("login"))

    async with httpx.AsyncClient() as client:
        response = await client.post(TOKEN_URL, data={
            "grant_type": "refresh_token",
            "client_id": CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "refresh_token": refresh_token
        })

    if response.status_code != 200:
        # Refresh failed, re-login required
        session.clear()
        return redirect(url_for("login"))

    tokens = response.json()
    session["access_token"] = tokens["access_token"]
    session["refresh_token"] = tokens.get("refresh_token", refresh_token)

    return redirect(url_for("dashboard"))

@app.route("/logout")
def logout():
    """Logout and revoke tokens."""
    # Revoke tokens
    access_token = session.get("access_token")
    if access_token:
        httpx.post(
            "https://auth.tracertm.dev/oauth/revoke",
            data={"token": access_token}
        )

    session.clear()
    return redirect(url_for("login"))

if __name__ == "__main__":
    app.run(debug=True)
```

### Django Implementation

```python
# views.py
from django.shortcuts import redirect
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django.conf import settings
import httpx
import secrets

def oauth_login(request):
    """Initiate OAuth login."""
    state = secrets.token_urlsafe(32)
    request.session["oauth_state"] = state

    params = {
        "client_id": settings.TRACERTM_CLIENT_ID,
        "redirect_uri": settings.TRACERTM_REDIRECT_URI,
        "response_type": "code",
        "scope": "read:projects write:items",
        "state": state
    }

    url = f"{settings.TRACERTM_AUTH_URL}?{'&'.join(f'{k}={v}' for k, v in params.items())}"
    return redirect(url)

async def oauth_callback(request):
    """Handle OAuth callback."""
    # Verify state
    expected_state = request.session.pop("oauth_state", None)
    actual_state = request.GET.get("state")

    if expected_state != actual_state:
        return HttpResponse("Invalid state", status=400)

    code = request.GET.get("code")
    error = request.GET.get("error")

    if error:
        return HttpResponse(f"OAuth error: {error}", status=400)

    # Exchange code for tokens
    async with httpx.AsyncClient() as client:
        response = await client.post(settings.TRACERTM_TOKEN_URL, data={
            "grant_type": "authorization_code",
            "client_id": settings.TRACERTM_CLIENT_ID,
            "client_secret": settings.TRACERTM_CLIENT_SECRET,
            "code": code,
            "redirect_uri": settings.TRACERTM_REDIRECT_URI
        })

    tokens = response.json()

    # Store in session
    request.session["tracertm_access_token"] = tokens["access_token"]
    request.session["tracertm_refresh_token"] = tokens["refresh_token"]

    return redirect("dashboard")
```

## Authorization Code + PKCE (SPAs)

For single-page applications where client secrets cannot be stored securely.

### Complete React Application

```typescript
// src/lib/auth.ts
import { TraceRTMClient } from '@tracertm/sdk'

const CONFIG = {
  clientId: import.meta.env.VITE_TRACERTM_CLIENT_ID,
  redirectUri: `${window.location.origin}/callback`,
  authUrl: 'https://auth.tracertm.dev/oauth/authorize',
  tokenUrl: 'https://auth.tracertm.dev/oauth/token',
  scopes: ['read:projects', 'write:items', 'read:links']
}

// PKCE helpers
async function generateCodeVerifier(): Promise<string> {
  const array = new Uint8Array(32)
  crypto.getRandomValues(array)
  return btoa(String.fromCharCode(...array))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '')
}

async function generateCodeChallenge(verifier: string): Promise<string> {
  const encoder = new TextEncoder()
  const data = encoder.encode(verifier)
  const hash = await crypto.subtle.digest('SHA-256', data)
  return btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '')
}

export interface TokenResponse {
  access_token: string
  refresh_token: string
  expires_in: number
  token_type: string
  scope: string
}

export class AuthManager {
  private accessToken: string | null = null
  private refreshToken: string | null = null
  private expiresAt: number = 0
  private refreshTimer: NodeJS.Timeout | null = null

  constructor() {
    this.loadFromStorage()
  }

  private loadFromStorage() {
    this.accessToken = sessionStorage.getItem('access_token')
    this.refreshToken = sessionStorage.getItem('refresh_token')
    const expiresAt = sessionStorage.getItem('expires_at')
    this.expiresAt = expiresAt ? parseInt(expiresAt, 10) : 0

    if (this.accessToken && this.expiresAt > Date.now()) {
      this.scheduleRefresh()
    }
  }

  private saveToStorage(tokens: TokenResponse) {
    this.accessToken = tokens.access_token
    this.refreshToken = tokens.refresh_token
    this.expiresAt = Date.now() + (tokens.expires_in * 1000)

    sessionStorage.setItem('access_token', tokens.access_token)
    sessionStorage.setItem('refresh_token', tokens.refresh_token)
    sessionStorage.setItem('expires_at', this.expiresAt.toString())

    this.scheduleRefresh()
  }

  private scheduleRefresh() {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer)
    }

    // Refresh 5 minutes before expiry
    const refreshIn = this.expiresAt - Date.now() - (5 * 60 * 1000)

    if (refreshIn > 0) {
      this.refreshTimer = setTimeout(() => this.refresh(), refreshIn)
    }
  }

  async login(): Promise<void> {
    const verifier = await generateCodeVerifier()
    const challenge = await generateCodeChallenge(verifier)
    const state = crypto.randomUUID()

    sessionStorage.setItem('pkce_verifier', verifier)
    sessionStorage.setItem('oauth_state', state)

    const params = new URLSearchParams({
      client_id: CONFIG.clientId,
      redirect_uri: CONFIG.redirectUri,
      response_type: 'code',
      scope: CONFIG.scopes.join(' '),
      state,
      code_challenge: challenge,
      code_challenge_method: 'S256'
    })

    window.location.href = `${CONFIG.authUrl}?${params}`
  }

  async handleCallback(searchParams: URLSearchParams): Promise<void> {
    const code = searchParams.get('code')
    const state = searchParams.get('state')
    const error = searchParams.get('error')

    if (error) {
      const description = searchParams.get('error_description') || 'Unknown error'
      throw new Error(`OAuth error: ${error} - ${description}`)
    }

    if (!code || !state) {
      throw new Error('Missing code or state parameter')
    }

    const expectedState = sessionStorage.getItem('oauth_state')
    const verifier = sessionStorage.getItem('pkce_verifier')

    if (state !== expectedState) {
      throw new Error('Invalid state parameter - possible CSRF attack')
    }

    if (!verifier) {
      throw new Error('Missing PKCE verifier')
    }

    // Exchange code for tokens
    const response = await fetch(CONFIG.tokenUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        client_id: CONFIG.clientId,
        code,
        redirect_uri: CONFIG.redirectUri,
        code_verifier: verifier
      })
    })

    if (!response.ok) {
      const error = await response.json()
      throw new Error(`Token exchange failed: ${error.error_description || error.error}`)
    }

    const tokens: TokenResponse = await response.json()
    this.saveToStorage(tokens)

    // Clean up
    sessionStorage.removeItem('pkce_verifier')
    sessionStorage.removeItem('oauth_state')
  }

  async refresh(): Promise<void> {
    if (!this.refreshToken) {
      throw new Error('No refresh token available')
    }

    const response = await fetch(CONFIG.tokenUrl, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: CONFIG.clientId,
        refresh_token: this.refreshToken
      })
    })

    if (!response.ok) {
      // Refresh failed, clear tokens
      this.logout()
      throw new Error('Token refresh failed')
    }

    const tokens: TokenResponse = await response.json()
    this.saveToStorage(tokens)
  }

  logout(): void {
    this.accessToken = null
    this.refreshToken = null
    this.expiresAt = 0

    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer)
    }

    sessionStorage.removeItem('access_token')
    sessionStorage.removeItem('refresh_token')
    sessionStorage.removeItem('expires_at')
  }

  getClient(): TraceRTMClient | null {
    if (!this.accessToken || this.expiresAt < Date.now()) {
      return null
    }

    return new TraceRTMClient({
      accessToken: this.accessToken
    })
  }

  isAuthenticated(): boolean {
    return !!this.accessToken && this.expiresAt > Date.now()
  }
}

export const auth = new AuthManager()
```

```tsx
// src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { auth } from './lib/auth'

function ProtectedRoute({ children }: { children: React.ReactNode }) {
  if (!auth.isAuthenticated()) {
    return <Navigate to="/login" replace />
  }
  return <>{children}</>
}

function Login() {
  return (
    <div className="login">
      <h1>Welcome to TraceRTM</h1>
      <button onClick={() => auth.login()}>
        Login with TraceRTM
      </button>
    </div>
  )
}

function Callback() {
  const [error, setError] = useState<string | null>(null)

  useEffect(() => {
    const params = new URLSearchParams(window.location.search)

    auth.handleCallback(params)
      .then(() => {
        window.location.href = '/dashboard'
      })
      .catch(err => {
        setError(err.message)
      })
  }, [])

  if (error) {
    return <div className="error">{error}</div>
  }

  return <div>Authenticating...</div>
}

function Dashboard() {
  const [projects, setProjects] = useState([])

  useEffect(() => {
    const client = auth.getClient()
    if (client) {
      client.projects.list().then(setProjects)
    }
  }, [])

  return (
    <div>
      <h1>Dashboard</h1>
      <ul>
        {projects.map(p => (
          <li key={p.id}>{p.name}</li>
        ))}
      </ul>
    </div>
  )
}

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/login" element={<Login />} />
        <Route path="/callback" element={<Callback />} />
        <Route path="/dashboard" element={
          <ProtectedRoute>
            <Dashboard />
          </ProtectedRoute>
        } />
        <Route path="/" element={<Navigate to="/dashboard" replace />} />
      </Routes>
    </BrowserRouter>
  )
}
```

## Client Credentials Flow (Server-to-Server)

For automated systems and service accounts.

```python
# server_to_server.py
import httpx
from datetime import datetime, timedelta
import asyncio

class ServiceClient:
    """TraceRTM client using client credentials flow."""

    def __init__(self, client_id: str, client_secret: str):
        self.client_id = client_id
        self.client_secret = client_secret
        self.token_url = "https://auth.tracertm.dev/oauth/token"
        self.access_token: str | None = None
        self.token_expires: datetime | None = None

    async def get_token(self) -> str:
        """Get valid access token, refreshing if necessary."""
        if self.access_token and self.token_expires and datetime.utcnow() < self.token_expires:
            return self.access_token

        async with httpx.AsyncClient() as client:
            response = await client.post(
                self.token_url,
                data={
                    "grant_type": "client_credentials",
                    "client_id": self.client_id,
                    "client_secret": self.client_secret,
                    "scope": "read:projects write:items read:links write:links"
                }
            )
            response.raise_for_status()

        tokens = response.json()
        self.access_token = tokens["access_token"]
        self.token_expires = datetime.utcnow() + timedelta(seconds=tokens["expires_in"] - 60)

        return self.access_token

    async def request(self, method: str, path: str, **kwargs) -> dict:
        """Make authenticated API request."""
        token = await self.get_token()

        async with httpx.AsyncClient() as client:
            response = await client.request(
                method,
                f"https://api.tracertm.dev{path}",
                headers={"Authorization": f"Bearer {token}"},
                **kwargs
            )
            response.raise_for_status()

        return response.json()

# Usage
async def main():
    service = ServiceClient(
        client_id="service_xxx",
        client_secret="secret_xxx"
    )

    # List projects
    projects = await service.request("GET", "/v1/projects")
    print(f"Found {len(projects)} projects")

    # Create item
    item = await service.request(
        "POST",
        "/v1/projects/proj_123/items",
        json={
            "title": "Automated Test Result",
            "type": "TEST",
            "metadata": {"automated": True}
        }
    )
    print(f"Created item: {item['id']}")

asyncio.run(main())
```

## Device Authorization Flow

For CLI tools and devices without browsers.

```python
# device_flow.py
import httpx
import time
import asyncio

async def device_login():
    """Perform device authorization flow."""
    client_id = "your_client_id"
    device_url = "https://auth.tracertm.dev/oauth/device"
    token_url = "https://auth.tracertm.dev/oauth/token"

    # Step 1: Request device code
    async with httpx.AsyncClient() as client:
        response = await client.post(device_url, data={
            "client_id": client_id,
            "scope": "read:projects write:items"
        })
        response.raise_for_status()

    data = response.json()
    device_code = data["device_code"]
    user_code = data["user_code"]
    verification_uri = data["verification_uri"]
    interval = data.get("interval", 5)
    expires_in = data["expires_in"]

    # Step 2: Display instructions to user
    print("\n" + "=" * 50)
    print("To authorize this device, visit:")
    print(f"  {verification_uri}")
    print(f"\nAnd enter the code: {user_code}")
    print("=" * 50 + "\n")

    # Step 3: Poll for token
    deadline = time.time() + expires_in

    while time.time() < deadline:
        await asyncio.sleep(interval)

        async with httpx.AsyncClient() as client:
            response = await client.post(token_url, data={
                "grant_type": "urn:ietf:params:oauth:grant-type:device_code",
                "client_id": client_id,
                "device_code": device_code
            })

        result = response.json()

        if response.status_code == 200:
            # Success!
            print("Authorization successful!")
            return result

        error = result.get("error")
        if error == "authorization_pending":
            print("Waiting for authorization...")
            continue
        elif error == "slow_down":
            interval += 5
            continue
        elif error == "expired_token":
            raise Exception("Device code expired. Please try again.")
        elif error == "access_denied":
            raise Exception("Authorization denied by user.")
        else:
            raise Exception(f"Error: {result.get('error_description', error)}")

    raise Exception("Authorization timed out")

# CLI usage
if __name__ == "__main__":
    tokens = asyncio.run(device_login())

    # Save tokens
    with open(".tracertm_tokens", "w") as f:
        json.dump(tokens, f)

    print(f"Access token saved. Expires in {tokens['expires_in']} seconds.")
```

## Implicit Flow (Deprecated)

> **Warning**: The implicit flow is deprecated and not recommended for new applications. Use Authorization Code + PKCE instead.

```typescript
// Legacy implicit flow - DO NOT USE for new apps
function legacyImplicitLogin() {
  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: 'token',  // Returns token directly in URL
    scope: 'read:projects'
  })

  window.location.href = `${AUTH_URL}?${params}`
}

// Token is in URL fragment after redirect
function handleImplicitCallback() {
  const hash = window.location.hash.substring(1)
  const params = new URLSearchParams(hash)
  const accessToken = params.get('access_token')
  // WARNING: Token exposed in URL, browser history, logs
}
```

## Related Documentation

- [OAuth 2.0](/docs/api-reference/authentication/oauth) - OAuth overview
- [JWT](/docs/api-reference/authentication/jwt) - JWT token details
- [Security Best Practices](/docs/wiki/guides/security) - Security guidelines

