How Does Gitea Secure Authentication Tokens: Cryptographic Deep Dive into Remember-Me Security

Gitea secures authentication tokens by storing only SHA-256 hashes while rotating plaintext tokens on every use, using cryptographically secure random generation and constant-time comparison to prevent timing attacks.

Understanding how Gitea secures authentication tokens reveals a defense-in-depth approach to session persistence. The implementation in the go-gitea/gitea repository demonstrates production-grade patterns for "remember me" functionality, balancing user convenience with rigorous cryptographic protections.

Token Structure and Storage

ID-Secret Format

Gitea uses a dual-component token format: ID:plainToken (e.g., a1b2c3:9f8e7d…). This separation allows the public ID to act as a database lookup key while keeping the actual secret (plainToken) private. The ID is generated using util.CryptoRandomString(10) to ensure unpredictability even for public identifiers.

Database Storage Strategy

Only hashed values persist to the database. In models/auth/auth_token.go, the AuthToken struct stores TokenHash as a SHA-256 digest, never the plaintext. This design ensures that even a complete database breach exposes only irreversible hashes, not usable session credentials.

Token Lifecycle

Creating Tokens

The CreateAuthTokenForUserID function in services/auth/auth_token.go (lines 86-104) handles initial generation:

t, token, err := auth.CreateAuthTokenForUserID(ctx, userID)

This function generates a random 10-character ID and a 32-byte random token using util.CryptoRandomBytes, encoding the latter as hexadecimal. It computes the SHA-256 hash of the plaintext token and stores only the hash alongside an expiration timestamp derived from setting.LogInRememberDays.

Validating Tokens

Validation occurs in CheckAuthToken within services/auth/auth_token.go (lines 33-63):

at, err := auth.CheckAuthToken(ctx, rawValue)

The process splits the incoming cookie on the : delimiter, retrieves the database record by ID, verifies the expiration (ExpiresUnix), and validates the hash using subtle.ConstantTimeCompare. This constant-time comparison prevents timing attacks that could leak information about valid token prefixes.

Rotating Tokens

To prevent replay attacks, Gitea implements automatic token rotation via RegenerateAuthToken (lines 66-84 in services/auth/auth_token.go):

newToken, newPlain, err := auth.RegenerateAuthToken(ctx, oldToken)

Each successful authentication generates a fresh random token and hash while preserving the original ID. The database updates only the TokenHash and ExpiresUnix columns, invalidating the previous plaintext token immediately. If an attacker attempts to reuse a stolen token, the hash mismatch triggers ErrAuthTokenInvalidHash, signaling potential token theft.

Expiration and Cleanup

Tokens enforce configurable expiration through setting.LogInRememberDays. The system periodically invokes DeleteExpiredAuthTokens from models/auth/auth_token.go (lines 62-65) to purge expired records, ensuring no indefinite session persistence.

Security Mechanisms

Cryptographic Randomness

Gitea relies on util.CryptoRandomBytes for all random generation, sourcing from the operating system's cryptographically secure random number generator. This provides 256 bits of entropy for tokens, making brute-force attempts computationally infeasible.

Hashing and Timing Attack Prevention

All token comparisons use subtle.ConstantTimeCompare rather than standard string equality. This ensures the comparison operation takes identical time regardless of how many characters match, eliminating timing side-channels that could reveal valid token prefixes to attackers.

Replay Attack Prevention

The rotation mechanism serves as a theft detection system. When a user legitimately logs in, the token rotates. If an attacker subsequently attempts to use the stolen previous token, the hash verification fails, and the system can flag the anomaly. This limits the attack window to the interval between theft and legitimate use.

Practical Implementation Example

The following example demonstrates issuing and validating remember-me cookies using Gitea's authentication service:

// Issue a secure remember-me cookie after primary authentication
func issueRememberMeCookie(ctx context.Context, w http.ResponseWriter, userID int64) error {
    // Generate new token pair
    at, plain, err := auth.CreateAuthTokenForUserID(ctx, userID)
    if err != nil {
        return err
    }

    // Format as ID:plain
    cookieValue := fmt.Sprintf("%s:%s", at.ID, plain)

    // Set secure cookie attributes
    http.SetCookie(w, &http.Cookie{
        Name:     "gitea_auth",
        Value:    cookieValue,
        Path:     "/",
        HttpOnly: true,
        Secure:   setting.Scheme == "https",
        SameSite: http.SameSiteStrictMode,
        MaxAge:   int(at.ExpiresUnix.Sub(time.Now()).Seconds()),
    })
    return nil
}

// Middleware validating remember-me tokens with automatic rotation
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        c, err := r.Cookie("gitea_auth")
        if err == nil {
            token, err := auth.CheckAuthToken(r.Context(), c.Value)
            if err == nil {
                // Valid token: rotate for next request
                newToken, newPlain, _ := auth.RegenerateAuthToken(r.Context(), token)
                
                // Update cookie with new token value
                newCookie := fmt.Sprintf("%s:%s", newToken.ID, newPlain)
                // ... set updated cookie
                
                r = r.WithContext(context.WithValue(r.Context(), "userID", token.UserID))
            }
        }
        next.ServeHTTP(w, r)
    })
}

Key files referenced in this implementation include services/auth/auth_token.go for core logic and models/auth/auth_token.go for database operations.

Summary

  • Storage security: Only SHA-256 hashes of tokens are stored in the database (AuthToken.TokenHash), never plaintext.
  • Cryptographic generation: Uses util.CryptoRandomBytes for 256-bit entropy tokens and CryptoRandomString for unpredictable IDs.
  • Timing attack resistance: Hash comparisons use subtle.ConstantTimeCompare to prevent information leakage.
  • Automatic rotation: Every successful validation triggers RegenerateAuthToken, invalidating the old plaintext token and preventing replay attacks.
  • Configurable expiration: setting.LogInRememberDays controls token lifetime, with DeleteExpiredAuthTokens handling cleanup.

Frequently Asked Questions

How does Gitea prevent timing attacks during token validation?

Gitea uses subtle.ConstantTimeCompare from the Go standard library when comparing the SHA-256 hash of the provided token against the stored hash in CheckAuthToken. This ensures the comparison takes constant time regardless of how many characters match, preventing attackers from measuring response times to guess valid token prefixes.

If an attacker steals a remember-me cookie containing a valid ID:plainToken pair, they can use it until the legitimate user logs in again. Once the legitimate user authenticates, RegenerateAuthToken creates a new hash, invalidating the stolen plaintext token. Subsequent use of the stolen token triggers ErrAuthTokenInvalidHash, potentially alerting administrators to the compromise.

Why does Gitea use SHA-256 instead of bcrypt for token hashing?

SHA-256 is used instead of bcrypt because authentication tokens are high-entropy (256-bit random values), making them resistant to brute-force attacks without the computational overhead of bcrypt. The system prioritizes fast validation for API requests while maintaining security through token rotation and expiration rather than slow hashing algorithms.

Where are authentication tokens stored in the Gitea codebase?

Token logic resides primarily in services/auth/auth_token.go, which contains CreateAuthTokenForUserID, CheckAuthToken, and RegenerateAuthToken. The database model and cleanup utilities live in models/auth/auth_token.go, while configuration settings like LogInRememberDays are defined in the settings module.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →