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.
- 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.
docker-compose up -d postgresEnable pgcrypto once:
psql "postgres://oauth_user:oauth_password@localhost/oauth_broker?sslmode=disable" \
-c "CREATE EXTENSION IF NOT EXISTS pgcrypto;"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=8080Generate keys (dev):
openssl rand -base64 32 # use output for ENCRYPTION_KEY
openssl rand -base64 32 # use output for STATE_KEYKeep 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.
source .env && go run ./cmd/nexus-brokerHealth check:
curl -s http://localhost:8080/healthEndpoint requires payload under profile.
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"]}'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 forauth_type: "saml".
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 .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 .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.
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.
If you request the openid scope, the Broker attempts OIDC Discovery:
- It uses the configured
auth_urlortoken_urlas a hint to find/.well-known/openid-configuration. - If found, it dynamically uses the endpoints (Authorization, Token, UserInfo) declared in the metadata, ignoring your manually configured values if they differ.
- This simplifies maintenance for providers like Google, Okta, and Microsoft—you just need a valid "base" URL.
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/metadataResponse:
{
"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": { ... }
}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.
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"Prometheus at /metrics. All metrics are registered on startup.
| 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) |
| Metric | Type | Labels | Description |
|---|---|---|---|
oauth_token_get_total |
Counter | provider, has_id_token |
Token retrievals by provider |
| 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 |
| 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.
- 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.
- invalid_scope (Google) for
offline_access: remove; broker already adds Google-specific refresh params. - redirect_uri_mismatch: ensure provider console matches
BASE_URL + REDIRECT_PATHexactly. - Failed to decrypt token: likely
ENCRYPTION_KEYchanged. Keep it stable and re-consent. - Provider not found / TEXT[] scan errors: ensure we use
pq.Arrayfor 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).
- 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_FILEorAPI_KEYS_FILE; monitor/metrics. - Document each provider in
docs/PROVIDERS.mdwhen added.
Run tests:
go test ./...Build:
go build -o nexus-broker ./cmd/nexus-brokerdocs/PROVIDERS.md– registry and templates for supported providersdocs/TECH_DEBT.md– OIDC hardening plan and acceptance criteria