Skip to content

Latest commit

 

History

History
333 lines (277 loc) · 12.5 KB

File metadata and controls

333 lines (277 loc) · 12.5 KB

Nexus Broker

Minimal, internal OAuth 2.0/OIDC and SAML 2.0 broker written in Go. It initiates consent, exchanges OAuth codes, validates SAML assertions, and stores credential material encrypted in PostgreSQL. Tokens and assertions are treated as opaque; we do not rely on id_token claims by default.

Key Capabilities

  • Provider registry (DB-backed)
  • Consent-spec builder (PKCE + HMAC state)
  • OAuth callback and code exchange
  • SAML SP AuthnRequest generation and ACS validation
  • AES-GCM token vault (encrypted at rest)
  • Token retrieval and on-demand refresh
  • Security gates (API key, IP allowlist, return URL validation)
  • Ready for service-mesh mTLS (tracked in docs/TECH_DEBT.md)
  • Prometheus metrics and structured logging
  • Integration Metadata: Exposes API base URLs and endpoints for frontend discovery.

Setup

1) Start PostgreSQL (dev)

docker-compose up -d postgres

Enable pgcrypto once:

psql "postgres://oauth_user:oauth_password@localhost/oauth_broker?sslmode=disable" \
  -c "CREATE EXTENSION IF NOT EXISTS pgcrypto;"

2) Configure environment

Create .env (see .env.example). Minimal:

DATABASE_URL=postgres://oauth_user:oauth_password@localhost/oauth_broker?sslmode=disable
BASE_URL=http://localhost:8080
ENCRYPTION_KEY=<32-byte base64, stable>
STATE_KEY=<32-byte base64, stable>
REDIRECT_PATH=/auth/callback
API_KEY=dev-api-key-12345
API_KEY_FILE=
API_KEYS_FILE=
API_KEY_RELOAD_INTERVAL=30s
ALLOWED_CIDRS=127.0.0.1/32,::1/128
ALLOWED_RETURN_DOMAINS=localhost,127.0.0.1,::1
PORT=8080

Generate keys (dev):

openssl rand -base64 32  # use output for ENCRYPTION_KEY
openssl rand -base64 32  # use output for STATE_KEY

Keep ENCRYPTION_KEY and STATE_KEY constant; changing them breaks decrypting stored tokens.

For production API key rotation, mount a secret file and set API_KEY_FILE or API_KEYS_FILE. API_KEY_FILE may contain one key, while API_KEYS_FILE may contain comma- or newline-separated keys. The broker reloads these files at API_KEY_RELOAD_INTERVAL without requiring a process restart.

3) Run the broker

source .env && go run ./cmd/nexus-broker

Health check:

curl -s http://localhost:8080/health

Register Providers

Endpoint requires payload under profile.

Partial Updates (PATCH)

Use PATCH /providers/{id} to update specific fields without overwriting the entire profile (e.g. updating scopes only).

curl -X PATCH http://localhost:8080/providers/<id> \
  -H "Content-Type: application/json" \
  -d '{"scopes": ["new", "scope"]}'

New Fields (v2)

  • auth_header: Set to "client_secret_basic" for providers requiring Basic Auth (Twitter, GitHub). Default is "client_secret_post" (Body).
  • api_base_url: Root URL for the provider's API (e.g., https://api.github.com). Used by frontend.
  • user_info_endpoint: Path to fetch user profile (e.g., /user). Used by frontend.
  • description: Human-readable description of what this provider is used for. Shown in the connected apps UI. Optional but recommended.
  • saml_idp_entity_id, saml_idp_sso_url, saml_idp_x509_cert, saml_sp_entity_id: SAML metadata fields required for auth_type: "saml".

Google

jq -n '{
  profile: {
    name: "google",
    auth_type: "oauth2",
    description: "Connect Google to access Gmail, Calendar, Drive, and Workspace apps for seamless productivity automation.",
    auth_url: "https://accounts.google.com/o/oauth2/v2/auth",
    token_url: "https://oauth2.googleapis.com/token",
    client_id: "<client-id>",
    client_secret: "<client-secret>",
    scopes: ["openid","email","profile"],
    api_base_url: "https://www.googleapis.com",
    user_info_endpoint: "/oauth2/v3/userinfo"
  }
}' | curl -s -X POST http://localhost:8080/providers -H "Content-Type: application/json" -d @- | jq .

