---
title: "OAuth 2.0"
description: "Implement OAuth 2.0 authentication for TraceRTM web applications and third-party integrations"
---

# OAuth 2.0

TraceRTM supports OAuth 2.0 for secure authentication in web applications, mobile apps, and third-party integrations. OAuth provides delegated access without sharing credentials.

## Supported Flows

| Flow | Use Case | Security Level |
|------|----------|----------------|
| **Authorization Code** | Web applications with backend | Highest |
| **Authorization Code + PKCE** | SPAs, mobile apps | Highest |
| **Client Credentials** | Server-to-server | High |
| **Device Authorization** | CLI, IoT devices | High |

## Quick Start

### 1. Create an OAuth Application

```bash
# Via CLI
rtm oauth app create \
  --name "My Web App" \
  --redirect-uris "https://myapp.com/callback" \
  --scopes "read:projects,write:items"
```

Or via the Dashboard:
1. Go to **Settings** → **OAuth Applications**
2. Click **Create Application**
3. Configure redirect URIs and scopes
4. Save your Client ID and Client Secret

### 2. Implement the Authorization Flow

```python
from tracertm.oauth import OAuthClient

oauth = OAuthClient(
    client_id="your_client_id",
    client_secret="your_client_secret",
    redirect_uri="https://myapp.com/callback"
)

# Generate authorization URL
auth_url = oauth.get_authorization_url(
    scopes=["read:projects", "write:items"],
    state="random_state_string"
)
# Redirect user to auth_url

# After user authorizes, exchange code for tokens
tokens = await oauth.exchange_code(
    code="authorization_code_from_callback",
    state="random_state_string"
)

# Use access token
client = TraceRTMClient(access_token=tokens.access_token)
```

## Authorization Code Flow

The most secure flow for web applications with a backend server.

```
┌─────────────────────────────────────────────────────────────────────────┐
│                    Authorization Code Flow                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   User          Your App          TraceRTM Auth        TraceRTM API     │
│     │               │                   │                    │           │
│     │──[1] Login───>│                   │                    │           │
│     │               │──[2] Redirect────>│                    │           │
│     │               │   to /authorize   │                    │           │
│     │<──[3] Login/Consent───────────────│                    │           │
│     │──[4] Approve─>│                   │                    │           │
│     │               │<──[5] Code────────│                    │           │
│     │               │                   │                    │           │
│     │               │──[6] Exchange─────>│                    │           │
│     │               │   code + secret   │                    │           │
│     │               │<──[7] Tokens──────│                    │           │
│     │               │                   │                    │           │
│     │               │──[8] API Request──┼───────────────────>│           │
│     │               │<──[9] Response────┼────────────────────│           │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘
```

### Implementation

#### Step 1: Redirect to Authorization

```python
from urllib.parse import urlencode
import secrets

def get_authorization_url():
    state = secrets.token_urlsafe(32)
    # Store state in session for verification
    session["oauth_state"] = state

    params = {
        "client_id": "your_client_id",
        "redirect_uri": "https://myapp.com/callback",
        "response_type": "code",
        "scope": "read:projects write:items read:links",
        "state": state
    }

    return f"https://auth.tracertm.dev/oauth/authorize?{urlencode(params)}"
```

#### Step 2: Handle Callback

```python
from flask import Flask, request, redirect, session
import httpx

app = Flask(__name__)

@app.route("/callback")
async def oauth_callback():
    # Verify state
    if request.args.get("state") != session.get("oauth_state"):
        return "Invalid state", 400

    # Check for errors
    if "error" in request.args:
        return f"Error: {request.args['error']}", 400

    code = request.args.get("code")

    # Exchange code for tokens
    async with httpx.AsyncClient() as client:
        response = await client.post(
            "https://auth.tracertm.dev/oauth/token",
            data={
                "grant_type": "authorization_code",
                "client_id": "your_client_id",
                "client_secret": "your_client_secret",
                "code": code,
                "redirect_uri": "https://myapp.com/callback"
            }
        )

    tokens = response.json()

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

    return redirect("/dashboard")
```

#### Step 3: Use Access Token

```python
from tracertm import TraceRTMClient

@app.route("/dashboard")
async def dashboard():
    client = TraceRTMClient(
        access_token=session["access_token"]
    )

    projects = await client.projects.list()
    return render_template("dashboard.html", projects=projects)
```

