Skip to content

Bearer Token Authentication

Stateless JWT authentication for modern applications

Bearer token authentication in AgentUp provides JWT (JSON Web Token) validation for stateless, secure authentication. This guide covers setup, configuration, and best practices for implementing bearer token authentication.

Overview

Bearer token authentication in AgentUp provides comprehensive JWT-based authentication with AgentUp Security Framework integration:

  • JWT Support - RFC 7519 compliant JSON Web Token validation with full cryptographic verification
  • Stateless Authentication - No server-side session storage required for scalable deployments
  • Scope-Based Authorization - Hierarchical permission system with inherited scope management
  • Unified Security Integration - Context-aware middleware with plugin classification support
  • Enhanced Endpoint Protection - Comprehensive coverage including previously vulnerable MCP endpoints
  • Custom Claims - Extract user information and permissions from tokens for fine-grained access control
  • A2A Compliance - Proper Bearer token security scheme advertising in agent discovery

When to Use Bearer Tokens

Good For Consider OAuth2 Instead
Custom JWT systems Enterprise OAuth2 providers
Stateless applications Complex authorization flows
Microservices auth Third-party integrations
Simple JWT validation Token revocation requirements
Development/testing Multi-tenant systems

Unified Security Integration

Bearer token authentication is fully integrated with AgentUp's AgentUp Security Framework, providing enhanced protection.

Middleware Integration

Bearer token authentication works with context-aware middleware:

# Context-aware middleware configuration
middleware:
  - name: rate_limited
    params: {}
  - name: cached
    params:
      # Authentication-aware caching
      auth_aware_caching: true
      user_specific_cache: true

Quick Setup

Step 1: Generate or Obtain a JWT Token

# For testing, create a simple JWT token
python3 -c "
import time, json
from authlib.jose import jwt

# Create test payload
payload = {
    'sub': 'user123',
    'iss': 'your-app',
    'aud': 'your-agent',
    'iat': int(time.time()),
    'exp': int(time.time()) + 3600,  # 1 hour
    'name': 'Test User',
    'email': 'user@example.com'
}

# Generate token (use your own secret in production)
token = jwt.encode({'alg': 'HS256', 'typ': 'JWT'}, payload, 'your-secret-key')
print('Test JWT Token:')
print(token)
"

Step 2: Configure Your Agent

Add to your agentup.yml:

security:
  enabled: true
  type: "bearer"
  bearer_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Step 3: Test Authentication

# Start your agent
uv run uvicorn src.agent.main:app --reload --port 8000

# Test with bearer token
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
     http://localhost:8000/agent/card

# Should return 200 OK with agent card data

JWT Token Validation

Current Implementation

AgentUp's bearer token authenticator currently performs:

  1. Format Validation - Ensures proper "Bearer" prefix
  2. String Comparison - Secure constant-time comparison with configured token
  3. Basic JWT Structure - Validates JWT format (header.payload.signature)

Note: Full JWT cryptographic validation (signature verification, claims validation) is available in the OAuth2 authenticator. Consider using OAuth2 for production JWT validation.

Token Format Requirements

Valid JWT tokens must: - Be properly formatted (header.payload.signature) - Include standard claims (iss, aud, exp, etc.) - Use supported algorithms (HS256, RS256, etc.) - Be base64-encoded without padding issues - Include scope claims for authorization (recommended)

Scope-Based Authorization

Bearer token authentication integrates with AgentUp's hierarchical scope system to provide fine-grained access control. Scopes define what operations users can perform and automatically inherit from parent scopes.

Scope Hierarchy

The Agentup Security Framework system supports hierarchical scopes with automatic inheritance:

# Example scope hierarchy in agentup.yml
security:
  enabled: true
  type: "bearer"
  bearer:
    jwt_secret: "${JWT_SECRET}"
    algorithm: "HS256"
    scope_hierarchy:
      # Administrative scopes
      admin: ["*"]                          # Full access
      api:admin: ["api:write", "api:read"]

      # Functional scopes
      api:write: ["api:read"]               # Write includes read
      files:write: ["files:read"]

      # Base scopes
      api:read: []                          # Base permission
      files:read: []

JWT Token with Scopes

Include scopes in your JWT token payload:

# Generate JWT with scopes
payload = {
    "sub": "user123",
    "iss": "your-app",
    "aud": "your-agent",
    "iat": int(time.time()),
    "exp": int(time.time()) + 3600,

    # Scope-based authorization
    "scopes": ["api:write", "files:read"],

    # Additional claims
    "name": "John Doe",
    "email": "john@example.com"
}

