DEV Community

Cover image for Securing OAuth: Best Practices for Safer Authorization Flows
Athreya aka Maneshwar
Athreya aka Maneshwar

Posted on

Securing OAuth: Best Practices for Safer Authorization Flows

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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"))
Enter fullscreen mode Exit fullscreen mode

On the token exchange step:

token, err := oauth2Config.Exchange(ctx, code,
    oauth2.SetAuthURLParam("code_verifier", verifier))
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

When redirecting to the auth server:

authURL := oauth2Config.AuthCodeURL(state)
http.Redirect(w, r, authURL, http.StatusFound)
Enter fullscreen mode Exit fullscreen mode

Validate on callback:

if r.URL.Query().Get("state") != session["oauth_state"] {
    http.Error(w, "Invalid state", http.StatusBadRequest)
    return
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Also consider assigning a default scope for fallback:

if len(scopes) == 0 {
    scopes = []string{"read"}
}
Enter fullscreen mode Exit fullscreen mode

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.

LiveAPI Demo

If you're tired of updating Swagger manually or syncing Postman collections, give it a shot.

Top comments (0)