Question

Authentication for Go REST APIs: Sessions, Tokens, and Middleware

goauthentication

Question

For a Go application with a RESTful API and a JavaScript front end, what authentication architecture should be used? How can the application securely verify user credentials, maintain an authenticated user across requests, and protect private API endpoints without creating custom cryptography?

Short Answer

By the end of this page, you will understand the building blocks of authentication in Go: password hashing, login verification, sessions or bearer tokens, authentication middleware, and safe handling of browser-based API requests. You will also know how to choose between common approaches based on how your front end communicates with the API.

Concept

Authentication answers one question: who is making this request?

For a typical Go REST API, authentication is usually a flow rather than a single function:

  1. A user submits credentials, often an email and password, to a login endpoint.
  2. The server finds that user's stored password hash.
  3. The server compares the submitted password with the hash.
  4. If the credentials are valid, the server creates evidence that the user is authenticated.
  5. Later requests include that evidence.
  6. Authentication middleware validates the evidence and attaches the user identity to the request.

Two common ways to keep a user authenticated are:

  • Server-side sessions: the server generates a random session ID and stores session data on the server. The browser usually sends the ID back in a secure cookie.
  • Bearer tokens: the client sends a token in an Authorization: Bearer ... header. The server validates it before serving protected endpoints.

A token can be an opaque random value looked up by the server, or a signed token containing claims. Both designs have trade-offs, but neither removes the need for careful expiration, revocation, transport security, and authorization.

Authentication is not authorization. After identifying a user, your application must still decide whether that user may perform an action, such as editing a project or viewing an administrator-only report.

Never design your own password hashing algorithm or invent custom encryption. Use well-reviewed implementations of established algorithms, such as bcrypt, and rely on Go's crypto/rand for security-sensitive random values.

Mental Model

Think of an API as a secure office building.

  • A password is proof that a visitor is the person they claim to be. The receptionist checks it once at login.
  • A password hash is a secure verifier in the receptionist's records. It is not the original password.
  • A session ID or bearer token is a temporary visitor badge issued after the check succeeds.
  • Authentication middleware is the guard at each restricted door. It checks the badge and tells the rest of the application who the visitor is.
  • Authorization is the rule that decides which rooms that particular visitor may enter.

The server should store or validate the badge, but it should never need to ask for the user's password again on every request.

Take Quiz

Syntax and Examples

A protected Go endpoint is commonly implemented with middleware. The middleware validates a credential, then stores the authenticated user ID in the request context.

package main

import (
	"context"
	"net/http"
	"strings"
)

type contextKey string

const userIDKey contextKey = "userID"

func requireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		header := r.Header.Get("Authorization")
		parts := strings.SplitN(header, " ", 2)

		if len(parts) != 2 || parts[0] != "Bearer" || parts[1] != "demo-token" {
			http.Error(w, "authentication required", http.StatusUnauthorized)
			return
		}

		ctx := context.WithValue(r.Context(), userIDKey, "user-42")
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

func profile(w http.ResponseWriter, r *http.Request) {
	userID, _ := r.Context().Value(userIDKey).(string)
	w.Write([]byte("Profile for " + userID))
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("GET /api/profile", requireAuth(http.HandlerFunc(profile)))

	http.ListenAndServe(":8080", mux)
}

A client can call the protected endpoint with:

GET /api/profile HTTP/1.1
Authorization: Bearer demo-token

This example uses a hard-coded token only to demonstrate the request flow. A real application must validate an unpredictable token stored securely, or validate a correctly signed token with expiration and issuer rules.

For passwords, use bcrypt rather than comparing plaintext values:

hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
	// Handle the error.
}

err = bcrypt.CompareHashAndPassword(hash, []byte(submittedPassword))
if err != nil {
	// Password does not match.
}

The stored database value is hash, not password.

Step by Step Execution

Consider this request flow:

func requireAuth(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
		userID, ok := sessions[token]
		if !ok {
			http.Error(w, "authentication required", http.StatusUnauthorized)
			return
		}

		ctx := context.WithValue(r.Context(), userIDKey, userID)
		next.ServeHTTP(w, r.WithContext(ctx))
	})
}

Suppose the client sends:

Authorization: Bearer 7f3a-example-session

