WikifitaGitHub live67e8de5
outro · sdk-test/sdk-test-auth-chain

Authentication Chain — OAuth2, Keychain, and Token Lifecycle

Complete authentication chain analysis: TrySilentAuth flow, credential management, keychain format, scopes, passive refresh via Heartbeat, and installation IDs

Baixar raw

Authentication Chain — OAuth2, Keychain, and Token Lifecycle

Overview

The JETSKY ecosystem uses a unified OAuth2 authentication layer shared across all components (Master, IDE, CLI, SDK). Tokens are stored in the macOS Keychain as the primary store, with a file-based fallback at ~/.gemini/. The authentication chain was reverse-engineered from the agy Go binary via Ghidra decompilation (3,305 functions).

All credentials on this page are [REDACTED]. No real secrets are stored in the wiki.

Complete Authentication Flow

flowchart TD
    A["API Call Needed<br/>(any gRPC or model request)"] --> B["TrySilentAuth()"]
    
    B --> C{"Memory cache<br/>(singleton AuthProvider)"}
    C -->|"Cache hit + valid TTL"| F["Return Bearer<br/>access_token"]
    C -->|"Cache miss or expired"| D{"ChainedAuth()"}
    
    D --> E["keyringAuth()<br/>[Primary Strategy]"]
    E --> E1{"macOS Keychain<br/>service: gemini<br/>account: antigravity"}
    E1 -->|"Found + valid TTL"| F
    E1 -->|"Found + expired"| G["refreshAndSaveToken()"]
    E1 -->|"Not found (1s timeout)"| H["fileAuth()<br/>[Fallback Strategy]"]
    
    H --> H1{"~/.gemini/<br/>token file"}
    H1 -->|"Found + valid TTL"| F
    H1 -->|"Found + expired"| G
    H1 -->|"Not found"| I["interactiveAuth()<br/>[Last Resort]"]
    
    I --> I1["PKCE Browser Flow<br/>accounts.google.com/o/oauth2/v2/auth"]
    I1 --> I2["User authorizes in browser"]
    I2 --> G
    
    G --> G1["POST oauth2.googleapis.com/token"]
    G1 --> G2["applyAuthResult()"]
    G2 --> G3["Write to Keychain"]
    G2 --> G4["Write to File Fallback"]
    G2 --> G5["Update Memory Cache"]
    G3 --> F
    G4 --> F
    G5 --> F

Decompiled Auth Functions

The following functions were decompiled from the agy binary:

FunctionPackage PathPurpose
TrySilentAuthauth.(*AuthProvider)Entry point. Checks memory cache, delegates to ChainedAuth if expired
ChainedAuthauth.(*AuthProvider)Chains strategies: keyring → file → interactive
keyringAuthauth.(*AuthProvider)Reads from macOS Keychain via go-keyring library
fileAuthauth.(*AuthProvider)Reads from ~/.gemini/ file fallback
interactiveAuthauth.(*AuthProvider)Launches PKCE browser flow as last resort
refreshAndSaveTokenauth.(*AuthProvider)POSTs to Google OAuth2 endpoint, calls applyAuthResult
applyAuthResultauth.(*AuthProvider)Writes refreshed credentials to all three storage layers
GetGrantedScopesauth.(*AuthProvider)Returns scopes from original PKCE authorization
generateStateauthclientGenerates PKCE state parameter
openBrowserauthclientOpens system browser for PKCE flow
performTerminalAuthFlowauthclientFull PKCE flow for terminal/CLI context
validateLoginAndUpdateStatusauthclientValidates login state and updates internal status
LoginWithBrowserauthclientInitiates browser-based login
GetAuthStatusauthclientReturns current authentication status

Credential Management

OAuth2 Client IDs

Three distinct OAuth2 Client ID/Secret pairs exist in the binary:

Consumer (ACTIVE)

FieldValue
Client ID[REDACTED]
Client Secret[REDACTED]
UsageGoogle One / Consumer OAuth2
StatusActive -- used by agy CLI + IDE + Master
ScopesFull scope set (see below)

Enterprise

FieldValue
Client ID[REDACTED]
Client Secret[REDACTED]
UsageGCP / Code Assist enterprise flows
StatusAvailable in binary, not active by default

Legacy (DISCONTINUED)

FieldValue
Client ID[REDACTED]
Client Secret[REDACTED]
Usageopencode-gemini-auth (legacy Antigravity)
StatusDiscontinued -- credential file deleted

OAuth2 Scopes

openid
email
profile
https://www.googleapis.com/auth/userinfo.email
https://www.googleapis.com/auth/userinfo.profile
https://www.googleapis.com/auth/cloud-platform
https://www.googleapis.com/auth/cclog
https://www.googleapis.com/auth/experimentsandconfigs
ScopePurposeSensitivity
openidOpenID ConnectLow
emailUser email accessMedium
profileUser profile accessMedium
userinfo.emailEmail via userinfo endpointMedium
userinfo.profileProfile via userinfo endpointMedium
cloud-platformFull GCP API accessHigh
cclogCorporate loggingLow
experimentsandconfigsFeature flags / A/B experimentsLow