## Authorization Code + PKCE

For single-page applications (SPAs) and mobile apps where client secrets cannot be safely stored.

### PKCE Flow

```python
import hashlib
import base64
import secrets

def generate_pkce():
    """Generate PKCE code verifier and challenge."""
    # Generate random verifier
    code_verifier = secrets.token_urlsafe(64)

    # Create challenge (SHA256 hash, base64url encoded)
    digest = hashlib.sha256(code_verifier.encode()).digest()
    code_challenge = base64.urlsafe_b64encode(digest).decode().rstrip("=")

    return code_verifier, code_challenge
```

### React Implementation

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

const CLIENT_ID = 'your_client_id'
const REDIRECT_URI = 'http://localhost:3000/callback'
const AUTH_URL = 'https://auth.tracertm.dev/oauth/authorize'
const TOKEN_URL = 'https://auth.tracertm.dev/oauth/token'

// Generate PKCE codes
function generatePKCE(): { verifier: string; challenge: string } {
  const verifier = crypto.randomUUID() + crypto.randomUUID()
  const encoder = new TextEncoder()
  const data = encoder.encode(verifier)

  return crypto.subtle.digest('SHA-256', data).then(hash => {
    const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+$/, '')

    return { verifier, challenge }
  })
}

// Start login flow
export async function login() {
  const { verifier, challenge } = await generatePKCE()
  const state = crypto.randomUUID()

  // Store for callback
  sessionStorage.setItem('pkce_verifier', verifier)
  sessionStorage.setItem('oauth_state', state)

  const params = new URLSearchParams({
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    response_type: 'code',
    scope: 'read:projects write:items read:links',
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256'
  })

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

// Handle callback
export async function handleCallback(code: string, state: string) {
  // Verify state
  if (state !== sessionStorage.getItem('oauth_state')) {
    throw new Error('Invalid state')
  }

  const verifier = sessionStorage.getItem('pkce_verifier')

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

  const tokens = await response.json()

  // Store tokens
  sessionStorage.setItem('access_token', tokens.access_token)
  sessionStorage.setItem('refresh_token', tokens.refresh_token)

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

  return tokens
}

// Get authenticated client
export function getClient(): TraceRTMClient {
  const token = sessionStorage.getItem('access_token')
  if (!token) throw new Error('Not authenticated')

  return new TraceRTMClient({ accessToken: token })
}
```

### React Component

```tsx
// src/components/AuthProvider.tsx
import { createContext, useContext, useState, useEffect, ReactNode } from 'react'
import { login, handleCallback, getClient } from '../auth/oauth'
import { TraceRTMClient } from '@tracertm/sdk'

interface AuthContextType {
  isAuthenticated: boolean
  client: TraceRTMClient | null
  login: () => void
  logout: () => void
}

const AuthContext = createContext<AuthContextType | null>(null)

export function AuthProvider({ children }: { children: ReactNode }) {
  const [client, setClient] = useState<TraceRTMClient | null>(null)

  useEffect(() => {
    // Check for existing session
    try {
      setClient(getClient())
    } catch {
      // Not authenticated
    }

    // Handle OAuth callback
    const params = new URLSearchParams(window.location.search)
    const code = params.get('code')
    const state = params.get('state')

    if (code && state) {
      handleCallback(code, state).then(() => {
        setClient(getClient())
        window.history.replaceState({}, '', window.location.pathname)
      })
    }
  }, [])

  const logout = () => {
    sessionStorage.removeItem('access_token')
    sessionStorage.removeItem('refresh_token')
    setClient(null)
  }

  return (
    <AuthContext.Provider value={{
      isAuthenticated: !!client,
      client,
      login,
      logout
    }}>
      {children}
    </AuthContext.Provider>
  )
}

export const useAuth = () => {
  const context = useContext(AuthContext)
  if (!context) throw new Error('useAuth must be used within AuthProvider')
  return context
}
```

## Client Credentials Flow

For server-to-server authentication without user involvement.

```python
from tracertm.oauth import OAuthClient

oauth = OAuthClient(
    client_id="your_client_id",
    client_secret="your_client_secret"
)

# Get access token using client credentials
tokens = await oauth.client_credentials(
    scopes=["read:projects", "write:items"]
)

# Use the token
client = TraceRTMClient(access_token=tokens.access_token)

# Tokens are cached and auto-refreshed
projects = await client.projects.list()
```

### Service Account Setup

```python
# Create service account for automated systems
from tracertm.admin import AdminClient

admin = AdminClient(api_key="rtm_live_admin...")

service_account = await admin.service_accounts.create(
    name="CI/CD Pipeline",
    scopes=["read:projects", "write:items", "write:links"],
    description="Automated test result synchronization"
)

# service_account contains client_id and client_secret
print(f"Client ID: {service_account.client_id}")
print(f"Client Secret: {service_account.client_secret}")
```

## Token Management

### Token Response

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "dGhpcyBpcyBhIHJlZnJlc2g...",
  "scope": "read:projects write:items"
}
```

### Token Refresh

```python
# Automatic refresh with SDK
client = TraceRTMClient(
    access_token=tokens.access_token,
    refresh_token=tokens.refresh_token,
    client_id="your_client_id",
    client_secret="your_client_secret",
    auto_refresh=True  # Default
)

# Manual refresh
new_tokens = await oauth.refresh_token(
    refresh_token=tokens.refresh_token
)
```

### Token Introspection

```python
# Check token validity and scopes
info = await oauth.introspect(access_token)

print(f"Active: {info.active}")
print(f"Scopes: {info.scope}")
print(f"Expires: {info.exp}")
print(f"Client ID: {info.client_id}")
```

### Token Revocation

```python
# Revoke access token
await oauth.revoke(access_token)

# Revoke refresh token (invalidates all related tokens)
await oauth.revoke(refresh_token, token_type_hint="refresh_token")
```

## OAuth Application Management

### List Applications

```python
apps = await client.oauth_apps.list()

for app in apps:
    print(f"{app.name}: {app.client_id}")
    print(f"  Redirect URIs: {app.redirect_uris}")
    print(f"  Scopes: {app.scopes}")
```

### Update Application

```python
await client.oauth_apps.update(
    app_id="app_123",
    name="Updated App Name",
    redirect_uris=[
        "https://myapp.com/callback",
        "https://staging.myapp.com/callback"
    ]
)
```

### Rotate Client Secret

```python
# Rotate secret with grace period
rotation = await client.oauth_apps.rotate_secret(
    app_id="app_123",
    grace_period_hours=24
)

print(f"New secret: {rotation.new_secret}")
print(f"Old secret expires: {rotation.old_secret_expires_at}")
```

## Security Best Practices

### State Parameter

Always use the state parameter to prevent CSRF attacks:

```python
import secrets

state = secrets.token_urlsafe(32)
session["oauth_state"] = state

# Include in authorization URL
auth_url = f"{AUTH_URL}?...&state={state}"

# Verify on callback
if callback_state != session["oauth_state"]:
    raise SecurityError("Invalid state parameter")
```

### Secure Token Storage

```python
# Backend: Encrypt tokens at rest
from cryptography.fernet import Fernet

cipher = Fernet(ENCRYPTION_KEY)

def store_tokens(user_id: str, tokens: dict):
    encrypted = cipher.encrypt(json.dumps(tokens).encode())
    redis.set(f"tokens:{user_id}", encrypted, ex=3600)

def get_tokens(user_id: str) -> dict:
    encrypted = redis.get(f"tokens:{user_id}")
    if encrypted:
        return json.loads(cipher.decrypt(encrypted))
    return None
```

### Minimal Scopes

```python
# Request only needed scopes
auth_url = oauth.get_authorization_url(
    scopes=["read:projects"],  # Not "admin:*"!
)
```

## Error Handling

```python
from tracertm.oauth import OAuthError, InvalidGrantError, ExpiredTokenError

try:
    tokens = await oauth.exchange_code(code)
except InvalidGrantError:
    # Code already used or expired
    return redirect("/login")
except ExpiredTokenError:
    # Try refresh
    tokens = await oauth.refresh_token(refresh_token)
except OAuthError as e:
    logger.error(f"OAuth error: {e.error} - {e.description}")
    return redirect("/login?error=oauth_failed")
```

## Related Documentation

- [OAuth Flows](/docs/api-reference/authentication/oauth-flows) - Detailed flow implementations
- [JWT](/docs/api-reference/authentication/jwt) - JWT token details
- [API Keys](/docs/api-reference/authentication/api-keys) - Alternative authentication
- [Security](/docs/wiki/guides/security) - Security best practices