token = jwt.encode(header, payload, secret)

Automatic Scope Resolution

The system automatically resolves inherited permissions:

# User has "api:write" scope
# Automatically grants: ["api:write", "api:read"]

# Plugin requires "api:read" scope
# Request succeeds because api:write inherits api:read

Plugin-Specific Scopes

Different plugin types can require specific scopes:

# Plugin configuration with scope requirements
plugins:
  - plugin_id: document_processor
    plugin_type: "hybrid"
    required_scopes: ["files:write", "api:read"]

  - plugin_id: ai_agent
    plugin_type: "ai_function"
    required_scopes: ["api:read"]

  - plugin_id: system_monitor
    plugin_type: "network"
    required_scopes: ["system:read", "api:read"]

Scope Validation in Practice

The system automatically validates scopes for each request:

  1. Token Extraction - JWT token extracted from Authorization header
  2. Scope Resolution - User scopes resolved with inheritance
  3. Plugin Classification - Target plugin type identified
  4. Requirement Matching - Required scopes checked against user scopes
  5. Context Enhancement - Request context enhanced with security information

Configuration Options

Simple Configuration

# Basic bearer token setup
security:
  enabled: true
  type: "bearer"
  bearer_token: "your-jwt-token-here"

Advanced Configuration with Unified Security

# Full bearer token configuration with AgentUp Security Framework
security:
  enabled: true
  type: "bearer"
  bearer:
    jwt_secret: "your-secret-key"
    algorithm: "HS256"
    issuer: "your-app"
    audience: "your-agent"

    # Scope hierarchy for authorization
    scope_hierarchy:
      admin: ["*"]
      api:write: ["api:read"]
      api:read: []
      files:write: ["files:read"]
      files:read: []
      system:read: []

    # Optional: Custom scope claim name in JWT
    scope_claim: "scopes"  # Default: "scopes"

    # Optional: User ID claim name in JWT
    user_claim: "sub"      # Default: "sub"

Plugin Classification Integration

# Bearer authentication with plugin classification
security:
  enabled: true
  type: "bearer"
  bearer:
    jwt_secret: "${JWT_SECRET}"
    algorithm: "HS256"
    scope_hierarchy:
      admin: ["*"]
      api:admin: ["api:write", "api:read"]
      api:write: ["api:read"]
      files:admin: ["files:write", "files:read"]
      files:write: ["files:read"]
      ai:execute: ["api:read"]
      network:admin: ["system:read", "api:read"]
      api:read: []
      files:read: []
      system:read: []

# Plugin configuration with scope requirements
plugins:
  - plugin_id: local_processor
    plugin_type: "local"
    required_scopes: ["api:read"]

  - plugin_id: network_api
    plugin_type: "network"
    required_scopes: ["network:admin"]

  - plugin_id: ai_agent
    plugin_type: "ai_function"
    required_scopes: ["ai:execute"]

  - plugin_id: file_manager
    plugin_type: "hybrid"
    required_scopes: ["files:write"]

Environment Variables

# Using environment variables (recommended)
security:
  enabled: true
  type: "bearer"
  bearer_token: "${JWT_TOKEN}"
# .env file
JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Custom JWT Claims

Standard Claims

JWT tokens should include standard claims:

{
  "sub": "user123",           // Subject (user ID)
  "iss": "your-app",         // Issuer
  "aud": "your-agent",       // Audience
  "iat": 1640995200,         // Issued at
  "exp": 1640998800,         // Expires at
  "nbf": 1640995200          // Not before
}

Custom Claims with Scopes

Add application-specific claims including scope-based authorization:

{
  "sub": "user123",
  "iss": "your-app",
  "aud": "your-agent",
  "iat": 1640995200,
  "exp": 1640998800,

  // Scope-based authorization (AgentUp Security Framework)
  "scopes": ["api:write", "files:read", "system:read"],

  // Legacy role-based claims (optional)
  "roles": ["user", "admin"],
  "permissions": ["read", "write"],

  // Additional custom claims
  "name": "John Doe",
  "email": "john@example.com",
  "tenant_id": "org123",
  "department": "engineering"
}

Scope Claim Examples

Different scope configurations for various user types:

// Admin user
{
  "sub": "admin123",
  "scopes": ["admin"],  // Inherits all permissions
  "name": "System Administrator"
}

// API developer
{
  "sub": "dev456",
  "scopes": ["api:write", "files:read"],
  "name": "API Developer"
}

