Skip to main content
Weaver supports OAuth 2.0 authentication with PKCE (Proof Key for Code Exchange) for secure access to LLM providers that require OAuth flows. This guide covers the implementation details and usage of Weaver’s OAuth system.

Overview

Weaver’s OAuth implementation (pkg/auth/oauth.go) provides:
  • Browser-based flow: Opens browser for user authentication
  • Device code flow: For headless/remote environments
  • PKCE support: Enhanced security for public clients
  • Token refresh: Automatic access token renewal
  • Multi-provider: Support for OpenAI, GitHub Copilot, and custom OAuth providers

OAuth Architecture

Weaver implements the OAuth 2.0 Authorization Code flow with PKCE:
┌─────────┐                                 ┌──────────┐
│ Weaver  │                                 │ Provider │
│ Client  │                                 │  (OAuth) │
└────┬────┘                                 └────┬─────┘
     │                                           │
     │ 1. Generate PKCE codes                    │
     │    (code_verifier, code_challenge)        │
     │                                           │
     │ 2. Open browser with authorization URL    │
     │───────────────────────────────────────────>│
     │                                           │
     │                      3. User authenticates│
     │                         and authorizes    │
     │                                           │
     │ 4. Redirect to callback with auth code    │
     │<───────────────────────────────────────────│
     │                                           │
     │ 5. Exchange code for tokens               │
     │    (with code_verifier)                   │
     │───────────────────────────────────────────>│
     │                                           │
     │ 6. Return access_token + refresh_token    │
     │<───────────────────────────────────────────│
     │                                           │

Authentication Methods

Weaver provides three OAuth authentication flows:

1. Browser-Based Authentication

Best for interactive environments with a web browser.
1

Generate PKCE Codes

From pkg/auth/pkce.go:14:
func GeneratePKCE() (PKCECodes, error) {
    buf := make([]byte, 64)
    if _, err := rand.Read(buf); err != nil {
        return PKCECodes{}, err
    }

    verifier := base64.RawURLEncoding.EncodeToString(buf)
    hash := sha256.Sum256([]byte(verifier))
    challenge := base64.RawURLEncoding.EncodeToString(hash[:])

    return PKCECodes{
        CodeVerifier:  verifier,
        CodeChallenge: challenge,
    }, nil
}
This creates:
  • code_verifier: Random 64-byte string
  • code_challenge: SHA256 hash of verifier
2

Build Authorization URL

From pkg/auth/oauth.go:301:
func buildAuthorizeURL(cfg OAuthProviderConfig, pkce PKCECodes, state, redirectURI string) string {
    params := url.Values{
        "response_type":              {"code"},
        "client_id":                  {cfg.ClientID},
        "redirect_uri":               {redirectURI},
        "scope":                      {cfg.Scopes},
        "code_challenge":             {pkce.CodeChallenge},
        "code_challenge_method":      {"S256"},
        "id_token_add_organizations": {"true"},
        "codex_cli_simplified_flow":  {"true"},
        "state":                      {state},
    }
    return cfg.Issuer + "/oauth/authorize?" + params.Encode()
}
3

Start Local Callback Server

From pkg/auth/oauth.go:64:
mux := http.NewServeMux()
mux.HandleFunc("/auth/callback", func(w http.ResponseWriter, r *http.Request) {
    if r.URL.Query().Get("state") != state {
        resultCh <- callbackResult{err: fmt.Errorf("state mismatch")}
        http.Error(w, "State mismatch", http.StatusBadRequest)
        return
    }

    code := r.URL.Query().Get("code")
    if code == "" {
        errMsg := r.URL.Query().Get("error")
        resultCh <- callbackResult{err: fmt.Errorf("no code received: %s", errMsg)}
        http.Error(w, "No authorization code received", http.StatusBadRequest)
        return
    }

    w.Header().Set("Content-Type", "text/html")
    fmt.Fprint(w, "<html><body><h2>Authentication successful!</h2><p>You can close this window.</p></body></html>")
    resultCh <- callbackResult{code: code}
})

listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", cfg.Port))
Weaver starts a local HTTP server on port 1455 (or configured port) to receive the OAuth callback.
4

Exchange Code for Tokens