Twitter (Requires Basic Auth)

jq -n '{
  profile: {
    name: "twitter",
    auth_type: "oauth2",
    description: "Connect Twitter (X) to automate tweet publishing, monitor mentions, and trigger social media workflows from engagement and timeline events.",
    auth_url: "https://twitter.com/i/oauth2/authorize",
    token_url: "https://api.twitter.com/2/oauth2/token",
    client_id: "<client-id>",
    client_secret: "<client-secret>",
    scopes: ["tweet.read","users.read"],
    auth_header: "client_secret_basic",
    api_base_url: "https://api.twitter.com/2",
    user_info_endpoint: "/users/me"
  }
}' | curl -s -X POST http://localhost:8080/providers -H "Content-Type: application/json" -d @- | jq .

Microsoft Graph (Common)

jq -n '{
  profile: {
    name: "microsoft-graph",
    auth_type: "oauth2",
    description: "Connect Microsoft 365 to access Teams, Outlook, OneDrive, and SharePoint for enterprise automation.",
    auth_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
    token_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
    client_id: "<application-client-id-guid>",
    client_secret: "<client-secret-value>",
    scopes: ["openid","email","profile","offline_access","User.Read"],
    api_base_url: "https://graph.microsoft.com/v1.0",
    user_info_endpoint: "/me"
  }
}' | curl -s -X POST http://localhost:8080/providers -H "Content-Type: application/json" -d @- | jq .

Note: Ensure your Azure App Registration has a Web platform configured with the correct Redirect URI.

SAML 2.0

jq -n '{
  profile: {
    name: "okta-saml",
    auth_type: "saml",
    description: "Authenticate users with the enterprise Okta SAML application.",
    saml_idp_entity_id: "https://idp.example.com/app/abc123",
    saml_idp_sso_url: "https://idp.example.com/app/abc123/sso/saml",
    saml_idp_x509_cert: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
    saml_sp_entity_id: "https://broker.example.com/saml/sp/okta"
  }
}' | curl -s -X POST http://localhost:8080/providers -H "Content-Type: application/json" -d @- | jq .

After registration, retrieve Nexus SP metadata and upload it to the IdP:

curl -H "X-API-Key: $API_KEY" \
  http://localhost:8080/saml/metadata/<provider_id>

SAML connections use the same consent-spec endpoint as OAuth. The returned authUrl points to the IdP with a SAML AuthnRequest and signed RelayState. The IdP must POST responses to BASE_URL + /saml/acs.

OIDC & Self-Discovery

If you request the openid scope, the Broker attempts OIDC Discovery:

  1. It uses the configured auth_url or token_url as a hint to find /.well-known/openid-configuration.
  2. If found, it dynamically uses the endpoints (Authorization, Token, UserInfo) declared in the metadata, ignoring your manually configured values if they differ.
  3. This simplifies maintenance for providers like Google, Okta, and Microsoft—you just need a valid "base" URL.

Provider Metadata (Frontend Integration)

Retrieve a grouped list of all configured providers and their API metadata. Useful for building dynamic "Connect" UIs.

curl -H "X-API-Key: $API_KEY" http://localhost:8080/providers/metadata

Response:

{
  "oauth2": {
    "google": {
      "api_base_url": "https://www.googleapis.com",
      "user_info_endpoint": "/oauth2/v3/userinfo",
      "scopes": ["openid", "email", "profile"],
      "description": "Connect Google to access Gmail, Calendar, Drive, and Workspace apps for seamless productivity automation."
    },
    "twitter": {
      "api_base_url": "https://api.twitter.com/2",
      "user_info_endpoint": "/users/me",
      "scopes": ["tweet.read", "users.read"],
      "description": "Connect Twitter (X) to automate tweet publishing, monitor mentions, and trigger social media workflows."
    }
  },
  "api_key": { ... }
}

Perform a Consent

Request the consent spec to get an authorization URL:

curl -s -X POST http://localhost:8080/auth/consent-spec \
  -H "Content-Type: application/json" \
  -d '{
    "workspace_id":"ws-123",
    "provider_id":"<provider_id>",
    "scopes":["openid","email"],
    "return_url":"http://localhost:3000/my-app-callback"
  }' | jq .

