In the previous chapter, Authentication Flow & Server, we successfully performed the OAuth2 "dance" with Google. At the end of that process, we obtained a prize: the Refresh Token.
This token is the "Key to the Kingdom." It allows gogcli to generate new access tokens forever without asking the user to log in again.
The Problem: Where do we put this key?
config.json, anyone who can read that file can steal your identity.The Solution: The Keyring. Operating systems have built-in digital safes:
In this chapter, we will build an abstraction that talks to these secure safes.
We don't want our main code to worry about whether it is running on Windows or a Mac. We want a unified way to say "Save this secret."
We define this contract in internal/secrets/store.go using a Go Interface.
// internal/secrets/store.go
type Store interface {
// Save a token for a specific email
SetToken(client string, email string, tok Token) error
// Retrieve a token
GetToken(client string, email string) (Token, error)
// Delete a token (e.g., during logout)
DeleteToken(client string, email string) error
}
What is happening? This interface promises that whatever storage backend we use (Mac, Windows, or File), it will support these three basic operations.
Keychains usually store simple pairs: a Key (the name) and a Secret (a blob of text or bytes).
However, our Token is a complex Go struct containing the refresh token, creation date, and scopes.
// internal/secrets/store.go
type Token struct {
Email string `json:"email"`
RefreshToken string `json:"-"` // The sensitive part!
Scopes []string `json:"scopes"`
CreatedAt time.Time `json:"created_at"`
}
To fit this struct into the keychain, we must convert it into JSON (text) before saving it.
SetToken ImplementationHere is how we implement the logic to save a token. Note how we convert the data to JSON before handing it to the underlying ring.
// internal/secrets/store.go
func (s *KeyringStore) SetToken(client, email string, tok Token) error {
// 1. Convert the Go struct to JSON bytes
payload, err := json.Marshal(tok)
if err != nil {
return err
}
// 2. Create a standardized item
item := keyring.Item{
Key: fmt.Sprintf("token:%s:%s", client, email),
Data: payload,
Label: "gogcli",
}
// 3. Save it to the OS Keychain
return s.ring.Set(item)
}
The Process:
Token struct into a JSON string.token:default:user@gmail.com so we can find it later.
We use a library called 99designs/keyring. It acts as a universal adapter.
When gogcli starts, it tries to open the keychain. It automatically detects the operating system.
The OpenDefault function handles the connection logic. It's robust enough to handle servers that don't have a screen (headless).
// internal/secrets/store.go
func OpenDefault() (Store, error) {
// Define configuration
cfg := keyring.Config{
ServiceName: "gogcli",
// Allowed backends (Mac, Windows, Linux, File)
AllowedBackends: []keyring.BackendType{
keyring.KeychainBackend,
keyring.WinCredBackend,
keyring.FileBackend,
},
}
// Open the ring
ring, err := keyring.Open(cfg)
return &KeyringStore{ring: ring}, err
}
What happens if you run gogcli on a Linux server without a graphical interface? There is no "GNOME Keyring" there.
If gogcli detects this, it falls back to the File Backend.
GOG_KEYRING_PASSWORD).This ensures that even on a server, your tokens aren't just sitting in a plain text file.
// Internal logic simplified
if isHeadlessLinux {
// If we can't find a desktop keychain...
cfg.AllowedBackends = []keyring.BackendType{keyring.FileBackend}
// We need a password to encrypt the file
cfg.FilePasswordFunc = func(s string) (string, error) {
return "Please enter a password for the keyring file:", nil
}
}
When the user runs gog gmail list, we need the token back to authenticate the request. We reverse the storage process.
// internal/secrets/store.go
func (s *KeyringStore) GetToken(client, email string) (Token, error) {
// 1. Ask the OS for the data
key := fmt.Sprintf("token:%s:%s", client, email)
item, err := s.ring.Get(key)
if err != nil {
return Token{}, err
}
// 2. Convert JSON back to Go struct
var tok Token
if err := json.Unmarshal(item.Data, &tok); err != nil {
return Token{}, err
}
return tok, nil
}
Why is this secure? If a malicious script runs on your computer, it cannot easily get this token. On macOS, for example, the OS will pop up a dialog: "Terminal wants to access the key 'gogcli'. Allow?". Unless the user clicks Allow, the token remains hidden.
In the previous chapter's AuthAddCmd, we used this system in the final step.
RefreshToken.store.SetToken(...).Now, the token is safe in the OS vault.
In this chapter, we learned how to move sensitive data out of our code and configuration files.
Store interface to decouple our code from OS specifics.Now we have the Command Framework (Chapter 1), the Authentication (Chapter 2), and the Secure Storage (Chapter 3).
We have everything we need to actually talk to Google. In the next chapter, we will build the client that uses these tokens to make API requests, handling retries and rate limits automatically.
Generated by Code IQ