The login wall
You are building a Go service that needs to read from a private GitHub repository or call a Stripe API. Hardcoding passwords into your source tree is a security liability. Rotating them manually is a maintenance nightmare. You need a system that hands out temporary, scoped credentials and revokes them automatically. That system is OAuth2.
OAuth2 is not a single library. It is a protocol specification that describes how applications delegate authentication. The Go ecosystem handles it through the golang.org/x/oauth2 package. The package abstracts the HTTP handshakes, token storage, and refresh logic into a single interface. You configure the flow once, and the package attaches valid credentials to every outgoing request.
What OAuth2 actually does
OAuth2 replaces long-lived secrets with short-lived tokens. Think of a hotel key card. The front desk gives you a card that opens your room and the gym. It stops working after checkout. If you lose it, the hotel can deactivate it without changing the locks on every door. OAuth2 works the same way. You trade a client identifier and secret for an access token. The token expires. When it expires, you trade a refresh token for a new access token. The original password never leaves the authorization server.
The protocol defines several flows. Each flow matches a different application shape. A web server uses a different flow than a CLI tool. A background worker uses a different flow than a mobile app. The oauth2 package normalizes these differences behind the TokenSource interface. A TokenSource is just a function that returns a valid token or an error. The HTTP client calls it before every request. If the token is fresh, it uses it. If the token is stale, the source refreshes it transparently.
The minimal token exchange
Here is the simplest way to configure an OAuth2 client and fetch a token. This example uses the client credentials flow, which is common for server-to-server communication.
package main
import (
"context"
"fmt"
"log"
"golang.org/x/oauth2"
)
func main() {
// Client ID and secret come from the provider dashboard.
// Never commit these to version control.
conf := &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
Scopes: []string{"read:repo"},
Endpoint: oauth2.Endpoint{
TokenURL: "https://provider.example.com/oauth/token",
},
}
// Context carries deadlines and cancellation signals.
// It always travels as the first parameter in Go.
ctx := context.Background()
// Exchange credentials for a token.
// The package handles the HTTP POST and JSON unmarshaling.
token, err := conf.ClientCredentials(ctx)
if err != nil {
log.Fatalf("token exchange failed: %v", err)
}
fmt.Println("Access token:", token.AccessToken)
fmt.Println("Expires in:", token.Expiry)
}
The oauth2.Config struct holds the static pieces: identifiers, scopes, and the token endpoint URL. The ClientCredentials method builds the HTTP request, signs it with the client secret, and parses the JSON response into an *oauth2.Token. The token struct contains the access string, an expiration timestamp, and optional refresh credentials. The package does not store the token for you. You must attach it to your HTTP client or cache it in your application state.
How the pieces move
When you call ClientCredentials, the package sends a POST request to the TokenURL. The body contains the grant type, client ID, client secret, and requested scopes. The authorization server validates the credentials. If they match, the server returns a JSON payload with the access token, token type, expiration duration, and sometimes a refresh token. The oauth2 package unmarshals this into a struct and returns it.
The real magic happens when you wrap that token in a TokenSource. The package provides conf.TokenSource(ctx, token). This returns an object that implements the TokenSource interface. The interface has one method: Token() (*Token, error). When your HTTP client needs credentials, it calls Token(). The source checks the expiration timestamp. If the token is still valid, it returns it immediately. If the token is expired or about to expire, the source uses the refresh token to fetch a new one. The entire refresh cycle happens inside the Token() call. Your request logic never sees the expiration logic.
This design keeps your business code clean. You do not write if time.Now().After(token.Expiry) { refresh() } in every handler. You delegate the timing to the package. The package respects context deadlines, so a slow refresh will cancel gracefully instead of hanging your goroutine.
Attaching tokens to HTTP calls
You rarely use tokens directly. You attach them to an http.Client. The oauth2 package provides a helper that wraps the standard library client. Here is how you configure it for a real API call.
package main
import (
"context"
"fmt"
"log"
"net/http"
"golang.org/x/oauth2"
)
func callAPI(ctx context.Context, conf *oauth2.Config, token *oauth2.Token) error {
// Wrap the token in a source that auto-refreshes.
// The source checks expiry before every request.
src := conf.TokenSource(ctx, token)
// Create an HTTP client that uses the token source.
// The client intercepts outgoing requests and adds the Authorization header.
client := conf.Client(ctx, src)
// Send a request to a protected endpoint.
// The client handles token attachment automatically.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.example.com/data", nil)
if err != nil {
return fmt.Errorf("building request: %w", err)
}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
fmt.Println("Request succeeded with valid token")
return nil
}
The conf.Client method returns a standard *http.Client. It looks identical to the one you get from http.DefaultClient. The difference lives in the transport layer. The package injects a custom RoundTripper that calls src.Token() before forwarding the request. It formats the Authorization: Bearer <token> header and attaches it. If the refresh fails, the transport returns an error instead of sending an unauthorized request. Your code only needs to handle the standard http.Client error path.
The community convention for error handling applies here. You wrap errors with fmt.Errorf and %w so callers can unwrap them later. You check if err != nil immediately. The boilerplate is verbose by design. It makes the failure path visible instead of hiding it behind silent retries or swallowed errors.
Where things break
OAuth2 implementations fail in predictable ways. The most common failure is token expiration without a refresh token. Some providers issue single-use access tokens. If your flow does not request the offline_access scope or the equivalent, the refresh field stays empty. When the access token expires, TokenSource returns an error. The HTTP client aborts the request. You must catch the error and re-authenticate the user or restart the flow.
Context cancellation is another frequent trap. If you pass a context with a short deadline to conf.Client, the token refresh might hit the deadline before the authorization server responds. The package returns a context deadline exceeded error. The request never leaves your machine. Always give token refresh enough time. A five-second deadline is usually too short for a network hop plus server validation. Use a separate context for long-running HTTP calls, or set a reasonable timeout like thirty seconds.
You might also run into import confusion. The GOAUTH environment variable configures the go command for private module proxy authentication. It has nothing to do with runtime OAuth2. If you set GOAUTH expecting it to authenticate your HTTP client, the client will still send unauthenticated requests. The go toolchain and your application runtime are completely separate processes. Keep module proxy configuration in your CI pipeline. Keep runtime OAuth2 configuration in your application code.
The compiler will catch structural mistakes early. If you pass a token where a context is expected, you get cannot use token (type *oauth2.Token) as context.Context value in argument. If you forget to import the package, you get undefined: oauth2. If you try to modify a token struct directly instead of using the source, the package ignores your changes because it caches the original reference. Always treat *oauth2.Token as immutable after creation. Let the TokenSource manage the lifecycle.
Goroutine leaks happen when you spawn a background refresh loop but never cancel it. The oauth2 package does not start background goroutines for you. It refreshes synchronously on demand. If you build your own caching layer, you must wire up context cancellation. A leaked refresh goroutine will block your process shutdown and exhaust file descriptors. Trust the synchronous design. Run it through every long-lived call site.
Picking the right flow
OAuth2 defines multiple grant types. Each one matches a specific deployment pattern. Choose the flow that matches your application architecture.
Use the client credentials flow when your service communicates with another service and no human user is involved. Use the authorization code flow when a web application needs to act on behalf of a logged-in user. Use the device authorization flow when a user must approve access on a separate screen, like a smart TV or a CLI tool. Use the password credentials flow only when you fully control both the client and the server and cannot use a redirect-based flow. Use plain API keys when the provider does not support OAuth2 and the resource does not require scoped delegation.
The flow you pick determines which oauth2.Config fields you populate. It also determines whether you receive a refresh token. Client credentials often omit refresh tokens. Authorization code almost always includes them. Read the provider documentation carefully. The oauth2 package will not guess the correct grant type for you.
Where to go next
- How to Create a gRPC Server in Go
- How to Use ResponseWriter in Go
- How to Use gRPC Health Checking in Go
OAuth2 is delegation, not magic. Configure the flow, attach the source, respect the context. Let the package handle the handshake.