Open .authUrl in a browser and complete consent. You’ll be redirected to your return_url with connection_id, status, and provider as query parameters.


Retrieve and Refresh Tokens

Token retrieval (gated by API key + IP allowlist):

curl -H "X-API-Key: $API_KEY" \
  "http://localhost:8080/connections/<connection_id>/token"

On-demand refresh:

curl -X POST -H "X-API-Key: $API_KEY" \
  "http://localhost:8080/connections/<connection_id>/refresh"

Metrics and Logging

Prometheus at /metrics. All metrics are registered on startup.

Token Flows

Metric Type Labels Description
oauth_consents_created_total Counter Consent specs issued
oauth_consents_with_openid_total Counter Consents requesting OpenID scope
oauth_token_exchanges_total Counter status={success,error} Code-for-token exchanges
oauth_exchange_duration_seconds Histogram Duration of token exchange
oauth_id_tokens_returned_total Counter Exchanges that returned an id_token
oauth_token_refreshes_total Counter status={success,error} On-demand refresh attempts
oauth_refresh_duration_seconds Histogram Duration of token refresh
oauth_credential_captures_total Counter status={success,error} API-key credential captures (SaveCredential)

Token Retrieval

Metric Type Labels Description
oauth_token_get_total Counter provider, has_id_token Token retrievals by provider

OIDC Infrastructure

Metric Type Labels Description
oidc_verifications_total Counter result={success,error} ID token verifications
oidc_verification_duration_seconds Histogram ID token verification latency
oidc_discovery_total Counter result={success,error} OIDC discovery attempts
oidc_discovery_duration_seconds Histogram OIDC discovery latency

System Health

Metric Type Labels Description
nexus_connections_total Gauge status Live count of connections by status (polled every 30s)
nexus_db_operation_duration_seconds Histogram operation Repository-level DB operation latency

Access logs are structured; audit events are recorded in audit_events.


Security

  • PKCE and HMAC-signed state on every OAuth consent
  • HMAC-signed SAML RelayState bound to the pending connection and AuthnRequest
  • AES-GCM token encryption; keys never logged
  • API key required for sensitive endpoints (use X-API-Key)
  • IP allowlisting via ALLOWED_CIDRS
  • Return URL domain validation via ALLOWED_RETURN_DOMAINS
  • Always use HTTPS in production (set BASE_URL=https://...)
  • mTLS via service mesh planned; see docs/TECH_DEBT.md

See docs/SECURITY.md for detailed guardrails and operations.

OIDC hardening (id_token verification via JWKS, nonce, discovery) is fully implemented. See pkg/oidc for the validator and pkg/discovery for provider discovery.


Troubleshooting

  • invalid_scope (Google) for offline_access: remove; broker already adds Google-specific refresh params.
  • redirect_uri_mismatch: ensure provider console matches BASE_URL + REDIRECT_PATH exactly.
  • Failed to decrypt token: likely ENCRYPTION_KEY changed. Keep it stable and re-consent.
  • Provider not found / TEXT[] scan errors: ensure we use pq.Array for scopes (handled in code) and correct UUID types.
  • 404 on Google authorize: use https://accounts.google.com/o/oauth2/v2/auth (not legacy URLs).
  • pgcrypto missing: CREATE EXTENSION IF NOT EXISTS pgcrypto; on your DB.
  • Token exchange failed (Twitter/GitHub): Ensure you set "auth_header": "client_secret_basic" in the provider profile.
  • Token exchange failed (Microsoft): Ensure your Azure App Registration has a Web platform (not SPA/Public).

Production Notes

  • Use managed Postgres (e.g., Azure Flexible Server). Set sslmode=require.
  • Restrict DB network (VNet/private DNS or firewall IPs). Restrict broker sensitive routes by IP.
  • Keep encryption/state keys constant; rotate API keys through API_KEY_FILE or API_KEYS_FILE; monitor /metrics.
  • Document each provider in docs/PROVIDERS.md when added.

Development

Run tests:

go test ./...

Build:

go build -o nexus-broker ./cmd/nexus-broker

See also

  • docs/PROVIDERS.md – registry and templates for supported providers
  • docs/TECH_DEBT.md – OIDC hardening plan and acceptance criteria