// Read-only user
{
  "sub": "viewer789",
  "scopes": ["api:read", "files:read"],
  "name": "Read Only User"
}

// AI service account
{
  "sub": "ai-service",
  "scopes": ["ai:execute", "api:read"],
  "name": "AI Service Account"
}

Generating Custom Tokens

Note, the following scripts are for validation and development, tokens should be generated in secure vault, preferably with a hardware backend.

#!/usr/bin/env python3
# generate-jwt.py
import time
from authlib.jose import jwt

def generate_custom_jwt(user_id, name, email, scopes=None, roles=None, secret="your-secret"):
    """Generate a custom JWT token with scope-based authorization."""

    # Default scopes based on roles (for backward compatibility)
    if not scopes and roles:
        if "admin" in roles:
            scopes = ["admin"]
        elif "developer" in roles:
            scopes = ["api:write", "files:read"]
        else:
            scopes = ["api:read"]
    elif not scopes:
        scopes = ["api:read"]  # Default minimal access

    payload = {
        # Standard claims
        "sub": user_id,
        "iss": "your-app",
        "aud": "your-agent",
        "iat": int(time.time()),
        "exp": int(time.time()) + 3600,  # 1 hour

        # Scope-based authorization (AgentUp Security Framework)
        "scopes": scopes,

        # Additional claims
        "name": name,
        "email": email,

        # Legacy claims (optional)
        "roles": roles or ["user"],
    }

    token = jwt.encode(
        header={"alg": "HS256", "typ": "JWT"},
        payload=payload,
        key=secret
    )

    return token.decode('utf-8') if isinstance(token, bytes) else token

# Example usage
if __name__ == "__main__":
    # Generate tokens with scope-based authorization
    admin_token = generate_custom_jwt(
        user_id="admin123",
        name="Admin User",
        email="admin@example.com",
        scopes=["admin"]  # Full access
    )

    developer_token = generate_custom_jwt(
        user_id="dev456",
        name="API Developer",
        email="dev@example.com",
        scopes=["api:write", "files:read"]
    )

    readonly_token = generate_custom_jwt(
        user_id="user789",
        name="Read Only User",
        email="user@example.com",
        scopes=["api:read", "files:read"]
    )

    ai_service_token = generate_custom_jwt(
        user_id="ai-service",
        name="AI Service Account",
        email="ai@example.com",
        scopes=["ai:execute", "api:read"]
    )

    print("Admin Token (full access):")
    print(admin_token)
    print("\nDeveloper Token (api:write, files:read):")
    print(developer_token)
    print("\nRead-only Token (api:read, files:read):")
    print(readonly_token)
    print("\nAI Service Token (ai:execute, api:read):")
    print(ai_service_token)

Environment Variables

Secure Token Management

# agentup.yml (safe to commit)
security:
  enabled: true
  type: "bearer"
  bearer_token: "${JWT_TOKEN}"
  bearer:
    jwt_secret: "${JWT_SECRET:default-dev-secret}"
    jwt_issuer: "${JWT_ISSUER:your-app}"
    jwt_audience: "${JWT_AUDIENCE:your-agent}"
# .env file (DO NOT commit)
JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT_SECRET=your-production-secret-key
JWT_ISSUER=https://your-app.com
JWT_AUDIENCE=your-agent-id

Multiple Environment Setup

# development.env
JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.dev-token...
JWT_SECRET=dev-secret-key
JWT_ISSUER=localhost

# production.env
JWT_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.prod-token...
JWT_SECRET=super-secure-production-secret
JWT_ISSUER=https://your-production-app.com

Testing and Validation

Bearer Token Test Script

#!/bin/bash
# test-bearer-auth.sh - Test bearer token authentication

AGENT_URL="http://localhost:8000"
JWT_TOKEN="$1"

if [ -z "$JWT_TOKEN" ]; then
    echo "Usage: $0 <jwt_token>"
    echo "Example: $0 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    exit 1
fi

echo "Testing Bearer Token Authentication"
echo "====================================="

# Test 1: Discovery endpoint (should work without auth)
echo "1. Testing discovery endpoint (no auth required)..."
DISCOVERY_STATUS=$(curl -s -w "%{http_code}" "${AGENT_URL}/.well-known/agent.json" -o /dev/null)
if [ "$DISCOVERY_STATUS" = "200" ]; then
    echo "   Discovery endpoint accessible"
else
    echo "   Discovery endpoint failed ($DISCOVERY_STATUS)"
    exit 1