From pkg/auth/oauth.go:322:
func exchangeCodeForTokens(cfg OAuthProviderConfig, code, codeVerifier, redirectURI string) (*AuthCredential, error) {
    data := url.Values{
        "grant_type":    {"authorization_code"},
        "code":          {code},
        "redirect_uri":  {redirectURI},
        "client_id":     {cfg.ClientID},
        "code_verifier": {codeVerifier},
    }

    resp, err := http.PostForm(cfg.Issuer+"/oauth/token", data)
    if err != nil {
        return nil, fmt.Errorf("exchanging code for tokens: %w", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("token exchange failed: %s", string(body))
    }

    return parseTokenResponse(body, "openai")
}

2. Device Code Flow

For headless environments (servers, SSH sessions).
1

Request Device Code

From pkg/auth/oauth.go:174:
func LoginDeviceCode(cfg OAuthProviderConfig) (*AuthCredential, error) {
    reqBody, _ := json.Marshal(map[string]string{
        "client_id": cfg.ClientID,
    })

    resp, err := http.Post(
        cfg.Issuer+"/api/accounts/deviceauth/usercode",
        "application/json",
        strings.NewReader(string(reqBody)),
    )
    if err != nil {
        return nil, fmt.Errorf("requesting device code: %w", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    deviceResp, err := parseDeviceCodeResponse(body)
}
2

Display User Code

From pkg/auth/oauth.go:203:
fmt.Printf("\nTo authenticate, open this URL in your browser:\n\n  %s/codex/device\n\nThen enter this code: %s\n\nWaiting for authentication...\n",
    cfg.Issuer, deviceResp.UserCode)
Example output:
To authenticate, open this URL in your browser:

  https://auth.openai.com/codex/device

Then enter this code: ABCD-1234

Waiting for authentication...
3

Poll for Token

From pkg/auth/oauth.go:210:
deadline := time.After(15 * time.Minute)
ticker := time.NewTicker(time.Duration(deviceResp.Interval) * time.Second)
defer ticker.Stop()

for {
    select {
    case <-deadline:
        return nil, fmt.Errorf("device code authentication timed out after 15 minutes")
    case <-ticker.C:
        cred, err := pollDeviceCode(cfg, deviceResp.DeviceAuthID, deviceResp.UserCode)
        if err != nil {
            continue
        }
        if cred != nil {
            return cred, nil
        }
    }
}
Polls every 5 seconds (or server-specified interval) until:
  • User completes authentication
  • 15-minute timeout expires

3. Token Paste (Fallback)

For environments where OAuth is not available. From pkg/auth/token.go:10:
func LoginPasteToken(provider string, r io.Reader) (*AuthCredential, error) {
    fmt.Printf("Paste your API key or session token from %s:\n", providerDisplayName(provider))
    fmt.Print("> ")

    scanner := bufio.NewScanner(r)
    if !scanner.Scan() {
        if err := scanner.Err(); err != nil {
            return nil, fmt.Errorf("reading token: %w", err)
        }
        return nil, fmt.Errorf("no input received")
    }

    token := strings.TrimSpace(scanner.Text())
    if token == "" {
        return nil, fmt.Errorf("token cannot be empty")
    }

    return &AuthCredential{
        AccessToken: token,
        Provider:    provider,
        AuthMethod:  "token",
    }, nil
}

Token Management

Credential Storage

From pkg/auth/oauth.go:365:
type AuthCredential struct {
    AccessToken  string
    RefreshToken string
    ExpiresAt    time.Time
    Provider     string
    AuthMethod   string
    AccountID    string
}

Token Refresh

From pkg/auth/oauth.go:261:
func RefreshAccessToken(cred *AuthCredential, cfg OAuthProviderConfig) (*AuthCredential, error) {
    if cred.RefreshToken == "" {
        return nil, fmt.Errorf("no refresh token available")
    }

    data := url.Values{
        "client_id":     {cfg.ClientID},
        "grant_type":    {"refresh_token"},
        "refresh_token": {cred.RefreshToken},
        "scope":         {"openid profile email"},
    }

    resp, err := http.PostForm(cfg.Issuer+"/oauth/token", data)
    if err != nil {
        return nil, fmt.Errorf("refreshing token: %w", err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("token refresh failed: %s", string(body))
    }

    refreshed, err := parseTokenResponse(body, cred.Provider)
    if err != nil {
        return nil, err
    }
    if refreshed.RefreshToken == "" {
        refreshed.RefreshToken = cred.RefreshToken
    }
    return refreshed, nil
}

JWT Parsing

Extract account information from ID tokens (pkg/auth/oauth.go:385):
func extractAccountID(token string) string {
    claims, err := parseJWTClaims(token)
    if err != nil {
        return ""
    }

    if accountID, ok := claims["chatgpt_account_id"].(string); ok && accountID != "" {
        return accountID
    }

    if accountID, ok := claims["https://api.openai.com/auth.chatgpt_account_id"].(string); ok && accountID != "" {
        return accountID
    }

    if authClaim, ok := claims["https://api.openai.com/auth"].(map[string]interface{}); ok {
        if accountID, ok := authClaim["chatgpt_account_id"].(string); ok && accountID != "" {
            return accountID
        }
    }

    return ""
}

Provider Configuration

OpenAI OAuth

From pkg/auth/oauth.go:29:
func OpenAIOAuthConfig() OAuthProviderConfig {
    return OAuthProviderConfig{
        Issuer:     "https://auth.openai.com",
        ClientID:   "app_EMoamEEZ73f0CkXaXp7hrann",
        Scopes:     "openid profile email offline_access",
        Originator: "codex_cli_rs",
        Port:       1455,
    }
}

GitHub Copilot OAuth

Configure in config.json:
{
  "providers": {
    "github_copilot": {
      "auth_method": "oauth",
      "connect_mode": "grpc"
    }
  }
}

Custom Provider

Define your own OAuth provider:
type OAuthProviderConfig struct {
    Issuer     string  // OAuth server base URL
    ClientID   string  // OAuth client ID
    Scopes     string  // Space-separated scopes
    Originator string  // Optional originator identifier
    Port       int     // Local callback server port
}

customProvider := OAuthProviderConfig{
    Issuer:     "https://oauth.example.com",
    ClientID:   "your-client-id",
    Scopes:     "openid profile api.access",
    Originator: "weaver",
    Port:       1455,
}

Usage Examples

Command-Line Authentication

weaver auth login --provider openai
# Opens browser automatically
# Redirects to http://localhost:1455/auth/callback

Programmatic Usage

import (
    "context"
    "github.com/operatoronline/weaver/pkg/auth"
)

// Browser-based OAuth
cfg := auth.OpenAIOAuthConfig()
cred, err := auth.LoginBrowser(cfg)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("Access Token: %s\n", cred.AccessToken)
fmt.Printf("Expires At: %s\n", cred.ExpiresAt)
fmt.Printf("Account ID: %s\n", cred.AccountID)

// Token refresh
if time.Now().After(cred.ExpiresAt) {
    refreshed, err := auth.RefreshAccessToken(cred, cfg)
    if err != nil {
        log.Fatal(err)
    }
    cred = refreshed
}

Security Considerations

Weaver uses PKCE (RFC 7636) to prevent authorization code interception:
  • code_verifier: Kept secret on client
  • code_challenge: Sent to authorization server
  • code_challenge_method: SHA256 hashing
This prevents attackers from using stolen authorization codes.
From pkg/auth/oauth.go:39:
func generateState() (string, error) {
    buf := make([]byte, 32)
    if _, err := rand.Read(buf); err != nil {
        return "", err
    }
    return hex.EncodeToString(buf), nil
}
64-character random state prevents CSRF attacks.
  • Binds to 127.0.0.1 only (not 0.0.0.0)
  • Shuts down immediately after receiving callback
  • 5-minute timeout to prevent hanging
  • HTML response confirms success
  • Credentials stored in ~/.weaver/auth/
  • File permissions: 0600 (owner read/write only)
  • Refresh tokens enable long-term access
  • Access tokens expire (typically 1 hour)

Troubleshooting

Manually open the URL displayed in terminal:
Open this URL to authenticate:

https://auth.openai.com/oauth/authorize?client_id=...
Or use device code flow:
weaver auth login --provider openai --device-code
Possible causes:
  • Multiple authentication attempts
  • Browser cached old redirect
  • Network proxy interference
Solution: Clear browser cache and retry.
If port 1455 is occupied:
cfg := auth.OAuthProviderConfig{
    // ... other fields
    Port: 1456, // Use different port
}
If refresh token is invalid or expired:
weaver auth logout --provider openai
weaver auth login --provider openai
Re-authenticate to obtain new tokens.

Best Practices

OAuth provides:
  • Shorter-lived access tokens
  • Automatic token refresh
  • Account-level access control
  • Revocation capabilities
  • Never commit credentials to version control
  • Use file permissions 0600 for auth files
  • Rotate credentials regularly
  • Revoke tokens when no longer needed
Always check ExpiresAt before API calls:
if time.Now().After(cred.ExpiresAt) {
    cred, err = auth.RefreshAccessToken(cred, cfg)
    if err != nil {
        // Re-authenticate
    }
}
Use device code flow for:
  • Headless servers
  • SSH sessions
  • Docker containers
  • CI/CD pipelines

Next Steps

Build docs developers (and LLMs) love