Deprecated: Function get_magic_quotes_gpc() is deprecated in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 99

Deprecated: The each() function is deprecated. This message will be suppressed on further calls in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 619

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1169

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176

Warning: Cannot modify header information - headers already sent by (output started at /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php:99) in /hermes/walnacweb04/walnacweb04ab/b2791/pow.jasaeld/htdocs/De1337/nothing/index.php on line 1176
8000 GitHub - agoodway/tango: πŸ’ƒ Tango - OAuth Integrations Library
Nothing Special   »   [go: up one dir, main page]

Skip to content

agoodway/tango

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

27 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ’ƒ Tango - OAuth Integrations Library

Tango handles the OAuth dance between third-party services and your Phoenix application.

Tango is an Elixir OAuth integration library for Phoenix applications that provides drop-in OAuth support for third-party integrations. Inspired by Nango (previously Pizzly) and compatible with Nango's provider configuration format, Tango leverages the extensive Nango provider catalog while providing a library-first approach for Phoenix applications.

Key Features

  • Complete OAuth2 flows: Session creation, authorization URL generation, code exchange, token refresh, and revocation
  • Multi-tenant isolation: Tenant-scoped queries for all connections with automatic session lifecycle management
  • Security-first design: AES-GCM encryption for tokens, PKCE implementation, and secure random token generation
  • Comprehensive audit trail: Structured logging for OAuth events, token operations, and system activities

Core Modules

lib/tango/
β”œβ”€β”€ auth.ex                    # Main OAuth flow orchestrator
β”œβ”€β”€ provider.ex                # Provider configuration management
β”œβ”€β”€ connection.ex              # Token lifecycle and refresh
β”œβ”€β”€ vault.ex                   # AES-GCM encryption
└── schemas/                   # Ecto schemas
    β”œβ”€β”€ provider.ex            # OAuth provider configurations
    β”œβ”€β”€ connection.ex          # Active OAuth connections
    β”œβ”€β”€ oauth_session.ex       # Temporary OAuth sessions
    └── audit_log.ex           # Security audit logging

OAuth User Flow

Tango implements a complete OAuth2 Authorization Code Flow:

1. Provider Setup
   β”œβ”€ Create provider from Nango catalog or custom config
   β”œβ”€ Store OAuth endpoints and client credentials
   └─ Encrypt client secrets with AES-GCM

2. Session Creation
   β”œβ”€ Create OAuth session with secure tokens
   β”œβ”€ Generate PKCE parameters (64-byte verifier β†’ SHA256 challenge)
   └─ Store with 30-minute expiration and CSRF state

3. Authorization URL Generation
   β”œβ”€ Retrieve session and decrypt provider configuration
   β”œβ”€ Build authorization URL with PKCE challenge
   └─ Return URL for user redirect

4. Token Exchange
   β”œβ”€ Validate CSRF state and prevent cross-tenant attacks
   β”œβ”€ Exchange authorization code for access tokens
   β”œβ”€ Encrypt and store tokens with AES-GCM
   └─ Create persistent connection with status tracking

5. Connection Management
   β”œβ”€ Automatic refresh with 5-minute expiration buffer
   β”œβ”€ Exponential backoff with 3-attempt limits
   └─ Batch operations for background refresh jobs

Each step includes comprehensive audit logging, multi-tenant isolation, and security validations.

Installation

Add tango to your dependencies in mix.exs:

def deps do
  [
    {:tango, "~> 0.1.0"}
  ]
end

Install dependencies:

mix deps.get

Configuration

Configure Tango in your application:

# config/config.exs
config :tango,
  repo: MyApp.Repo,
  schema_prefix: "tango", # Optional
  encryption_key: System.get_env("TANGO_ENCRYPTION_KEY"),
  api_key: System.get_env("TANGO_API_KEY")

Migrations

If using Ecto for migrations, generate and run the Tango migration:

# Generate migration file
mix ecto.gen.migration add_tango_tables