The cloud-platform scope is the most significant -- it grants full Google Cloud Platform API access. This is necessary for Vertex AI backend routing and GCP project validation.

Keychain Storage Format

Structure

Service:  "gemini"
Account:  "antigravity"
Format:   go-keyring-base64:<base64(JSON)>
Auth Method: "consumer"

Decoded JSON Schema

The base64-decoded value is a JSON object:

{
  "access_token": "[REDACTED]",
  "refresh_token": "[REDACTED]",
  "token_type": "Bearer",
  "expiry": "2026-07-22T15:30:00Z",
  "client_id": "[REDACTED]",
  "client_secret": "[REDACTED]",
  "granted_scopes": [
    "openid",
    "email",
    "profile",
    "https://www.googleapis.com/auth/userinfo.email",
    "https://www.googleapis.com/auth/userinfo.profile",
    "https://www.googleapis.com/auth/cloud-platform",
    "https://www.googleapis.com/auth/cclog",
    "https://www.googleapis.com/auth/experimentsandconfigs"
  ]
}
FieldTypeDescription
access_tokenstringCurrent OAuth2 Bearer token (JWT, opaque)
refresh_tokenstringOffline refresh token (persistent, long-lived)
token_typestringAlways "Bearer"
expirystringRFC 3339 timestamp of access_token expiry
client_idstringOAuth2 client ID that obtained this token
client_secretstringOAuth2 client secret
granted_scopesstring[]Scopes granted during PKCE authorization

Keychain Read Timeout

The keychain read has a 1-second timeout. If the macOS Keychain is locked (e.g., screen locked, keychain locked after sleep), the auth chain falls through to the file fallback. This prevents the auth chain from hanging indefinitely.

// Decompiled pattern (simplified)
func keyringAuth() (*Token, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
    defer cancel()
    
    value, err := keyring.GetWithContext(ctx, "gemini", "antigravity")
    if err != nil {
        return nil, err  // Falls through to fileAuth()
    }
    return parseKeyringValue(value)
}

Token Lifecycle

TTL Characteristics

PropertyValue
TypeOAuth2 Bearer Token
TTL~3600 seconds (1 hour)
FormatJWT (opaque to the client)
RefreshVia refresh_token (offline, persistent)
StorageKeychain (primary) + ~/.gemini/ (fallback) + Memory (cache)
timeline
    title Token Lifecycle Timeline
    section Token A
        PKCE Auth : access_token A issued
        Active Use : 0-3600s
    section Token B
        Auto Refresh : access_token B issued
        Active Use : 3600-7200s
    section Token C
        Auto Refresh : access_token C issued
        Active Use : 7200-10800s

Triple Storage Architecture

graph TB
    subgraph "AuthProvider Singleton"
        MC["Memory Cache<br/>access_token + expiry<br/>Fastest, per-process"]
    end

    subgraph "Persistent Storage"
        KC["macOS Keychain<br/>Service: gemini<br/>Account: antigravity<br/>Primary, 1s timeout"]
        FF["File Fallback<br/>~/.gemini/<br/>Secondary, no timeout"]
    end

    MC <-->|"read/write"| KC
    MC <-->|"read/write"| FF
    KC <-.->|"synced via<br/>applyAuthResult"| FF

    style MC fill:#2a4a6b
    style KC fill:#4a6b2a
    style FF fill:#6b4a2a

Token Refresh Mechanics

HTTP Request

POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&client_id=[REDACTED]
&client_secret=[REDACTED]
&refresh_token=[REDACTED]

Response

{
  "access_token": "[REDACTED]",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "openid email profile ..."
}

Note: The refresh response does NOT include a new refresh_token. The original refresh token persists indefinitely (until revoked).

applyAuthResult

After a successful refresh, applyAuthResult() writes to all three storage layers:

sequenceDiagram
    participant Refresh as refreshAndSaveToken
    participant Apply as applyAuthResult
    participant KC as macOS Keychain
    participant FF as File Fallback
    participant MC as Memory Cache

    Refresh->>Apply: new access_token + expiry

    par Write to all stores
        Apply->>KC: keyring.Set("gemini", "antigravity", "go-keyring-base64:" + base64(json))
        Apply->>FF: Write JSON to ~/.gemini/ token file
        Apply->>MC: Update singleton AuthProvider
    end

    Note over KC,FF,MC: All three stores are now synchronized

When Does Refresh Happen?

TriggerMechanismAutomatic?
Any agy CLI commandTrySilentAuth() → TTL checkYes
agy --print <anything>Same auth chainYes
Heartbeat cron jobInvokes agy --printYes (via cron)
SDK API callSDK invokes auth before gRPCYes
First boot after sleepToken may be expired, refreshes on first useYes

There is no background daemon or periodic timer in the agy binary. Refresh only happens when the binary is invoked. This is why the Heartbeat cron is important.

Token Validation

curl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=[REDACTED]" | python3 -m json.tool

Response:

{
  "azp": "[REDACTED]",
  "aud": "[REDACTED]",
  "scope": "openid email profile ...",
  "expires_in": 2847,
  "email": "[REDACTED]",
  "email_verified": "true"
}

