Understanding API Authentication: Stateful vs. Stateless Tokens in Golang
API authentication is a crucial aspect of web development, ensuring that only authorized users can access protected resources and perform actions on an application's API.
Two common approaches to API authentication are stateful and stateless token-based methods. In this article, we explore the key differences between these two methods and how they can be implemented in Golang.
Stateful Tokens:
Stateful tokens require the server to store Token information on the server-side. These tokens are typically in the form of session IDs, which are unique identifiers associated with each user's Token. When a user logs in, the server generates a Token and stores user information on the server-side using this Token as a key. The Token then passed back to the client and is sent along with each subsequent request to the server. This allows the server to identify the user and retrieve the associated Token data.
Take a look at the following example of a stateful token:
package main
import (
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"time"
)
var secretKey = []byte("your-secret-key")
var tokenDuration = time.Hour * 24 // Token validity duration, change as needed
// Simulated database to store token-related information
var tokensDB = map[string]TokenData{} // Token Identifier -> TokenData mapping
// TokenData struct to hold token-related information
type TokenData struct {
TokenHash string // Hashed token
Expiration time.Time
UserID int
Abilities string // Store abilities as a string in the server
}
// Simulated user database
var users = map[int]User{
123: {ID: 123, Username: "john_doe"},
456: {ID: 456, Username: "jane_smith"},
}
// User struct to hold user-related information
type User struct {
ID int
Username string
}
// Function to generate a new random identifier (token)
func generateRandomToken() string {
const tokenLength = 32
b := make([]byte, tokenLength)
if _, err := rand.Read(b); err != nil {
panic(err) // Handle error if cryptographically secure random number generation fails
}
return base64.URLEncoding.EncodeToString(b)
}
// Function to hash the token identifier using SHA-256
func hashToken(token string) string {
// Create a new HMAC-SHA256 hasher using the secret key
h := hmac.New(sha256.New, secretKey)
// Write the token bytes to the hasher
h.Write([]byte(token))
// Get the resulting hash and return it as a hexadecimal string
return hex.EncodeToString(h.Sum(nil))
}
// Function to generate a new token and store token-related information on the server
func generateToken(userID int, abilities string) (string, error) {
// Generate a new random identifier (token)
token := generateRandomToken()
// Hash the token identifier using SHA-256
tokenHash := hashToken(token)
// Store the token hash and its expiration time in the tokensDB map
tokensDB[tokenHash] = TokenData{
TokenHash: tokenHash,
Expiration: time.Now().Add(tokenDuration),
UserID: userID,
Abilities: abilities, // Store the abilities string on the server
}
// Return the raw token identifier to the user
return token, nil
}
func apiDataHandler(w http.ResponseWriter, r *http.Request) {
// Extract the token from the request header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse the token
token := authHeader[len("Bearer "):]
// Hash the token identifier using SHA-256
tokenHash := hashToken(token)
// Lookup the token information in the tokensDB map based on the hashed token
tokenData, found := tokensDB[tokenHash]
if !found || time.Now().After(tokenData.Expiration) {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Retrieve user data from the database based on the userID
user, found := users[tokenData.UserID]
if !found {
http.Error(w, "User not found", http.StatusNotFound)
return
}
// Provide access to the protected API data
fmt.Fprintf(w, "Welcome, User ID: %d, Username: %s", user.ID, user.Username)
fmt.Fprintf(w, "\nAbilities: %s", tokenData.Abilities)
}
func main() {
// Simulated login: here, we assume the user is authenticated and obtain the user ID.
userID := 123
abilities := "read,write" // Pass the abilities as a comma-separated string
// Generate a new token and store token-related information on the server
token, err := generateToken(userID, abilities)
if err != nil {
fmt.Println("Error creating token:", err)
return
}
// Send the raw token identifier to the client (you would typically return it in the login response)
fmt.Println("Token:", token)
// Start the HTTP server to handle API requests
http.HandleFunc("/api/data", apiDataHandler)
http.ListenAndServe(":8080", nil)
}
In this example:
-
we generate a random token identifier and hash it using SHA-256. We then store the hashed token and its expiration time in the tokensDB map.
-
When the client sends a request to the server, we extract the token from the request header and hash it using SHA-256. We then use the hashed token to lookup the token information in the tokensDB map.
-
If the token is valid, we retrieve the user data from the database based on the userID and provide access to the protected API data.
Stateless Tokens:
Stateless tokens, on the other hand, do not require the server to store any token information. These tokens contain all the necessary information within themselves, enabling the server to verify their authenticity without the need for any associated data stored on the server-side. Stateless tokens are typically used in API authentication and are often generated using cryptographic algorithms to ensure their integrity.
Take a look at the following example of a stateless token:
package main
import (
"fmt"
"net/http"
"time"
"github.com/dgrijalva/jwt-go"
)
var secretKey = []byte("your-secret-key")
var tokenDuration = time.Hour * 24 // Token validity duration, change as needed
// Simulated user database
var users = map[int]string{
123: "john_doe",
456: "jane_smith",
}
// Function to generate a new token for the given user ID
func generateToken(userID int) (string, error) {
// Create a new token
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user_id": userID,
"exp": time.Now().Add(tokenDuration).Unix(),
})
// Sign the token with the secret key
tokenString, err := token.SignedString(secretKey)
if err != nil {
return "", err
}
// Return the token to the user
return tokenString, nil
}
func apiDataHandler(w http.ResponseWriter, r *http.Request) {
// Extract the token from the request header
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Parse the token
tokenString := authHeader[len("Bearer "):]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return secretKey, nil
})
// Validate the token
if err != nil || !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Access the claims (user data) from the token
claims := token.Claims.(jwt.MapClaims)
userID := int(claims["user_id"].(float64))
// Retrieve user data from the database based on the userID
username, found := users[userID]
if !found {
http.Error(w, "User not found", http.StatusNotFound)
return
}
// Access user data (userID) and provide access to the protected API data
fmt.Fprintf(w, "Welcome, User ID: %d, Username: %s", userID, username)
}
func main() {
// Simulated login: here, we assume the user is authenticated and obtain the user ID.
userID := 123
// Generate a new token for the user
token, err := generateToken(userID)
if err != nil {
fmt.Println("Error creating token:", err)
return
}
// Send the token to the client (you would typically return it in the login response)
fmt.Println("Token:", token)
// Start the HTTP server to handle API requests
http.HandleFunc("/api/data", apiDataHandler)
http.ListenAndServe(":8080", nil)
}
In this example:
-
we generate a new token using the
jwt-gopackage and sign it with the secret key. We then return the token to the client. -
When the client sends a request to the server, we extract the token from the request header and parse it using the
jwt-gopackage. We then validate the token and access the claims (user data) from the token. -
If the token is valid, we retrieve the user data from the database based on the userID and provide access to the protected API data.
Conclusion:
Stateful tokens rely on server-side storage, while stateless tokens, like JWT, are self-contained and do not require server-side storage. Both methods can be used for securing APIs, and the choice depends on your application's requirements and use case.
Stateful tokens can be convenient for traditional web applications with server-side sessions, while stateless tokens are suitable for modern API authentication, especially in distributed systems. JWT is a popular choice for implementing stateless token-based authentication due to its simplicity and scalability.
Consider the security and performance aspects when selecting the appropriate token type for your API authentication. Stateful tokens may introduce server-side overhead, while stateless tokens require careful management of token expiration and token validation to maintain security.