# Edit the generated migration file to call Tango.Migration:
# priv/repo/migrations/YYYYMMDDHHMMSS_add_tango_tables.exs
defmodule MyApp.Repo.Migrations.AddTangoTables do
  use Ecto.Migration

  def up do
    Tango.Migration.up()
  end

  def down do
    Tango.Migration.down()
  end
end

# Run migrations
mix ecto.migrate

Otherwise, if not using Ecto migrations, you can copy the SQL from priv/repo/sql/versions/v01/v01_up.sql and add it to your migration tool of choice (be sure to replace the schema prefix with "public" or your custom prefix).

OAuth Providers

Providers are sourced from the Nango catalog with pre-configured OAuth endpoints and settings.

Mix Tasks

Use mix tasks for provider management with built-in validation and error handling:

# Show provider details from catalog
mix tango.providers.show github

# Create OAuth2 provider with single scope
mix tango.providers.create github --client-id=your_client_id --client-secret=your_secret

# Create OAuth2 provider with multiple scopes (individual --scope flags)
mix tango.providers.create microsoft \
  --client-id="your_azure_client_id" \
  --client-secret="your_azure_client_secret" \
  --scope="Calendars.Read" \
  --scope="Calendars.ReadWrite" \
  --scope="User.Read" \
  --scope="offline_access"

# Create OAuth2 provider with comma-separated scopes
mix tango.providers.create google \
  --client-id="your_google_client_id" \
  --client-secret="your_google_client_secret" \
  --scopes="https://www.googleapis.com/auth/calendar,https://www.googleapis.com/auth/userinfo.email"

# Create API key provider
mix tango.providers.create stripe --api-key=sk_live_xxx

# Sync all providers from catalog to database
mix tango.providers.sync

Programmatic Creation

For dynamic provider creation in your application code:

# OAuth2 provider using catalog configuration
{:ok, nango_config} = Tango.Catalog.get_provider("github")

client_id = "your_github_client_id"

{:ok, provider} = Tango.create_provider(%{
  name: nango_config["display_name"] || "GitHub",
  slug: "github",
  client_secret: "your_github_client_secret",
  config: Map.merge(nango_config, %{
    "client_id" => client_id
  }),
  default_scopes: ["user:email", "repo"],
  active: true,
  metadata: nango_config["metadata"] || %{}
})

# Custom provider without catalog
{:ok, provider} = Tango.create_provider(%{
  name: "Custom API",
  slug: "custom_api",
  client_secret: "your_client_secret",
  config: %{
    "client_id" => "your_client_id",
    "auth_url" => "https://api.example.com/oauth/authorize",
    "token_url" => "https://api.example.com/oauth/token",
    "auth_mode" => "OAUTH2"
  },
  default_scopes: ["read", "write"],
  active: true
})

# API key provider
{:ok, provider} = Tango.create_provider(%{
  name: "Custom API Key Service",
  slug: "custom_service",
  config: %{
    "auth_mode" => "API_KEY",
    "api_config" => %{
      "headers" => %{
        "authorization" => "Bearer ${api_key}"
      }
    }
  },
  api_key: "your_api_key",
  active: true
})

Quick Start

1. Set up OAuth Provider

# Create OAuth2 provider using mix task (recommended)
mix tango.providers.create github --client-id=your_client_id --client-secret=your_secret

# Create API key provider
mix tango.providers.create stripe --api-key=sk_live_xxx
# Or programmatically using catalog configuration
{:ok, nango_config} = Tango.Catalog.get_provider("github")

{:ok, provider} = Tango.create_provider(%{
  name: nango_config["display_name"] || "GitHub",
  slug: "github",
  client_secret: "your_client_secret",
  config: Map.merge(nango_config, %{
    "client_id" => "your_client_id"
  }),
  default_scopes: ["user:email", "repo"],
  active: true,
  metadata: nango_config["metadata"] || %{}
})

2. OAuth Flow Implementation

# Start OAuth session
{:ok, session} = Tango.create_session("github", tenant_id,
  redirect_uri: "https://yourapp.com/auth/callback",
  scopes: ["user:email", "repo"]
)

# Generate authorization URL
{:ok, auth_url} = Tango.authorize_url(session.session_token,
  redirect_uri: "https://yourapp.com/auth/callback"
)
# Redirect user to auth_url

