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:
- A user submits credentials, often an email and password, to a login endpoint.
- The server finds that user's stored password hash.
- The server compares the submitted password with the hash.
- If the credentials are valid, the server creates evidence that the user is authenticated.
- Later requests include that evidence.
- 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.
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:
Header.Getreads theAuthorizationheader.TrimPrefixremovesBearer, leaving7f3a-example-session.- The server looks up that value in
sessions. - If no matching session exists, it immediately returns HTTP
401 Unauthorized. The protected handler does not run. - If a session exists, the middleware gets its associated
userID. context.WithValuecreates a derived request context containing that ID.next.ServeHTTPcalls the protected handler with the updated request.- 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.
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: trueso the browser sends the cookie only over HTTPS.HttpOnly: trueso JavaScript cannot read the cookie.- A suitable
SameSitesetting 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
| Approach | How the client sends proof | Server-side storage | Useful when | Main consideration |
|---|---|---|---|---|
| Cookie-backed server session | Browser sends a cookie automatically | Yes, session-to-user mapping | Traditional web apps and browser clients | Configure cookie security and handle CSRF |
| Opaque bearer token | Client sends Authorization: Bearer ... | Usually yes, token-to-user mapping | APIs, mobile clients, controlled clients | Keep tokens secret; implement expiration and revocation |
| Signed bearer token | Client sends Authorization: Bearer ... | Not necessarily for each request | Distributed systems needing local validation | Validate signatures and claims; revocation is more complex |
| HTTP Basic authentication | Client sends username and password each request | No session required | Limited internal or temporary cases over HTTPS | Sends reusable credentials repeatedly; rarely ideal for browser apps |
| Term | Purpose |
|---|---|
| Authentication | Establishes the caller's identity. |
| Authorization | Determines whether that identity may perform an action. |
| Password hashing | Safely verifies a password without storing the original value. |
| Encryption | Protects 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.
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/bcryptfor password hashing and comparison. - Use
crypto/randfor 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 Unauthorizedfor missing or invalid credentials. - Use
403 Forbiddenfor an authenticated user without permission. - Use HTTPS in production.
- Cookie sessions should normally use
SecureandHttpOnly. - 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
Authorizationheaders. - 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.
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.
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.