Execution proceeds as follows:

  1. Header.Get reads the Authorization header.
  2. TrimPrefix removes Bearer , leaving 7f3a-example-session.
  3. The server looks up that value in sessions.
  4. If no matching session exists, it immediately returns HTTP 401 Unauthorized. The protected handler does not run.
  5. If a session exists, the middleware gets its associated userID.
  6. context.WithValue creates a derived request context containing that ID.
  7. next.ServeHTTP calls the protected handler with the updated request.
  8. The protected handler reads the user ID from the context and can load that user's data.

The early return after http.Error is essential: it prevents unauthenticated requests from reaching the protected endpoint.

Real World Use Cases

Authentication patterns appear in many Go services:

  • Single-page applications: A JavaScript front end signs in through a Go API, then uses a secure cookie or authorization header for later API calls.
  • Mobile applications: A mobile client obtains a short-lived access token and sends it with API requests.
  • Internal services: One backend service authenticates to another using service credentials or a signed service token.
  • Admin dashboards: Middleware identifies the user, while authorization checks ensure the user has an administrator role.
  • Command-line tools: A CLI stores a credential using an appropriate secure local storage mechanism and sends it to the API.
  • Multi-tenant applications: Authentication identifies the person; application logic then determines which organization or tenant they can access.

Regardless of the client, always use HTTPS in production. Without HTTPS, passwords, cookies, and bearer tokens can be intercepted in transit.

Take Quiz

Real Codebase Usage

In real Go codebases, authentication is usually separated into focused components:

  • A user repository loads users and their password hashes from a database.
  • A login handler validates request input, verifies the password hash, and creates a session or token.
  • A session or token service creates credentials, checks expiration, and supports logout or revocation where needed.
  • Middleware performs repeated authentication checks before protected routes.
  • Context values carry small request-scoped values, such as an authenticated user ID, from middleware to handlers.
  • Authorization functions check permissions close to the action being performed.

A typical route setup might look like this:

mux := http.NewServeMux()

mux.HandleFunc("POST /api/login", loginHandler)
mux.HandleFunc("POST /api/logout", logoutHandler)
mux.Handle("GET /api/me", requireAuth(http.HandlerFunc(meHandler)))
mux.Handle("POST /api/projects", requireAuth(http.HandlerFunc(createProjectHandler)))

Use guard clauses in handlers and middleware to keep code flat and readable:

if r.Method != http.MethodPost {
	http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
	return
}

For browser cookie sessions, configure cookie flags deliberately:

  • Secure: true so the browser sends the cookie only over HTTPS.
  • HttpOnly: true so JavaScript cannot read the cookie.
  • A suitable SameSite setting to reduce cross-site request risks.

If your browser automatically sends authentication cookies, plan for CSRF protection on state-changing requests. If a JavaScript application sends bearer tokens manually in headers, plan carefully for XSS because malicious JavaScript may be able to steal tokens stored in page-accessible storage.

Common Mistakes

Storing plaintext passwords

Broken approach:

if submittedPassword == user.Password {
	// Log in
}

Do not store user.Password as the original password. Store a bcrypt hash and use bcrypt.CompareHashAndPassword.

Using predictable session IDs

Broken approach:

sessionID := fmt.Sprintf("%s-%d", userID, time.Now().Unix())

An attacker may guess predictable values. Generate session IDs with crypto/rand and enough random bytes.

Trusting an unverified user ID from the client

Broken approach:

userID := r.Header.Get("X-User-ID")

Any caller can set this header. Derive the identity only after validating a session or token.

Forgetting to stop after an error

Broken approach:

if !valid {
	http.Error(w, "unauthorized", http.StatusUnauthorized)
}
next.ServeHTTP(w, r)

The request still reaches next. Return immediately:

if !valid {
	http.Error(w, "unauthorized", http.StatusUnauthorized)
	return
}

Mixing authentication and authorization

A valid login does not mean the user may modify every resource. Check ownership or roles after authentication.

if project.OwnerID != currentUserID {
	http.Error(w, "forbidden", http.StatusForbidden)
	return
}

Use 401 Unauthorized when authentication is missing or invalid. Use 403 Forbidden when the user is authenticated but lacks permission.

Logging secrets

Do not log passwords, complete bearer tokens, session IDs, or authentication headers. Logs are often widely accessible and retained for a long time.

Omitting expiration and logout behavior

