Hi there! I'm Maneshwar. Right now, I’m building LiveAPI, a first-of-its-kind tool that helps you automatically index API endpoints across all your repositories. LiveAPI makes it easier to discover, understand, and interact with APIs in large infrastructures.
OAuth is one of the most widely adopted standards for securing APIs and delegating user authorization.
But with great flexibility comes great risk. Poor implementation of OAuth can expose your application to phishing, token leakage, and redirection attacks.
This guide covers the essential best practices for securing your OAuth flows.
1. Validate redirect_uri
on the Server
Prevent open redirection and phishing
The redirect_uri
parameter is a critical part of OAuth—it tells the authorization server where to send the user after successful authentication.
But if you let users or clients supply arbitrary redirect_uri
values, attackers can exploit it for open redirect attacks.
Why it matters:
- Attackers can craft malicious URLs that look like legitimate OAuth redirects.
- Users may be redirected to a fake login page or malicious site.
- Sensitive data (like authorization codes or tokens) can be leaked.
Best Practice:
- Only allow pre-registered, exact-match redirect URIs.
- Validate this on the server side, not just in frontend configs.
- Do not allow wildcards (
*
) in production environments.
// List of allowed redirect URIs for a client
var allowedRedirectURIs = map[string]bool{
"https://client.example.com/callback": true,
}
// Validate the redirect URI
func isValidRedirectURI(uri string) bool {
_, ok := allowedRedirectURIs[uri]
return ok
}
Ensure that when handling the OAuth request, your server checks:
redirectURI := r.FormValue("redirect_uri")
if !isValidRedirectURI(redirectURI) {
http.Error(w, "Invalid redirect_uri", http.StatusBadRequest)
return
}
2. Avoid the Implicit Grant Flow (response_type=token
)
Use the Authorization Code flow with PKCE
The Implicit Grant Flow was originally intended for single-page apps (SPAs), where storing secrets is not possible. But it’s no longer recommended due to its security limitations:
- Access tokens are returned directly in the browser (URL fragment).
- Tokens can be leaked via browser history, logs, referrer headers, or network interception.
Using golang.org/x/oauth2
+ custom code for PKCE.
import (
"crypto/rand"
"encoding/base64"
)
func generateCodeVerifier() string {
b := make([]byte, 32)
_, _ = rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
Pass code_verifier
and code_challenge
into your OAuth2 config:
verifier := generateCodeVerifier()
challenge := base64.RawURLEncoding.EncodeToString(sha256Sum(verifier))
authURL := oauth2Config.AuthCodeURL(state, oauth2.SetAuthURLParam("code_challenge", challenge), oauth2.SetAuthURLParam("code_challenge_method", "S256"))
On the token exchange step:
token, err := oauth2Config.Exchange(ctx, code,
oauth2.SetAuthURLParam("code_verifier", verifier))
Safer Alternative:
Use the Authorization Code Flow with PKCE (Proof Key for Code Exchange). PKCE:
- Adds a secure verification step (code_challenge and code_verifier).
- Doesn’t expose access tokens in the browser.
- Doesn’t require storing a client secret.
Even SPAs can use PKCE safely without needing to handle secrets.
3. Use the state
Parameter
Defend against CSRF and response injection
The state
parameter is often overlooked, but it's crucial to secure OAuth authorization flows.
What it does:
- It binds the request to the client’s session.
- Prevents attackers from injecting authorization responses into existing user sessions.
Best Practice:
- Generate a cryptographically secure random string per session.
- Store it on the client (e.g., in a cookie or in-memory).
- On callback, verify that the
state
received matches the one originally sent.
This ensures that the response corresponds to the right user and client session.
func generateState() string {
b := make([]byte, 16)
_, _ = rand.Read(b)
return base64.URLEncoding.EncodeToString(b)
}
// Store state in user session (pseudo code)
session["oauth_state"] = generateState()
When redirecting to the auth server:
authURL := oauth2Config.AuthCodeURL(state)
http.Redirect(w, r, authURL, http.StatusFound)
Validate on callback:
if r.URL.Query().Get("state") != session["oauth_state"] {
http.Error(w, "Invalid state", http.StatusBadRequest)
return
}
4. Validate Scope Requests
Enforce least privilege and avoid over-permissioning
Scopes define what a client app can access on behalf of the user.
But poorly managed scopes can allow overreach—malicious or buggy apps can request more access than needed.
var allowedScopes = map[string][]string{
"client_app_1": {"read", "write"},
}
func isScopeValid(clientID string, requestedScopes []string) bool {
allowed := allowedScopes[clientID]
allowedSet := make(map[string]bool)
for _, s := range allowed {
allowedSet[s] = true
}
for _, s := range requestedScopes {
if !allowedSet[s] {
return false
}
}
return true
}
Parse and validate the scope from the request:
scopes := strings.Split(r.FormValue("scope"), " ")
clientID := r.FormValue("client_id")
if !isScopeValid(clientID, scopes) {
http.Error(w, "Invalid scope request", http.StatusBadRequest)
return
}
Also consider assigning a default scope for fallback:
if len(scopes) == 0 {
scopes = []string{"read"}
}
Best Practice:
- Define default scopes for every client application.
- Maintain a whitelist of allowed scopes per client.
- Reject unauthorized or unexpected scope requests.
Example:
A mobile app requesting admin:write
when it's only supposed to view user data should immediately raise red flags.
Also, display scopes clearly to the user during the consent step to ensure transparency.
TL;DR – Quick Checklist for Secure OAuth Implementation
Practice | Purpose |
---|---|
✅ Validate redirect_uri
|
Stop phishing/open redirect |
✅ Use Authorization Code + PKCE | Avoid token leakage |
✅ Use state parameter |
Prevent CSRF and response hijack |
✅ Validate scopes | Enforce least privilege |
❌ Don’t use Implicit Flow | Too risky; use PKCE instead |
Final Thoughts
OAuth is not inherently secure—it requires deliberate configuration. Most real-world vulnerabilities come from misconfigurations, not flaws in the spec.
Stick to:
- Server-side validations
- Minimum necessary privileges
- Secure flow selection (PKCE)
Use:
- PKCE
- Strict redirect_uri validation
- Session-bound
state
- Scope enforcement
Doing this in Go is straightforward and worth every line of code to avoid expensive security incidents.
LiveAPI helps you get all your backend APIs documented in a few minutes.
With LiveAPI, you can generate interactive API docs that allow users to search and execute endpoints directly from the browser.
If you're tired of updating Swagger manually or syncing Postman collections, give it a shot.
Top comments (0)