The OAuth2 dance in Go
You are building a dashboard that needs to read data from a third-party API. The API refuses to talk to you unless you present a valid OAuth2 token. You have the client ID and client secret from the provider. Now you need to guide a user through a login, capture the authorization code, swap it for a token, and make a request. Or perhaps you are writing a background service that needs a machine-to-machine token to access resources without a user.
Go does not include OAuth2 in the standard library. The ecosystem relies on golang.org/x/oauth2, a package maintained by the Go team that handles the protocol details, token refresh, and HTTP client integration. This package turns a multi-step protocol into a few struct methods and a reusable token source.
Concept: Tokens, not passwords
OAuth2 is a delegation protocol. Your application never sees the user's password. Instead, the user grants your app permission to access specific resources, and the provider hands your app a token. The token is a string that proves your app has permission. You attach the token to requests, and the provider checks it.
Think of the token like a hotel key card. The hotel (provider) gives the guest (user) a card. The guest can use the card to open their room (resource). The card has an expiration date. If the guest loses the card, the hotel can cancel it. The front desk never gives the guest the master key to the whole building.
The "flow" is the sequence of steps to get the token. The most common flow is Authorization Code. The user clicks a button, gets redirected to the provider, logs in, approves access, and gets redirected back to your app with a code. Your app sends that code to the provider along with your secret. The provider returns a token.
Another common flow is Client Credentials. This is for machine-to-machine communication. Your app sends its client ID and secret directly to the provider. The provider returns a token. No user is involved.
Minimal example: Exchanging a code
Here is the core operation: define the configuration, exchange the code, and get the token. The oauth2.Config struct holds the provider's endpoints and your credentials.
package main
import (
"context"
"fmt"
"log"
"golang.org/x/oauth2"
)
// ExchangeCode demonstrates the core step of trading an authorization code for a token.
func ExchangeCode(ctx context.Context, config *oauth2.Config, code string) (*oauth2.Token, error) {
// Exchange sends the code and client secret to the provider.
// The provider validates the code and returns a token.
token, err := config.Exchange(ctx, code)
if err != nil {
// Exchange fails if the code is expired, invalid, or the redirect URI mismatches.
return nil, fmt.Errorf("exchange: %w", err)
}
return token, nil
}
func main() {
// Config holds the provider's endpoints and your credentials.
// ClientID and ClientSecret come from the provider's developer console.
config := &oauth2.Config{
ClientID: "your-client-id",
ClientSecret: "your-client-secret",
RedirectURL: "http://localhost:8080/callback",
Scopes: []string{"read:data"},
Endpoint: oauth2.Endpoint{
AuthURL: "https://provider.com/oauth/authorize",
TokenURL: "https://provider.com/oauth/token",
},
}
ctx := context.Background()
token, err := ExchangeCode(ctx, config, "authorization-code-from-callback")
if err != nil {
log.Fatalf("failed to exchange code: %v", err)
}
// Token.AccessToken is the string you attach to requests.
fmt.Printf("Got token: %s\n", token.AccessToken)
}
The Exchange method handles the HTTP request to the token endpoint. It posts the code, client ID, client secret, and redirect URI. The provider responds with a JSON object containing the access token, refresh token, and expiration time. The method parses the JSON and returns an *oauth2.Token.
If the code is invalid, the provider returns an error response. The Exchange method parses the error and returns it. You get a plain error string like oauth2: cannot fetch token: 400 Bad Request. The compiler rejects the program with undefined: oauth2 if you forget to import the package.
How the TokenSource works
Tokens expire. An access token might be valid for one hour. After that, the provider rejects requests. OAuth2 supports refresh tokens. A refresh token is a long-lived credential that lets you get a new access token without asking the user to log in again.
Writing refresh logic manually is tedious. You need to check expiration, make a request, handle errors, and update the token. The oauth2 package solves this with TokenSource.
A TokenSource is an interface with one method: Token() (*oauth2.Token, error). When you call Token, the source returns a valid token. If the current token is expired, the source automatically uses the refresh token to get a new one. You don't write the refresh logic. The source handles it.
Here is how you create a client that uses a token source.
package main
import (
"context"
"net/http"
"golang.org/x/oauth2"
)
// NewClient shows how to create an HTTP client that automatically attaches tokens.
func NewClient(ctx context.Context, config *oauth2.Config, token *oauth2.Token) *http.Client {
// TokenSource wraps the token and handles refresh logic automatically.
// When the token expires, the source fetches a new one using the refresh token.
ts := config.TokenSource(ctx, token)
// ClientFromTokenSource builds an HTTP client that uses the token source.
// Every request gets a fresh or valid token in the Authorization header.
return oauth2.NewClient(ctx, ts)
}
The TokenSource is thread-safe. Multiple goroutines can call Token concurrently. The source ensures only one refresh request happens at a time. This prevents race conditions where multiple goroutines try to refresh simultaneously.
The NewClient function returns an *http.Client. You use this client like any other HTTP client. The client intercepts requests, gets a token from the source, and adds the Authorization: Bearer <token> header. You never touch the token manually.
Context is plumbing. Pass it through every call. The TokenSource respects context cancellation. If the context is cancelled, the source stops refreshing and returns an error. This lets you shut down the client cleanly.
Realistic example: HTTP handlers and callbacks
Real applications run in HTTP handlers. You need a login handler that redirects the user and a callback handler that processes the code. You also need to protect against CSRF attacks using a state parameter.
The state parameter is a random string. You generate it, store it in a cookie, and send it to the provider. The provider echoes the state back in the callback. You verify that the state matches the cookie. This ensures the callback request comes from your app and not an attacker.
Here is a callback handler that exchanges the code and stores the token.
package main
import (
"crypto/rand"
"encoding/hex"
"net/http"
"golang.org/x/oauth2"
)
// CallbackHandler processes the authorization code from the provider.
func CallbackHandler(w http.ResponseWriter, r *http.Request, config *oauth2.Config) {
// Retrieve the state from the cookie to verify the request.
stateCookie, err := r.Cookie("oauthstate")
if err != nil {
http.Error(w, "State cookie not found", http.StatusBadRequest)
return
}
// Verify the state matches to prevent CSRF attacks.
// If the states don't match, reject the request immediately.
if r.URL.Query().Get("state") != stateCookie.Value {
http.Error(w, "State mismatch", http.StatusForbidden)
return
}
// Exchange the code for a token.
// Exchange fails if the code is expired or the redirect URI mismatches.
token, err := config.Exchange(r.Context(), r.URL.Query().Get("code"))
if err != nil {
http.Error(w, "Failed to exchange code", http.StatusInternalServerError)
return
}
// Store the token securely.
// In production, save the token to a database or encrypted session.
// For this example, we just acknowledge success.
w.WriteHeader(http.StatusOK)
w.Write([]byte("Authenticated. Token stored."))
}
// generateState creates a random hex string for CSRF protection.
func generateState() string {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
panic("failed to generate random bytes")
}
return hex.EncodeToString(b)
}
The handler checks the state cookie. If the cookie is missing or the state doesn't match, the handler rejects the request. This prevents attackers from forcing a user to exchange a malicious code.
The handler calls Exchange to get the token. If the exchange fails, the handler returns an error. In production, you would log the error and show a user-friendly message.
The handler stores the token. You need to persist the token so you can use it later. Save the token to a database, a session store, or encrypted storage. Do not store tokens in plain text cookies. Tokens are secrets.
Convention aside: receiver names are usually one or two letters matching the type. Use (h *Handler) Callback(w http.ResponseWriter, r *http.Request) instead of (this *Handler). This keeps method signatures short and readable.
Pitfalls and errors
OAuth2 implementations have common failure modes. Understanding these helps you debug quickly.
Redirect URI mismatches are the most frequent error. The RedirectURL in your config must match the redirect URI registered with the provider exactly. Trailing slashes matter. http://localhost:8080/callback is different from http://localhost:8080/callback/. The provider rejects the code with redirect_uri_mismatch. The runtime returns oauth2: cannot fetch token: 400 Bad Request when the redirect URI is wrong.
CSRF attacks happen when you skip the state parameter. An attacker can craft a link that forces a user to exchange a code. The attacker gets the token. Always use state. Generate it, verify it, discard it.
Token storage leaks occur when you store tokens insecurely. Tokens grant access to user data. If an attacker steals a token, they can act as the user. Store tokens encrypted. Rotate tokens regularly. Treat tokens like passwords.
Goroutine leaks happen when you create a token source and never cancel the context. The source might hold resources. Always pass a context that can be cancelled. Use context.WithCancel and call the cancel function when the client is done.
The compiler complains with cannot use x (type string) as type *oauth2.Token in argument if you pass the wrong type to a function. Go's type system catches these errors at compile time. Trust the compiler.
Tokens expire. Refresh them or re-authenticate. If the refresh token is revoked, the user must log in again. Handle this case gracefully.
Decision: Which flow and package
Choose the right tool for your scenario. OAuth2 has multiple flows and several Go packages.
Use the golang.org/x/oauth2 package when you need a robust client for standard OAuth2 flows like Authorization Code or Client Credentials. Use the golang.org/x/oauth2/google subpackage when integrating with Google APIs to leverage pre-configured endpoints and scopes. Use a dedicated OpenID Connect library when you need to parse ID tokens, validate signatures, and extract user claims. Use raw HTTP requests only when you are implementing a proprietary flow that deviates from the OAuth2 specification.
Use the Authorization Code flow when a user interacts with your application and grants access. Use the Client Credentials flow when a service accesses resources on its own behalf. Use the Device Authorization flow when the user cannot easily type a URL, such as on a smart TV or IoT device. Use the Implicit flow only when you have no backend and cannot keep a secret, though modern security guidance discourages this flow.