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.
- 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
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
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.
Add tango
to your dependencies in mix.exs
:
def deps do
[
{:tango, "~> 0.1.0"}
]
end
Install dependencies:
mix deps.get
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")
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).
Providers are sourced from the Nango catalog with pre-configured OAuth endpoints and settings.
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
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
})
# 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"] || %{}
})
# 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"
)
# 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)
Tango includes a complete OAuth API router that can be mounted in Phoenix applications with a single line.
- 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")
- 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
The mounted API provides these endpoints:
POST /api/oauth/sessions
- Create OAuth sessionGET /api/oauth/authorize/:session_token
- Get authorization URLPOST /api/oauth/exchange
- Exchange authorization code for connectionGET /api/oauth/health
- Health check
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);
}
}
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)
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
- 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 is available on HexDocs or generate locally:
mix docs