{"id":32645554,"url":"https://github.com/agoodway/tango","last_synced_at":"2026-06-22T21:31:21.092Z","repository":{"id":314725113,"uuid":"1055532829","full_name":"agoodway/tango","owner":"agoodway","description":"💃 Tango - OAuth Integrations Library","archived":false,"fork":false,"pushed_at":"2026-04-23T20:37:11.000Z","size":548,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-05T14:33:49.022Z","etag":null,"topics":["access-token","api-integration","api-integrations","integrations","nango","oauth","oauth2","product-integration","product-integrations","refresh-token"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/agoodway.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-12T12:05:07.000Z","updated_at":"2026-04-23T20:37:16.000Z","dependencies_parsed_at":"2025-09-14T12:16:42.857Z","dependency_job_id":"7b2dd004-7c1e-4ec5-a408-5abf3fc6d83c","html_url":"https://github.com/agoodway/tango","commit_stats":null,"previous_names":["agoodway/tango"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/agoodway/tango","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agoodway%2Ftango","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agoodway%2Ftango/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agoodway%2Ftango/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agoodway%2Ftango/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/agoodway","download_url":"https://codeload.github.com/agoodway/tango/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/agoodway%2Ftango/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34666961,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-22T02:00:06.391Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["access-token","api-integration","api-integrations","integrations","nango","oauth","oauth2","product-integration","product-integrations","refresh-token"],"created_at":"2025-10-31T04:14:46.528Z","updated_at":"2026-06-22T21:31:21.087Z","avatar_url":"https://github.com/agoodway.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 💃 Tango - OAuth Integrations Library\n\n[![Hex.pm](https://img.shields.io/hexpm/v/tango.svg)](https://hex.pm/packages/tango)\n[![Hex Docs](https://img.shields.io/badge/hex-docs-blue.svg)](https://hexdocs.pm/tango)\n[![License](https://img.shields.io/hexpm/l/tango.svg)](https://github.com/agoodway/tango/blob/main/LICENSE)\n\n\u003e Tango handles the OAuth dance between third-party services and your Phoenix application.\n\n**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.\n\n## Key Features\n\n- **Complete OAuth2 flows**: Session creation, authorization URL generation, code exchange, token refresh, and revocation\n- **Multi-tenant isolation**: Tenant-scoped queries for all connections with automatic session lifecycle management\n- **Security-first design**: AES-GCM encryption for tokens, PKCE implementation, and secure random token generation\n- **Comprehensive audit trail**: Structured logging for OAuth events, token operations, and system activities\n\n## Core Modules\n\n```\nlib/tango/\n├── auth.ex                    # Main OAuth flow orchestrator\n├── provider.ex                # Provider configuration management\n├── connection.ex              # Token lifecycle and refresh\n├── vault.ex                   # AES-GCM encryption\n└── schemas/                   # Ecto schemas\n    ├── provider.ex            # OAuth provider configurations\n    ├── connection.ex          # Active OAuth connections\n    ├── oauth_session.ex       # Temporary OAuth sessions\n    └── audit_log.ex           # Security audit logging\n```\n\n## OAuth User Flow\n\nTango implements a complete OAuth2 Authorization Code Flow:\n\n```\n1. Provider Setup\n   ├─ Create provider from Nango catalog or custom config\n   ├─ Store OAuth endpoints and client credentials\n   └─ Encrypt client secrets with AES-GCM\n\n2. Session Creation\n   ├─ Create OAuth session with secure tokens\n   ├─ Generate PKCE parameters (64-byte verifier → SHA256 challenge)\n   └─ Store with 30-minute expiration and CSRF state\n\n3. Authorization URL Generation\n   ├─ Retrieve session and decrypt provider configuration\n   ├─ Build authorization URL with PKCE challenge\n   └─ Return URL for user redirect\n\n4. Token Exchange\n   ├─ Validate CSRF state and prevent cross-tenant attacks\n   ├─ Exchange authorization code for access tokens\n   ├─ Encrypt and store tokens with AES-GCM\n   └─ Create persistent connection with status tracking\n\n5. Connection Management\n   ├─ Automatic refresh with 5-minute expiration buffer\n   ├─ Exponential backoff with 3-attempt limits\n   └─ Batch operations for background refresh jobs\n```\n\nEach step includes comprehensive audit logging, multi-tenant isolation, and security validations.\n\n## Installation\n\nAdd `tango` to your dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:tango, \"~\u003e 0.1.1\"}\n  ]\nend\n```\n\nInstall dependencies:\n\n```bash\nmix deps.get\n```\n\n## Configuration\n\nConfigure Tango in your application:\n\n```elixir\n# config/config.exs\nconfig :tango,\n  repo: MyApp.Repo,\n  schema_prefix: \"tango\", # Optional\n  encryption_key: System.get_env(\"TANGO_ENCRYPTION_KEY\"),\n  api_key: System.get_env(\"TANGO_API_KEY\")\n```\n\n### Migrations\n\n`mix tango.setup` writes a wrapper migration to `priv/repo/migrations/`; `mix ecto.migrate` applies it.\n\n```bash\nmix tango.setup\nmix ecto.migrate\n```\n\nNot using Ecto migrations? Copy the SQL from `priv/repo/sql/versions/v01/v01_up.sql` into your migration tool of choice (replace the schema prefix with `\"public\"` or your custom prefix).\n\n## OAuth Providers\n\nProviders are sourced from the [Nango catalog](https://docs.nango.dev/integrations/overview) with pre-configured OAuth endpoints and settings.\n\n### Mix Tasks\n\nUse mix tasks for provider management with built-in validation and error handling:\n\n```bash\n# Show provider details from catalog\nmix tango.providers.show github\n\n# Create OAuth2 provider with single scope\nmix tango.providers.create github --client-id=your_client_id --client-secret=your_secret\n\n# Create OAuth2 provider with multiple scopes (individual --scope flags)\nmix tango.providers.create microsoft \\\n  --client-id=\"your_azure_client_id\" \\\n  --client-secret=\"your_azure_client_secret\" \\\n  --scope=\"Calendars.Read\" \\\n  --scope=\"Calendars.ReadWrite\" \\\n  --scope=\"User.Read\" \\\n  --scope=\"offline_access\"\n\n# Create OAuth2 provider with comma-separated scopes\nmix tango.providers.create google \\\n  --client-id=\"your_google_client_id\" \\\n  --client-secret=\"your_google_client_secret\" \\\n  --scopes=\"https://www.googleapis.com/auth/calendar,https://www.googleapis.com/auth/userinfo.email\"\n\n# Create API key provider\nmix tango.providers.create stripe --api-key=sk_live_xxx\n\n# Sync all providers from catalog to database\nmix tango.providers.sync\n```\n\n### Programmatic Creation\n\nFor dynamic provider creation in your application code:\n\n```elixir\n# OAuth2 provider using catalog configuration\n{:ok, nango_config} = Tango.Catalog.get_provider(\"github\")\n\nclient_id = \"your_github_client_id\"\n\n{:ok, provider} = Tango.create_provider(%{\n  name: nango_config[\"display_name\"] || \"GitHub\",\n  slug: \"github\",\n  client_secret: \"your_github_client_secret\",\n  config: Map.merge(nango_config, %{\n    \"client_id\" =\u003e client_id\n  }),\n  default_scopes: [\"user:email\", \"repo\"],\n  active: true,\n  metadata: nango_config[\"metadata\"] || %{}\n})\n\n# Custom provider without catalog\n{:ok, provider} = Tango.create_provider(%{\n  name: \"Custom API\",\n  slug: \"custom_api\",\n  client_secret: \"your_client_secret\",\n  config: %{\n    \"client_id\" =\u003e \"your_client_id\",\n    \"auth_url\" =\u003e \"https://api.example.com/oauth/authorize\",\n    \"token_url\" =\u003e \"https://api.example.com/oauth/token\",\n    \"auth_mode\" =\u003e \"OAUTH2\"\n  },\n  default_scopes: [\"read\", \"write\"],\n  active: true\n})\n\n# API key provider\n{:ok, provider} = Tango.create_provider(%{\n  name: \"Custom API Key Service\",\n  slug: \"custom_service\",\n  config: %{\n    \"auth_mode\" =\u003e \"API_KEY\",\n    \"api_config\" =\u003e %{\n      \"headers\" =\u003e %{\n        \"authorization\" =\u003e \"Bearer ${api_key}\"\n      }\n    }\n  },\n  api_key: \"your_api_key\",\n  active: true\n})\n```\n\n## Quick Start\n\n### 1. Set up OAuth Provider\n\n```bash\n# Create OAuth2 provider using mix task (recommended)\nmix tango.providers.create github --client-id=your_client_id --client-secret=your_secret\n\n# Create API key provider\nmix tango.providers.create stripe --api-key=sk_live_xxx\n```\n\n```elixir\n# Or programmatically using catalog configuration\n{:ok, nango_config} = Tango.Catalog.get_provider(\"github\")\n\n{:ok, provider} = Tango.create_provider(%{\n  name: nango_config[\"display_name\"] || \"GitHub\",\n  slug: \"github\",\n  client_secret: \"your_client_secret\",\n  config: Map.merge(nango_config, %{\n    \"client_id\" =\u003e \"your_client_id\"\n  }),\n  default_scopes: [\"user:email\", \"repo\"],\n  active: true,\n  metadata: nango_config[\"metadata\"] || %{}\n})\n```\n\n### 2. OAuth Flow Implementation\n\n```elixir\n# Start OAuth session\n{:ok, session} = Tango.create_session(\"github\", tenant_id,\n  redirect_uri: \"https://yourapp.com/auth/callback\",\n  scopes: [\"user:email\", \"repo\"]\n)\n\n# Generate authorization URL\n{:ok, auth_url} = Tango.authorize_url(session.session_token,\n  redirect_uri: \"https://yourapp.com/auth/callback\"\n)\n# Redirect user to auth_url\n\n# Handle callback - exchange code for tokens\n{:ok, connection} = Tango.exchange_code(state, authorization_code,\n  redirect_uri: \"https://yourapp.com/auth/callback\"\n)\n```\n\n### 3. Use OAuth Connection\n\n```elixir\n# Get active connection for API calls\n{:ok, connection} = Tango.get_connection_for_provider(\"github\", tenant_id)\n\n# Use connection.access_token in your API requests\nheaders = [{\"Authorization\", \"Bearer #{connection.access_token}\"}]\n\n# Mark connection as used (updates last_used_at)\nTango.mark_connection_used(connection)\n```\n\n## Ready-to-Use OAuth API\n\nTango includes a complete OAuth API router that can be mounted in Phoenix applications with a single line.\n\n### Setup\n\n1. Configure API key in your application:\n\n```elixir\n# config/config.exs  \nconfig :tango,\n  encryption_key: System.get_env(\"TANGO_ENCRYPTION_KEY\"),\n  api_key: System.get_env(\"TANGO_API_KEY\")\n```\n\n2. Add the API router to your Phoenix router:\n\n```elixir\n# router.ex\ndefmodule MyAppWeb.Router do\n  use MyAppWeb, :router\n\n  scope \"/api/oauth\" do\n    pipe_through :api\n    forward \"/\", Tango.API.Router\n  end\nend\n```\n\n### Available Endpoints\n\nThe mounted API provides these endpoints:\n\n- `POST /api/oauth/sessions` - Create OAuth session\n- `GET /api/oauth/authorize/:session_token` - Get authorization URL  \n- `POST /api/oauth/exchange` - Exchange authorization code for connection\n- `GET /api/oauth/health` - Health check\n\n### JavaScript Usage\n\n```javascript\nconst API_BASE = '/api/oauth';\nconst TENANT_ID = 'user-123';\nconst API_KEY = 'your-secret-api-key';\n\n// Start OAuth flow\nasync function startOAuth(provider, redirectUri, scopes = []) {\n  // Create session\n  const session = await fetch(`${API_BASE}/sessions`, {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n      'X-Tenant-ID': TENANT_ID,\n      'Authorization': `Bearer ${API_KEY}`\n    },\n    body: JSON.stringify({ provider, redirect_uri: redirectUri, scopes })\n  }).then(r =\u003e r.json());\n\n  // Get authorization URL\n  const authUrl = await fetch(\n    `${API_BASE}/authorize/${session.session_token}?redirect_uri=${redirectUri}\u0026scopes=${scopes.join(' ')}`,\n    { \n      headers: { \n        'X-Tenant-ID': TENANT_ID,\n        'Authorization': `Bearer ${API_KEY}`\n      } \n    }\n  ).then(r =\u003e r.json());\n\n  // Redirect to OAuth provider\n  window.location.href = authUrl.authorization_url;\n}\n\n// Handle OAuth callback\nasync function handleCallback() {\n  const params = new URLSearchParams(window.location.search);\n  const state = params.get('state');\n  const code = params.get('code');\n  \n  if (state \u0026\u0026 code) {\n    const connection = await fetch(`${API_BASE}/exchange`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'X-Tenant-ID': TENANT_ID,\n        'Authorization': `Bearer ${API_KEY}`\n      },\n      body: JSON.stringify({\n        state,\n        code,\n        redirect_uri: window.location.origin + '/callback'\n      })\n    }).then(r =\u003e r.json());\n\n    console.log('OAuth connection established:', connection);\n  }\n}\n```\n\n### Connection Management\n\nUse Tango's programmatic API in your Phoenix application to manage connections:\n\n```elixir\n# List connections for a user\nconnections = Tango.list_connections(user_id)\n\n# Get connection for API calls\n{:ok, connection} = Tango.get_connection_for_provider(\"github\", user_id)\nheaders = [{\"Authorization\", \"Bearer #{connection.access_token}\"}]\n\n# Revoke connection\n{:ok, _revoked} = Tango.revoke_connection(connection, user_id)\n```\n\n## Testing\n\n**Requirements**: PostgreSQL running locally with `postgres:postgres` credentials.\n\n```bash\n# Run all tests\nmix test\n\n# Run with coverage report\nmix coveralls\n\n# Generate HTML coverage report\nmix coveralls.html\n\n# Run quality checks (format, credo, tests)\nmix quality\n```\n\n## Planned Features\n\n- **TypeScript client library**: Client SDK for web or OAuth flows\n- **Phoenix LiveView components**: Pre-built UI component for OAuth flows\n- **Token Refresh Automation**: Background job integration with automatic token refresh scheduling\n- **Rate Limiting**: Built-in OAuth endpoint protection and request throttling\n- **OAuth1 Support**: Legacy OAuth support for older providers\n\n## Documentation\n\nDocumentation is available on [HexDocs](https://hexdocs.pm/tango) or generate locally:\n\n```bash\nmix docs\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagoodway%2Ftango","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fagoodway%2Ftango","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fagoodway%2Ftango/lists"}