Credentials should expire. Server-side sessions should be removable on logout; a real system may also need to revoke compromised credentials.

Comparisons

ApproachHow the client sends proofServer-side storageUseful whenMain consideration
Cookie-backed server sessionBrowser sends a cookie automaticallyYes, session-to-user mappingTraditional web apps and browser clientsConfigure cookie security and handle CSRF
Opaque bearer tokenClient sends Authorization: Bearer ...Usually yes, token-to-user mappingAPIs, mobile clients, controlled clientsKeep tokens secret; implement expiration and revocation
Signed bearer tokenClient sends Authorization: Bearer ...Not necessarily for each requestDistributed systems needing local validationValidate signatures and claims; revocation is more complex
HTTP Basic authenticationClient sends username and password each requestNo session requiredLimited internal or temporary cases over HTTPSSends reusable credentials repeatedly; rarely ideal for browser apps
TermPurpose
AuthenticationEstablishes the caller's identity.
AuthorizationDetermines whether that identity may perform an action.
Password hashingSafely verifies a password without storing the original value.
EncryptionProtects readable data by transforming it with a key; it is not a password-storage replacement.

For many browser applications, a server-side session in a secure cookie is a straightforward choice. For clients that cannot or should not rely on cookies, a bearer-token design is common. The best choice depends on the client, deployment architecture, logout requirements, and security model.

Take Quiz

Cheat Sheet

  • Authenticate once at login; protect later requests with a session or token.
  • Store password hashes, never plaintext passwords.
  • In Go, use golang.org/x/crypto/bcrypt for password hashing and comparison.
  • Use crypto/rand for session IDs and other security-sensitive random values.
  • Put authentication checks in middleware.
  • Put an authenticated user ID in r.Context() for downstream handlers.
  • Use 401 Unauthorized for missing or invalid credentials.
  • Use 403 Forbidden for an authenticated user without permission.
  • Use HTTPS in production.
  • Cookie sessions should normally use Secure and HttpOnly.
  • Automatically sent cookies require CSRF consideration for changing requests.
  • Bearer tokens must be kept secret and should have expiration.
  • Do not log passwords, cookies, or Authorization headers.
  • Authentication identifies a user; authorization checks what the user can do.

FAQ

Should I write my own authentication system in Go?

You will write application-specific login, session, and authorization code, but do not invent password hashing, token signing, or random-number algorithms. Use established Go packages and established protocols where appropriate.

Should a Go REST API use sessions or JWTs?

Neither is always best. Server-side sessions are often simpler for browser applications and make logout/revocation direct. Signed tokens can be useful when services need to validate credentials without a shared session store. Choose based on your client and operational needs.

Why should passwords be hashed instead of encrypted?

A server only needs to verify a submitted password, not recover the original one. Password hashing is intentionally slow and one-way, which reduces harm if the password database is exposed.

Where should authentication middleware store the current user?

Store a small request-scoped identity, such as a user ID, in context.Context. Handlers can retrieve it from r.Context(). Avoid putting database connections or mutable global state in context values.

What is the difference between HTTP 401 and 403?

Return 401 when the request has no valid authentication. Return 403 when authentication succeeded but the user is not allowed to perform the requested operation.

Are bearer tokens safe in browser local storage?

JavaScript can read local storage, so an XSS vulnerability may expose tokens stored there. Cookie-based sessions with HttpOnly cookies reduce JavaScript access, but then CSRF protection becomes important. Treat storage as part of your threat model.

Do I need HTTPS for local development?

Plain HTTP can be acceptable on a trusted local machine for development, but production authentication traffic must use HTTPS. Secure cookies also require HTTPS in normal production configurations.

Take Quiz

Mini Project

Description

Build a small Go API that accepts a login request, verifies a bcrypt password hash, issues an opaque random bearer token, and uses middleware to protect a profile endpoint. It demonstrates the core separation between login logic and repeated request authentication.

Goal

Create a runnable Go server where a user can log in and use the returned bearer token to access /api/me.

Requirements

Create a POST /api/login endpoint that accepts JSON email and password. Verify the submitted password with bcrypt rather than plaintext comparison. Generate a cryptographically random token after a successful login. Require that token for GET /api/me. Return 401 Unauthorized for missing, invalid, or expired tokens.

Take Quiz

Keep learning

Related questions

View more