Authentication
Best practices for token generation, storage, rotation, and security
Token Lifecycle
The Octopus Cards API issues two tokens on login:
| Token | Lifetime | Purpose |
|---|---|---|
| Access token | 1 hour | Authenticates every API request |
| Refresh token | 7 days | Obtains a new token pair without re-entering credentials |
Design your integration around this lifecycle. Never treat tokens as long-lived secrets.
Generate Tokens Once, Reuse Until Expiry
Call /auth/login once at startup (or on first request), then reuse the access token for its full lifetime. Do not authenticate on every request — this wastes time and increases load on both sides.
// Store the token and its expiry after login
type AuthState struct {
AccessToken string
RefreshToken string
ExpiresAt time.Time
}
func (a *AuthState) Token() string {
// Proactively refresh 5 minutes before expiry
if time.Until(a.ExpiresAt) < 5*time.Minute {
a.refresh()
}
return a.AccessToken
}Proactive Refresh
Do not wait for a 401 to refresh. Instead, check access_expires_at from the login response and refresh before the token expires. This avoids failed requests and retry logic.
A good pattern:
- Parse
access_expires_atfrom the login/refresh response - Schedule a refresh for ~5 minutes before that time
- Swap in the new token atomically
func (a *AuthState) refresh() error {
body, _ := json.Marshal(map[string]string{
"refresh_token": a.RefreshToken,
})
resp, err := http.Post("{{host}}/auth/refresh", "application/json", bytes.NewReader(body))
if err != nil {
return err
}
defer resp.Body.Close()
var result struct {
Data struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
AccessExpiresAt string `json:"access_expires_at"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)
a.AccessToken = result.Data.AccessToken
a.RefreshToken = result.Data.RefreshToken
a.ExpiresAt, _ = time.Parse(time.RFC3339, result.Data.AccessExpiresAt)
return nil
}Token Storage
Never log tokens, commit them to source control, or store them in plain text config files.
Server-side integrations:
- Store tokens in memory only. They are short-lived by design — persist the credentials (API key + secret), not the tokens.
- If you must persist tokens across restarts, use an encrypted store (e.g. a secrets manager, encrypted database column, or OS keychain).
- Never write tokens to disk in plain text, log files, or environment variable dumps.
Environment variables for credentials:
# Store credentials, not tokens
export OCTOPUS_API_KEY="your_api_key"
export OCTOPUS_API_SECRET="your_api_secret"Refresh Token Security
The refresh token is more sensitive than the access token — it can generate new access tokens for up to 7 days.
- Store refresh tokens separately from access tokens where possible (e.g. different memory scope, different encryption key).
- Never send the refresh token in API request headers. It is only used in the body of
POST /auth/refresh. - Rotate on every refresh — when you call
/auth/refresh, the old refresh token is automatically revoked and a new one is issued. Always store the new one. - If a refresh token is compromised, call
/auth/logoutimmediately to revoke all tokens for your client.
Concurrency
If your application runs multiple threads or processes making API calls:
- Share a single token across all threads rather than logging in from each one. Multiple logins create multiple active token sets and waste resources.
- Synchronize refresh — use a mutex or similar mechanism to ensure only one thread refreshes at a time. Other threads should wait for the new token.
type SafeAuth struct {
mu sync.RWMutex
state AuthState
}
func (s *SafeAuth) Token() string {
s.mu.RLock()
if time.Until(s.state.ExpiresAt) > 5*time.Minute {
defer s.mu.RUnlock()
return s.state.AccessToken
}
s.mu.RUnlock()
s.mu.Lock()
defer s.mu.Unlock()
// Double-check: another goroutine may have refreshed
if time.Until(s.state.ExpiresAt) > 5*time.Minute {
return s.state.AccessToken
}
s.state.refresh()
return s.state.AccessToken
}IP Whitelisting
For production integrations, configure IP whitelisting in your dashboard. This restricts API access to specific CIDR ranges and prevents stolen tokens from being used outside your infrastructure.
- Whitelist the egress IPs of your servers, not broad ranges.
- If you use a cloud provider, whitelist the NAT gateway or static IP range.
- Test your whitelist in sandbox first — the same restriction applies to login, refresh, and all protected endpoints.
Logout on Shutdown
When your application shuts down gracefully, call POST /auth/logout to revoke all active tokens. This is especially important for:
- Deployment rollouts (old instances should clean up)
- CI/CD test runs (avoid token accumulation)
- Credential rotation (logout before switching to new credentials)
Checklist
- Login once at startup, reuse the access token
- Refresh proactively before expiry (not after a
401) - Store tokens in memory, credentials in a secrets manager
- Synchronize token refresh across threads
- Enable IP whitelisting for production
- Logout on graceful shutdown
- Never log or expose tokens in error messages