Overview
switchAILocal supports multiple authentication methods across different provider types. The Auth Manager orchestrates credential lifecycle, automatic refresh, and secure storage.
Authentication Methods
API Keys Static credentials configured in config.yaml
OAuth 2.0 Interactive login flows with automatic token refresh
Service Accounts Long-lived credentials for automation
API Key Authentication
The simplest authentication method using static API keys.
Configuration
# Gemini API
gemini-api-key :
- api-key : "AIzaSy..."
prefix : "google"
base-url : "https://generativelanguage.googleapis.com"
# Claude API
claude-api-key :
- api-key : "sk-ant-..."
models :
- name : "claude-3-5-sonnet-20241022"
# OpenAI API
codex-api-key :
- api-key : "sk-proj-..."
base-url : "https://api.openai.com/v1"
API keys are read at startup and hot-reloaded when config.yaml changes.
Storage
API keys from config are stored in-memory only:
type Auth struct {
ID string
Provider string
Credentials map [ string ] string // {"api_key": "sk-..."}
Metadata map [ string ] any // Source, base URL, etc.
}
API keys in config.yaml are never persisted to the auth directory. Only OAuth tokens are saved.
OAuth 2.0 Authentication
Interactive login flows for providers like Gemini CLI, Codex, and Claude.
OAuth Providers
Gemini CLI (Google) OAuth with device flow and token refresh
Codex (OpenAI) OAuth for ChatGPT Plus accounts switchAILocal -codex-login
Claude (Anthropic) OAuth for Claude Pro accounts switchAILocal -claude-login
Qwen OAuth for Qwen services switchAILocal -qwen-login
Login Flow
The OAuth flow is managed by cmd/server/main.go:
Implementation
The Gemini authenticator (sdk/auth/gemini.go) implements the device flow: type GeminiAuthenticator struct {
clientID string
clientSecret string
scopes [] string
}
func ( a * GeminiAuthenticator ) Login ( ctx , cfg , opts ) ( * TokenData , error ) {
// 1. Request device code
config := & oauth2 . Config {
ClientID : geminiOAuthClientID ,
ClientSecret : geminiOAuthClientSecret ,
Endpoint : google . Endpoint ,
Scopes : [] string {
"https://www.googleapis.com/auth/cloud-platform" ,
"https://www.googleapis.com/auth/userinfo.email" ,
},
}
// 2. Display code to user
deviceAuth , err := config . DeviceAuth ( ctx )
fmt . Printf ( "Visit: %s \n " , deviceAuth . VerificationURI )
fmt . Printf ( "Code: %s \n " , deviceAuth . UserCode )
// 3. Poll for token
token , err := config . DeviceAccessToken ( ctx , deviceAuth )
// 4. Return token data
return & TokenData {
AccessToken : token . AccessToken ,
RefreshToken : token . RefreshToken ,
Expiry : token . Expiry ,
}, nil
}
Token Storage
OAuth tokens are persisted in the auth directory:
~ /.switchailocal/
├── auths/
│ ├── gemini_1234abcd.json # Gemini OAuth token
│ ├── codex_5678efgh.json # Codex OAuth token
│ └── claude_9012ijkl.json # Claude OAuth token
└── config.yaml # Not stored here
Token file format:
{
"id" : "gemini_1234abcd" ,
"provider" : "geminicli" ,
"credentials" : {
"access_token" : "ya29.a0..." ,
"refresh_token" : "1//0g..." ,
"token_type" : "Bearer"
},
"metadata" : {
"expiry" : "2026-03-09T15:30:00Z" ,
"email" : "user@gmail.com" ,
"project_id" : "my-project-123"
},
"status" : "active" ,
"created_at" : "2026-03-09T14:30:00Z" ,
"updated_at" : "2026-03-09T14:30:00Z"
}
Tokens are stored with 600 permissions (owner read/write only) for security.
Token Refresh
OAuth tokens are automatically refreshed before expiration.
Auto-Refresh System
The Auth Manager runs a background refresh loop:
func ( m * Manager ) StartAutoRefresh ( parent context . Context , interval time . Duration ) {
go func () {
ticker := time . NewTicker ( refreshCheckInterval ) // 5 seconds
defer ticker . Stop ()
for {
select {
case <- ctx . Done ():
return
case <- ticker . C :
m . checkRefreshes ( ctx )
}
}
}()
}
Refresh Decision
The system decides whether to refresh based on multiple factors: func ( m * Manager ) shouldRefresh ( a * Auth , now time . Time ) bool {
// Don't refresh if disabled
if a . Disabled {
return false
}
// Don't refresh if recently attempted
if ! a . NextRefreshAfter . IsZero () && now . Before ( a . NextRefreshAfter ) {
return false
}
// Get token expiry
expiry , hasExpiry := a . ExpirationTime ()
// Refresh if expired or expiring soon
if hasExpiry && ! expiry . IsZero () {
lead := ProviderRefreshLead ( a . Provider , a . Runtime )
if lead != nil && time . Until ( expiry ) <= * lead {
return true // Refresh within lead time
}
}
return false
}
Refresh lead times by provider:
Gemini : 5 minutes before expiry
Codex : 10 minutes before expiry
Claude : 15 minutes before expiry
Refresh Execution
func ( m * Manager ) refreshAuth ( ctx context . Context , id string ) {
auth := m . auths [ id ]
executor := m . executors [ auth . Provider ]
// Call provider-specific refresh
updated , err := executor . Refresh ( ctx , auth )
if err != nil {
// Mark refresh failed, retry later
auth . NextRefreshAfter = now . Add ( refreshFailureBackoff ) // 5 minutes
return
}
// Update with new tokens
updated . LastRefreshedAt = now
updated . NextRefreshAfter = time . Time {}
m . Update ( ctx , updated )
}
Refresh happens in the background without blocking requests. If a token expires during a request, the request fails and triggers immediate refresh.
Credential Lifecycle
Registration
Credentials are registered during initialization:
func ( m * Manager ) Register ( ctx context . Context , auth * Auth ) ( * Auth , error ) {
if auth . ID == "" {
auth . ID = uuid . NewString ()
}
m . mu . Lock ()
m . auths [ auth . ID ] = auth . Clone ()
m . mu . Unlock ()
// Persist to storage
m . persist ( ctx , auth )
// Notify hooks
m . hook . OnAuthRegistered ( ctx , auth . Clone ())
return auth . Clone (), nil
}
State Tracking
Each credential tracks its operational state:
type Auth struct {
ID string
Provider string
Status Status // Active, Error, Disabled
Unavailable bool // Currently unavailable
NextRetryAfter time . Time // When to retry
Quota QuotaState // Quota status
LastError * Error // Last error
ModelStates map [ string ] * ModelState // Per-model state
}
type Status string
const (
StatusActive Status = "active"
StatusError Status = "error"
StatusDisabled Status = "disabled"
)
Result Marking
Execution results update credential state:
func ( m * Manager ) MarkResult ( ctx context . Context , result Result ) {
auth := m . auths [ result . AuthID ]
if result . Success {
// Clear errors and cooldowns
resetModelState ( auth . ModelStates [ result . Model ], now )
auth . Status = StatusActive
auth . LastError = nil
} else {
// Mark as failed based on error type
switch result . Error . HTTPStatus {
case 401 : // Unauthorized
auth . NextRetryAfter = now . Add ( 30 * time . Minute )
auth . Status = StatusError
case 429 : // Rate limit
auth . Quota . Exceeded = true
auth . NextRetryAfter = now . Add ( quotaCooldown )
case 500 , 502 , 503 , 504 : // Server error
auth . NextRetryAfter = now . Add ( 1 * time . Minute )
}
}
m . persist ( ctx , auth )
m . hook . OnResult ( ctx , result )
}
Storage Backends
Flexible storage for different deployment scenarios.
File Store (Default)
Local filesystem storage:
type FileTokenStore struct {
baseDir string // ~/.switchailocal
}
func ( s * FileTokenStore ) Save ( ctx , auth ) ( * Auth , error ) {
path := filepath . Join ( s . baseDir , "auths" , auth . ID + ".json" )
data , _ := json . MarshalIndent ( auth , "" , " " )
return auth , os . WriteFile ( path , data , 0600 )
}
Postgres Store
Database-backed storage for multi-node deployments:
export PGSTORE_DSN = "postgres://user:pass@localhost/switchai"
export PGSTORE_SCHEMA = "public"
CREATE TABLE auths (
id TEXT PRIMARY KEY ,
provider TEXT NOT NULL ,
credentials JSONB NOT NULL ,
metadata JSONB,
status TEXT ,
updated_at TIMESTAMP
);
Git Store
Version-controlled storage with remote sync:
export GITSTORE_GIT_URL = "https://github.com/user/switchai-auth.git"
export GITSTORE_GIT_USERNAME = "user"
export GITSTORE_GIT_TOKEN = "ghp_..."
Automatic commit and push on credential changes.
Object Store
S3-compatible storage for cloud deployments:
export OBJECTSTORE_ENDPOINT = "s3.amazonaws.com"
export OBJECTSTORE_BUCKET = "switchai-auth"
export OBJECTSTORE_ACCESS_KEY = "AKIA..."
export OBJECTSTORE_SECRET_KEY = "..."
All storage backends implement the same Store interface, making them interchangeable.
Security Considerations
Credential Protection
File Permissions : Auth files use 0600 (owner read/write only)
Path Validation : Prevents path traversal attacks
Error Sanitization : Strips credentials from error messages
Memory Clearing : Overwrites sensitive data after use
// Path validation
func validateFilePath ( path string ) error {
if strings . Contains ( path , ".." ) || strings . Contains ( path , "~" ) {
return fmt . Errorf ( "invalid file path: path traversal" )
}
// Check for null bytes
// Validate absolute paths
}
// Error sanitization
func sanitizeError ( err error , context string ) error {
errStr := err . Error ()
sensitivePatterns := [] * regexp . Regexp {
regexp . MustCompile ( `(://[^:@]+):([^@]+)@` ),
regexp . MustCompile ( `\b(password|secret|token|key)=[^\s]+` ),
}
for _ , pattern := range sensitivePatterns {
errStr = pattern . ReplaceAllString ( errStr , "$1:***@" )
}
return fmt . Errorf ( " %s : %s " , context , errStr )
}
// Permission check
func checkFilePermissions ( filePath string ) error {
fileInfo , _ := os . Stat ( filePath )
mode := fileInfo . Mode ()
if mode . Perm () & 0077 != 0 {
return fmt . Errorf ( "insecure permissions" )
}
}
Best Practices
Use Environment Variables Store sensitive values in .env files, not committed to version control
Rotate Credentials Periodically rotate API keys and refresh OAuth tokens
Restrict Permissions Ensure auth directory and config files have proper permissions
Monitor Access Enable logging to track authentication attempts
Troubleshooting
Token Expired
# Force refresh
switchAILocal -login # Re-authenticate
Invalid API Key
# Check config.yaml for typos
gemini-api-key :
- api-key : "AIzaSy..." # Verify key is correct
Permission Denied
# Fix permissions
chmod 600 ~/.switchailocal/auths/ * .json
chmod 700 ~/.switchailocal/auths
Multiple Accounts
# Login with different account
switchAILocal -login -project_id my-other-project
Next Steps