fi

# Test 2: Protected endpoint without token (should fail)
echo "2. Testing protected endpoint without bearer token..."
NO_AUTH_STATUS=$(curl -s -w "%{http_code}" "${AGENT_URL}/agent/card" -o /dev/null)
if [ "$NO_AUTH_STATUS" = "401" ]; then
    echo "   Protected endpoint correctly requires authentication"
else
    echo "   Protected endpoint should require authentication ($NO_AUTH_STATUS)"
fi

# Test 3: Protected endpoint with malformed token (should fail)
echo "3. Testing protected endpoint with malformed token..."
BAD_TOKEN_STATUS=$(curl -s -w "%{http_code}" -H "Authorization: Bearer invalid-token" "${AGENT_URL}/agent/card" -o /dev/null)
if [ "$BAD_TOKEN_STATUS" = "401" ]; then
    echo "   Malformed token correctly rejected"
else
    echo "   Malformed token should be rejected ($BAD_TOKEN_STATUS)"
fi

# Test 4: Protected endpoint without "Bearer" prefix (should fail)
echo "4. Testing protected endpoint without Bearer prefix..."
NO_BEARER_STATUS=$(curl -s -w "%{http_code}" -H "Authorization: ${JWT_TOKEN}" "${AGENT_URL}/agent/card" -o /dev/null)
if [ "$NO_BEARER_STATUS" = "401" ]; then
    echo "   Missing Bearer prefix correctly rejected"
else
    echo "   Missing Bearer prefix should be rejected ($NO_BEARER_STATUS)"
fi

# Test 5: Protected endpoint with valid token (should work)
echo "5. Testing protected endpoint with valid bearer token..."
RESPONSE=$(curl -s -w "\n%{http_code}" -H "Authorization: Bearer ${JWT_TOKEN}" "${AGENT_URL}/agent/card")
STATUS=$(echo "$RESPONSE" | tail -1)
BODY=$(echo "$RESPONSE" | head -n -1)