Installation IDs

Each JETSKY component has an installation ID used for telemetry and client identification.

ComponentInstallation IDShared With
Antigravity Master[REDACTED]IDE
Antigravity IDE[REDACTED]Master
Antigravity CLI (agy)[REDACTED]None

Key observation: Master and IDE share the same installation ID. This means they appear as a single client to Google's backend. One PKCE authorization covers both applications. The CLI has a separate installation ID for independent telemetry tracking.

Installation ID in Telemetry

The installation ID appears in the ProductEvent protobuf:

message ProductEvent {
  string event_name = 1;
  string api_key = 2;
  string installation_id = 3;
  string ide_name = 4;
  string os = 5;
  string codeium_version = 6;
  int64 timestamp = 7;
  map<string, string> properties = 8;
}

Passive Refresh via Heartbeat

The Heartbeat Protocol exploits the auth chain to keep tokens fresh across the ecosystem.

Mechanism

Running agy --print "<any prompt>" triggers:

  1. TrySilentAuth() is called as a side effect
  2. If token is expired, it is transparently refreshed
  3. The refreshed token is written back to the keychain
  4. ALL other components (Master, IDE, SDK) benefit from the fresh token

Recommended Cron

# Every 45 minutes — well within the 1-hour TTL
*/45 * * * * /Users/alefita/.local/bin/agy --print "[REDACTED]" > /dev/null 2>&1

See sdk-test-deepseek-heartbeat for the complete Heartbeat Protocol including Proto-Unicode sequences.

Timing Analysis

ScenarioLatencySide Effect
Token valid (TTL > 0)~100msNo refresh needed
Token expired (TTL <= 0)~2-3sToken refreshed and written to keychain
Keychain locked~1s timeout + file fallbackFalls through to file auth
Network downVariableRefresh fails, cached token used

PKCE Flow (Interactive Auth)

When both keyring and file auth fail, the system launches a PKCE (Proof Key for Code Exchange) browser flow.

Flow Sequence

sequenceDiagram
    participant CLI as agy CLI
    participant Browser as System Browser
    participant Google as Google OAuth2
    participant KC as macOS Keychain

    CLI->>CLI: generateState() → random state param
    CLI->>CLI: Generate code_verifier + code_challenge (S256)
    CLI->>Browser: openBrowser(PKCE URL)
    Note right of Browser: URL: accounts.google.com/o/oauth2/v2/auth<br/>?response_type=code<br/>&client_id=[REDACTED]<br/>&redirect_uri=urn:ietf:wg:oauth:2.0:oob<br/>&scope=openid+email+profile+...<br/>&state=[random]<br/>&code_challenge=[S256]<br/>&code_challenge_method=S256

    Browser->>Google: User authorizes
    Google-->>Browser: Authorization code
    Browser-->>CLI: Display authorization code (OOB)
    CLI->>Google: Exchange code + code_verifier for tokens
    Google-->>CLI: access_token + refresh_token
    CLI->>KC: applyAuthResult() → write to all stores

Redirect URI

The PKCE flow uses urn:ietf:wg:oauth:2.0:oob (out-of-band) as the redirect URI. This means the authorization code is displayed directly in the browser for the user to copy, rather than being redirected to a local server. This is appropriate for CLI tools that cannot host a local HTTP server.

Terminal Auth Flow

The performTerminalAuthFlow function handles the interactive PKCE flow specifically for terminal contexts:

  1. Generates PKCE parameters (code_verifier, code_challenge)
  2. Prints the authorization URL to stdout
  3. Opens the URL in the system browser
  4. Waits for the user to paste the authorization code
  5. Exchanges the code for tokens
  6. Calls applyAuthResult() to persist

Security Considerations

Fail-Closed Design

The auth chain follows a fail-closed design:

  • Keychain read timeout: 1 second (prevents hanging)
  • Token refresh failure: Returns cached (possibly stale) token
  • All strategies exhausted: Raises error (does NOT silently proceed)

Credential Isolation

  • Consumer credentials are used by default for personal accounts
  • Enterprise credentials are available but not active by default
  • Legacy credentials are discontinued
  • Each credential pair has its own scope set

Keychain Security

The macOS Keychain provides:

  • Encryption at rest (hardware-backed on Apple Silicon)
  • Access control (requires user authorization)
  • Per-application access policies
  • Audit logging

The 1-second timeout on keychain reads is a deliberate trade-off: it prevents the auth chain from blocking when the keychain is locked, at the cost of occasionally falling through to the less secure file fallback.

Refresh Token Lifetime

The refresh_token is an offline token with no explicit expiry. It remains valid until:

  • The user revokes access
  • The OAuth2 client is deactivated
  • Google invalidates it (rare)

This means a single PKCE authorization can sustain token refreshes indefinitely, as long as the refresh token is preserved in the keychain.

Cross-References


Source: Decompiled from agy binary (ARM64, ~99MB) via Ghidra MCP. Functions: authclient.* (12 functions), auth.* (AuthProvider methods). Keychain format verified via security find-generic-password -s gemini -a antigravity.