This document describes the authentication mechanisms supported by ggcode's A2A (Agent-to-Agent) server, their applicable scenarios, and configuration examples.
When no authentication scheme is configured, the A2A server only accepts requests from localhost (127.0.0.1, ::1). This is safe for single-user development.
To expose A2A to remote agents, you must configure at least one authentication scheme (see below), or explicitly set allow_unauthenticated: true:
a2a:
auth:
allow_unauthenticated: true # NOT recommended for production
⚠️ allow_unauthenticated: trueallows any network client to access the A2A server. Only use this in isolated test environments.
| Scheme | Spec Type | Secret Required | Best For | User Interaction |
|---|---|---|---|---|
| API Key | apiKey |
Shared secret | Development, trusted networks | None |
| OAuth2 + PKCE | oauth2 |
No (public client) | Human-driven agents, web integrations | Browser login |
| OAuth2 Device Flow | oauth2 |
No (public client) | Headless servers, SSH, CI/CD | Visit URL + enter code |
| OpenID Connect | openIdConnect |
No (PKCE) | Enterprise identity, SSO | Browser login |
| Mutual TLS | mutualTLS |
Certificates only | Machine-to-machine, zero-trust | None (automated) |
Spec type: apiKey
Scenario: Local development, trusted internal networks, quick prototyping.
- A shared secret string is configured on both the A2A server and client.
- The client sends the key in the
X-API-KeyHTTP header. - Constant-time comparison prevents timing attacks.
# ggcode.yaml
a2a:
enabled: true
auth:
api_key: "${A2A_API_KEY}" # from env var (under auth: block)Note: The legacy top-level
a2a.api_keystill works, buta2a.auth.api_keytakes priority.
- ✅ Local development / testing
- ✅ Trusted internal network (VPN/VPC)
- ✅ Quick prototyping
- ❌ Public networks (secret can be intercepted)
- ❌ Multi-tenant environments (no user identity)
- ❌ Production deployments needing audit trails
- Single shared secret — no per-user differentiation
- No identity information — cannot tell who made the request
- Secret must be distributed to all clients out-of-band
Spec type: oauth2 (authorizationCode)
Scenario: Agents that operate with human initiation, web-based integrations, any environment with a browser.
┌──────────┐ ┌──────────┐ ┌──────────┐
│ ggcode │ │ Browser │ │ Identity │
│ (client) │ │ │ │ Provider │
└─────┬────┘ └────┬─────┘ └────┬─────┘
│ generate PKCE │ │
│ code_verifier │ │
│ code_challenge │ │
│───────────────────>│ open browser │
│ │──────────────────>│ authorize
│ │<──────────────────│ consent
│<───────────────────│ callback(code) │
│ exchange code + verifier for token │
│────────────────────────────────────────>│
│<────────────────────────────────────────│ access_token
│ │
│ use Bearer token for A2A requests │
Key point: No client_secret needed for public clients. PKCE (Proof Key for Code Exchange) proves the token exchange is coming from the same client that started the flow. For confidential clients (GitHub OAuth Apps), client_secret is required.
Flow selection: Set flow: "pkce" (default) or flow: "device" to explicitly choose the OAuth2 flow. If omitted, the system selects based on provider capabilities.
Zero-config (GitHub only — client_id built-in):
# ggcode.yaml
a2a:
enabled: true
auth:
oauth2:
provider: "github" # client_id auto-filled, just worksUsing a built-in provider preset with custom client_id:
# ggcode.yaml
a2a:
enabled: true
auth:
oauth2:
provider: "github"
client_id: "Ov23li-your-registered-app-id" # overrides default
scopes: "read:user user:email" # optional, defaults from presetUsing a custom provider:
# ggcode.yaml
a2a:
enabled: true
auth:
oauth2:
client_id: "my-custom-client"
issuer_url: "https://my-idp.example.com"
scopes: "openid profile email"Note: GitHub has a built-in public client_id (PKCE-only, no secret). Just set
provider: "github"for instant zero-config. Other providers require you to register an OAuth App and provide the client_id.
| Provider | provider value |
PKCE | Device Flow | Notes |
|---|---|---|---|---|
| GitHub | "github" |
✅ | ✅ | Register at Settings → Developer settings → OAuth Apps |
"google" |
✅ | ❌ | Register at Google Cloud Console → APIs & Services → Credentials | |
| Auth0 | "auth0" |
✅ | ✅ | Replace AUTH0_TENANT in URLs with your tenant name |
| Azure AD | "azure" |
✅ | ✅ | Replace AZURE_TENANT in URLs with your tenant ID |
- ✅ Agents triggered by humans (with browser access)
- ✅ Web-based agent dashboards
- ✅ Multi-user environments needing per-user identity
- ✅ Any scenario where the IdP is accessible
- ❌ Headless servers without browser
- ❌ Fully automated machine-to-machine
| Provider | issuer_url | Notes |
|---|---|---|
https://accounts.google.com |
Register OAuth2 Web App (public) | |
| GitHub | https://github.com/login/oauth |
Set OAuth App as public |
| Auth0 | https://<tenant>.auth0.com |
Enable PKCE in application settings |
| Keycloak | https://<host>/realms/<realm> |
Self-hosted, full control |
| Azure AD | https://login.microsoftonline.com/<tenant> |
Enterprise scenarios |
Spec type: oauth2 (deviceCode)
Scenario: Headless servers, SSH sessions, CI/CD pipelines, environments without a browser.
┌──────────┐ ┌──────────┐
│ ggcode │ │ Identity │
│ (client) │ │ Provider │
└─────┬────┘ └────┬─────┘
│ POST device_code request │
│──────────────────────────────────────>│
│<──────────────────────────────────────│ user_code + verification_uri
│ │
│ Display: "Visit https://... Enter: ABCD-1234"
│ │
│ Poll: POST token request (slow) │
│──────────────────────────────────────>│ authorization_pending
│<──────────────────────────────────────│
│ ... user visits URL and enters code ...│
│ Poll: POST token request │
│──────────────────────────────────────>│
│<──────────────────────────────────────│ access_token ✓
Key point: No browser on the server needed. User authenticates on any device with a browser.
# ggcode.yaml
a2a:
enabled: true
auth:
oauth2:
client_id: "ggcode-a2a-device" # safe to embed
issuer_url: "https://github.com/login/oauth" # must support device flow
scopes: "read write"- ✅ SSH / remote server environments
- ✅ CI/CD pipelines with human approval
- ✅ Containers / headless VMs
- ✅ Any environment where browser is not available
- ❌ Fully automated M2M (use mTLS instead)
- ❌ High-frequency automated calls (token lifecycle overhead)
| Provider | Device Flow Support |
|---|---|
| GitHub | ✅ Native |
| ❌ Not supported | |
| Auth0 | ✅ With configuration |
| Azure AD | ✅ Native |
| Keycloak | ✅ With protocol mapper |
Spec type: openIdConnect
Scenario: Enterprise SSO, identity federation, environments needing verified user identity.
OIDC = OAuth2 + standardized identity layer. Same flow as OAuth2 + PKCE, but additionally:
- Receives an
id_token(signed JWT with user claims) - Can call
/userinfoendpoint for profile data - Automatic configuration via
/.well-known/openid-configuration
Same as OAuth2 + PKCE, but also:
→ id_token contains: sub, name, email, groups, ...
→ Server validates JWT signature using provider's JWKS
→ User identity extracted from token claims
Using a built-in provider preset:
# ggcode.yaml
a2a:
enabled: true
auth:
oidc:
provider: "google" # auto-fills discovery URL
client_id: "your-client-id.apps.googleusercontent.com"
scopes: "openid profile email groups"Using a custom OIDC provider:
# ggcode.yaml
a2a:
enabled: true
auth:
oidc:
client_id: "my-oidc-client"
issuer_url: "https://keycloak.example.com/realms/myrealm"
scopes: "openid email roles"The
issuer_urlmust serve/.well-known/openid-configurationwith standard OIDC discovery metadata.
- ✅ Enterprise environments with SSO (Okta, Azure AD, Keycloak)
- ✅ Need for auditable user identity in A2A requests
- ✅ Group/role-based access control
- ✅ Federated identity across organizations
- ❌ Simple single-user setups (API Key is simpler)
- ❌ Machine-to-machine without identity context
Spec type: mutualTLS
Scenario: Machine-to-machine communication, zero-trust networks, environments where no IdP is available.
┌──────────┐ ┌──────────┐
│ ggcode │ TLS Handshake │ ggcode │
│ (client) │◄─────────────────────────►│ (server) │
└──────────┘ client_cert ↔ server_cert└──────────┘
│
Both sides verify
the other's certificate
against a shared CA
- Each ggcode instance has its own certificate signed by a shared CA.
- TLS handshake validates both sides — no tokens, no passwords.
- Certificate = Identity.
# ggcode.yaml
a2a:
enabled: true
auth:
mtls:
cert_file: "/etc/ggcode/a2a/server.crt"
key_file: "/etc/ggcode/a2a/server.key"
ca_file: "/etc/ggcode/a2a/ca.crt"Option A: Self-hosted CA (step-ca / cfssl)
# Install step-ca
step ca init --name="ggcode-a2a" --provisioner="admin"
# Issue server cert
step ca certificate "agent1.example.com" server.crt server.key
# Issue client cert
step ca certificate "agent2.example.com" client.crt client.keyOption B: HashiCorp Vault PKI
vault secrets enable pki
vault write pki/root/generate/internal common_name="ggcode-a2a"
vault write pki/roles/ggcode allowed_domains="example.com"
vault write pki/issue/ggcode common_name="agent1.example.com"- ✅ Pure machine-to-machine (no human in the loop)
- ✅ Zero-trust network architectures
- ✅ Environments without internet/IdP access (air-gapped)
- ✅ High-security deployments requiring cryptographic identity
- ✅ Low-latency requirements (no token validation round-trips)
- ❌ Environments where certificate management is too complex
- ❌ Need for human identity (use OAuth2/OIDC instead)
- ❌ Dynamic/unmanaged clients (cert distribution overhead)
Multiple authentication mechanisms can be enabled simultaneously:
# ggcode.yaml
a2a:
enabled: true
api_key: "${A2A_API_KEY}" # Legacy / dev fallback
auth:
oauth2: # Human-driven agents
client_id: "ggcode-a2a-public"
issuer_url: "https://accounts.google.com"
scopes: "openid profile"
mtls: # Machine-to-machine
cert_file: "/etc/ggcode/a2a/server.crt"
key_file: "/etc/ggcode/a2a/server.key"
ca_file: "/etc/ggcode/a2a/ca.crt"When multiple schemes are configured, the server accepts any valid credential:
X-API-Keyheader → API Key validationAuthorization: Bearer <token>→ OAuth2/OIDC validation- Client certificate (TLS) → mTLS validation
Is there a human in the loop?
├── Yes, with browser
│ └── Need identity/SSO?
│ ├── Yes → OpenID Connect
│ └── No → OAuth2 + PKCE
├── Yes, no browser (SSH/CI)
│ └── OAuth2 Device Flow
└── No, pure machine-to-machine
├── Have a CA/identity provider?
│ ├── Yes → Mutual TLS
│ └── No → API Key (internal network only)
└── Have an OAuth2 IdP?
├── Yes → OAuth2 Client Credentials
└── No → API Key (development only)
| Concern | API Key | OAuth2+PKCE | Device Flow | OIDC | mTLS |
|---|---|---|---|---|---|
| Secret in source code | ✅ No secret | ✅ No secret | ✅ No secret | ✅ Certs only | |
| Man-in-the-middle | ✅ PKCE | ✅ PKCE | ✅ PKCE | ✅ Built-in | |
| Identity tracking | ❌ None | ✅ Per-user | ✅ Per-user | ✅ Full claims | ✅ Per-cert |
| Token theft | ✅ Expires | ✅ Expires | ✅ Expires | ✅ No tokens | |
| Revocation | ❌ Manual | ✅ IdP | ✅ IdP | ✅ IdP | ✅ CRL/OCSP |
| Setup complexity | ⭐ Minimal | ⭐⭐ Low | ⭐⭐ Low | ⭐⭐⭐ Medium | ⭐⭐⭐ Medium |
| Production ready | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
OAuth2/OIDC tokens are automatically cached to disk so the agent doesn't need to re-authenticate on every restart.
~/.ggcode/oauth-tokens/
github-Ov23liq0EQyT.json ← built-in GitHub client_id
github-my-own-app.json ← user's own client_id
auth0-abc123def456.json ← different provider
- Cache key =
{provider}-{clientID[:12]}— first 12 chars of client ID - Same
client_id+provider= shared token file (intentional — same OAuth App) - Different
client_id= isolated cache (prevents multi-instance overwrites) - File permissions:
0600(owner read/write only)
- Client needs a token → checks cache first
- Cache hit + not expired → reuse (no user interaction)
- Cache miss or expired → initiate OAuth2 flow (PKCE or Device)
- Token received → save to cache with expiry
- Daemon restart → cache still valid → seamless
The token cache is purely client-side. Server auth state is config-driven — no server-side token persistence needed.
Each ggcode workspace can override the global A2A config via .ggcode/a2a.yaml:
# .ggcode/a2a.yaml — merges into global a2a config
auth:
api_key: "project-specific-key"
oauth2:
provider: "github"
flow: "device"This allows per-project auth settings while sharing the same global config. Fields are merged — instance values override global values.
When running in daemon mode with IM adapters, the following slash commands are available for adapter management:
| Command | Description |
|---|---|
/listim |
List all IM adapters with name, platform, status (online/muted/active) |
/muteim <name> |
Mute a specific adapter by name. Cannot mute yourself — use /muteself instead |
/muteall |
Mute all adapters except the one you're messaging from |
/muteself |
Mute THIS adapter. You will stop receiving all replies. Use /restart from another adapter to recover |
/restart |
Restart daemon — unmutes all adapters (mute is in-memory, not persisted) |
/help |
Show available slash commands |
Important behaviors:
- Mute is in-memory only — not persisted to the binding store. Daemon restart recovers all adapters.
/muteallusesMuteAllExcept(selfAdapter)— the sender's adapter is never touched./muteselfemits the warning message before muting (500ms delay ensures delivery).