# 💃 Tango - OAuth Integrations Library
[](https://hex.pm/packages/tango)
[](https://hexdocs.pm/tango)
[](https://github.com/agoodway/tango/blob/main/LICENSE)
> 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](https://github.com/NangoHQ/nango) (previously [Pizzly](https://dev.to/bearer/introducing-pizzly-an-open-sourced-free-fast-simple-api-integrations-manager-4jog)) and compatible with Nango's provider configuration format, Tango leverages the extensive Nango [provider catalog](https://docs.nango.dev/integrations) 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`:
```elixir
def deps do
[
{:tango, "~> 0.1.0"}
]
end
```
Install dependencies:
```bash
mix deps.get
```
## Configuration
Configure Tango in your application:
```elixir
# 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:
```bash
# 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](https://docs.nango.dev/integrations/overview) with pre-configured OAuth endpoints and settings.
### Mix Tasks
Use mix tasks for provider management with built-in validation and error handling:
```bash
# 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:
```elixir
# 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
```bash
# 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
```
```elixir
# 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
```elixir
# 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
```elixir
# 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:
```elixir
# config/config.exs
config :tango,
encryption_key: System.get_env("TANGO_ENCRYPTION_KEY"),
api_key: System.get_env("TANGO_API_KEY")
```
2. Add the API router to your Phoenix router:
```elixir
# 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
```javascript
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:
```elixir
# 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.
```bash
# 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](https://hexdocs.pm/tango) or generate locally:
```bash
mix docs
```