# Handle callback - exchange code for tokens
{:ok, connection} = Tango.exchange_code(state, authorization_code,
  redirect_uri: "https://yourapp.com/auth/callback"
)

3. Use OAuth Connection

# Get active connection for API calls
{:ok, connection} = Tango.get_connection_for_provider("github", tenant_id)

# Use connection.access_token in your API requests
headers = [{"Authorization", "Bearer #{connection.access_token}"}]

# Mark connection as used (updates last_used_at)
Tango.mark_connection_used(connection)

Ready-to-Use OAuth API

Tango includes a complete OAuth API router that can be mounted in Phoenix applications with a single line.

Setup

  1. Configure API key in your application:
# config/config.exs  
config :tango,
  encryption_key: System.get_env("TANGO_ENCRYPTION_KEY"),
  api_key: System.get_env("TANGO_API_KEY")
  1. Add the API router to your Phoenix router:
# router.ex
defmodule MyAppWeb.Router do
  use MyAppWeb, :router

  scope "/api/oauth" do
    pipe_through :api
    forward "/", Tango.API.Router
  end
end

Available Endpoints

The mounted API provides these endpoints:

  • POST /api/oauth/sessions - Create OAuth session
  • GET /api/oauth/authorize/:session_token - Get authorization URL
  • POST /api/oauth/exchange - Exchange authorization code for connection
  • GET /api/oauth/health - Health check

JavaScript Usage

const API_BASE = '/api/oauth';
const TENANT_ID = 'user-123';
const API_KEY = 'your-secret-api-key';

// Start OAuth flow
async function startOAuth(provider, redirectUri, scopes = []) {
  // Create session
  const session = await fetch(`${API_BASE}/sessions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-Tenant-ID': TENANT_ID,
      'Authorization': `Bearer ${API_KEY}`
    },
    body: JSON.stringify({ provider, redirect_uri: redirectUri, scopes })
  }).then(r => r.json());

  // Get authorization URL
  const authUrl = await fetch(
    `${API_BASE}/authorize/${session.session_token}?redirect_uri=${redirectUri}&scopes=${scopes.join(' ')}`,
    { 
      headers: { 
        'X-Tenant-ID': TENANT_ID,
        'Authorization': `Bearer ${API_KEY}`
      } 
    }
  ).then(r => r.json());

  // Redirect to OAuth provider
  window.location.href = authUrl.authorization_url;
}

// Handle OAuth callback
async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const state = params.get('state');
  const code = params.get('code');
  
  if (state && code) {
    const connection = await fetch(`${API_BASE}/exchange`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Tenant-ID': TENANT_ID,
        'Authorization': `Bearer ${API_KEY}`
      },
      body: JSON.stringify({
        state,
        code,
        redirect_uri: window.location.origin + '/callback'
      })
    }).then(r => r.json());

    console.log('OAuth connection established:', connection);
  }
}

Connection Management

Use Tango's programmatic API in your Phoenix application to manage connections:

# List connections for a user
connections = Tango.list_connections(user_id)

# Get connection for API calls
{:ok, connection} = Tango.get_connection_for_provider("github", user_id)
headers = [{"Authorization", "Bearer #{connection.access_token}"}]

# Revoke connection
{:ok, _revoked} = Tango.revoke_connection(connection, user_id)

Testing

Requirements: PostgreSQL running locally with postgres:postgres credentials.

# Run all tests
mix test

# Run with coverage report
mix coveralls

# Generate HTML coverage report
mix coveralls.html

# Run quality checks (format, credo, tests)
mix quality

Planned Features

  • TypeScript client library: Client SDK for web or OAuth flows
  • Phoenix LiveView components: Pre-built UI component for OAuth flows
  • Token Refresh Automation: Background job integration with automatic token refresh scheduling
  • Rate Limiting: Built-in OAuth endpoint protection and request throttling
  • OAuth1 Support: Legacy OAuth support for older providers

Documentation

Documentation is available on HexDocs or generate locally:

mix docs
0