if [ "$STATUS" = "200" ]; then
    echo "   Valid bearer token accepted"
    AGENT_NAME=$(echo "$BODY" | python -c "import sys, json; print(json.load(sys.stdin).get('name', 'Unknown Agent'))")
    echo "   📝 Connected to: $AGENT_NAME"

    # Check security scheme in response
    SECURITY_SCHEME=$(echo "$BODY" | python -c "
import sys, json
data = json.load(sys.stdin)
schemes = data.get('securitySchemes', {})
if 'BearerAuth' in schemes:
    print('Bearer token security scheme correctly advertised')
else:
    print('Warning: Bearer token security scheme not found')
")
    echo "   $SECURITY_SCHEME"
else
    echo "   Valid bearer token rejected ($STATUS)"
    echo "   📝 Error: $BODY"
fi

echo ""
echo "Bearer token testing completed!"

JWT Token Decoder

#!/usr/bin/env python3
# decode-jwt.py - Decode and validate JWT tokens
import json
import sys
import base64
from authlib.jose import jwt

def decode_jwt_header(token):
    """Decode JWT header without validation."""
    try:
        header_b64 = token.split('.')[0]
        # Add padding if needed
        header_b64 += '=' * (4 - len(header_b64) % 4)
        header_json = base64.urlsafe_b64decode(header_b64)
        return json.loads(header_json)
    except Exception as e:
        return {"error": str(e)}

def decode_jwt_payload(token):
    """Decode JWT payload without validation."""
    try:
        payload_b64 = token.split('.')[1]
        # Add padding if needed
        payload_b64 += '=' * (4 - len(payload_b64) % 4)
        payload_json = base64.urlsafe_b64decode(payload_b64)
        return json.loads(payload_json)
    except Exception as e:
        return {"error": str(e)}

def validate_jwt_token(token, secret=None):
    """Validate JWT token with secret."""
    if not secret:
        return {"error": "Secret required for validation"}

    try:
        claims = jwt.decode(token, secret)
        return {"valid": True, "claims": claims}
    except Exception as e:
        return {"valid": False, "error": str(e)}

def main():
    if len(sys.argv) < 2:
        print("Usage: python decode-jwt.py <jwt_token> [secret]")
        print("Example: python decode-jwt.py eyJhbGciOiJIUzI1NiIs... optional-secret")
        sys.exit(1)

    token = sys.argv[1]
    secret = sys.argv[2] if len(sys.argv) > 2 else None

    print("JWT Token Analysis")
    print("=" * 50)

    # Decode header
    header = decode_jwt_header(token)
    print("Header:")
    print(json.dumps(header, indent=2))

    # Decode payload
    payload = decode_jwt_payload(token)
    print("\nPayload:")
    print(json.dumps(payload, indent=2))

    # Validate if secret provided
    if secret:
        validation = validate_jwt_token(token, secret)
        print(f"\nValidation:")
        if validation.get("valid"):
            print("Token is valid")
        else:
            print(f"Token invalid: {validation.get('error')}")
    else:
        print("\nValidation: Skipped (no secret provided)")

    # Check expiration
    if "exp" in payload and not isinstance(payload.get("exp"), str):
        import time
        exp_time = payload["exp"]
        current_time = int(time.time())
        if exp_time > current_time:
            remaining = exp_time - current_time
            print(f"Token expires in {remaining} seconds")
        else:
            print(f"Token expired {current_time - exp_time} seconds ago")

if __name__ == "__main__":
    main()

Integration Testing

# test_bearer_auth.py
import pytest
import time
from authlib.jose import jwt
from fastapi.testclient import TestClient

@pytest.fixture
def test_jwt_token():
    """Generate test JWT token."""
    payload = {
        "sub": "test-user",
        "iss": "test-app",
        "aud": "test-agent",
        "iat": int(time.time()),
        "exp": int(time.time()) + 3600,
        "name": "Test User",
        "email": "test@example.com"
    }

    token = jwt.encode(
        header={"alg": "HS256", "typ": "JWT"},
        payload=payload,
        key="test-secret"
    )

    return token.decode('utf-8') if isinstance(token, bytes) else token

@pytest.fixture
def agent_app():
    """Create test agent with bearer auth."""
    from src.agent.main import app
    return app

@pytest.fixture
def client(agent_app):
    """Create test client."""
    return TestClient(agent_app)

def test_bearer_token_authentication(client, test_jwt_token):
    """Test bearer token authentication flow."""

    # Test 1: Discovery endpoint (no auth required)
    response = client.get("/.well-known/agent.json")
    assert response.status_code == 200

    # Test 2: Protected endpoint without token
    response = client.get("/agent/card")
    assert response.status_code == 401

    # Test 3: Protected endpoint with malformed authorization
    response = client.get(
        "/agent/card",
        headers={"Authorization": "Invalid token"}
    )
    assert response.status_code == 401

    # Test 4: Protected endpoint without Bearer prefix
    response = client.get(
        "/agent/card",
        headers={"Authorization": test_jwt_token}
    )
    assert response.status_code == 401

    # Test 5: Protected endpoint with valid bearer token
    response = client.get(
        "/agent/card",
        headers={"Authorization": f"Bearer {test_jwt_token}"}
    )
    assert response.status_code == 200

    # Verify security scheme in response
    agent_card = response.json()
    assert "securitySchemes" in agent_card
    assert "BearerAuth" in agent_card["securitySchemes"]
    assert agent_card["securitySchemes"]["BearerAuth"]["type"] == "http"
    assert agent_card["securitySchemes"]["BearerAuth"]["scheme"] == "bearer"

Security Considerations

Token Security

Best Practices

  • Use HTTPS only - Never transmit JWT tokens over HTTP
  • Short expiration times - Limit token lifetime (1-24 hours)
  • Secure storage - Store tokens securely on client side
  • Secret protection - Keep JWT secrets secure and rotate regularly
  • Algorithm specification - Always specify allowed algorithms

Common Vulnerabilities

  • None algorithm attack - Always validate algorithms are present
  • Weak secrets - Use strong, random secrets (256+ bits)
  • Token in URLs - Never put tokens in query parameters or URLs
  • XSS exposure - Protect against cross-site scripting
  • Replay attacks - Use short expiration and secure transmission

AgentUp Security Features

  • Format validation - Proper Bearer token format checking
  • Constant-time comparison - Protection against timing attacks
  • No token logging - Tokens are never logged or exposed
  • Secure error messages - Generic "Unauthorized" responses
  • Input sanitization - Comprehensive input validation

Migration and Upgrading

From API Key to Bearer Token

# Before: API key authentication
security:
  enabled: true
  type: "api_key"
  api_key: "sk-your-api-key"

# After: Bearer token authentication
security:
  enabled: true
  type: "bearer"
  bearer_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Client changes:

# Before: API key in header
curl -H "X-API-Key: sk-your-api-key" URL

# After: Bearer token in Authorization header
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." URL