feat: add env-based configuration and token-in-cookie auth
Implement 12-factor configuration via environment variables and token-in-cookie authentication for Gitea API access. - internal/config/config.go: reads GITEA_URL, GITEA_TOKEN, LISTEN_ADDR, SESSION_SECRET from environment with validation - internal/auth/cookie.go: HMAC-signed HTTP-only cookie for storing Gitea API tokens (Secure, SameSite=Strict) - internal/middleware/auth.go: extracts token from cookie, injects into request context, redirects unauthenticated users to /settings - internal/middleware/logging.go: structured JSON request logging - internal/handlers/settings.go: settings page for entering/removing Gitea API token with mobile-first dark UI - cmd/server/main.go: integrated config, auth middleware, and settings Includes unit tests for config loading, cookie signing/verification, and auth middleware bypass/redirect logic. Closes leeworks-agents/gitea-mobile#2 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cookieName = "gitea_token"
|
||||
cookieMaxAge = 30 * 24 * 60 * 60 // 30 days in seconds
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSignature = errors.New("invalid cookie signature")
|
||||
ErrMalformedCookie = errors.New("malformed cookie value")
|
||||
)
|
||||
|
||||
// SetTokenCookie stores a Gitea API token in a signed HTTP-only cookie.
|
||||
func SetTokenCookie(w http.ResponseWriter, token string, secret string, secure bool) {
|
||||
signed := sign(token, secret)
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: signed,
|
||||
Path: "/",
|
||||
MaxAge: cookieMaxAge,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
Expires: time.Now().Add(30 * 24 * time.Hour),
|
||||
})
|
||||
}
|
||||
|
||||
// ClearTokenCookie removes the token cookie.
|
||||
func ClearTokenCookie(w http.ResponseWriter, secure bool) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
}
|
||||
|
||||
// GetToken extracts and verifies the Gitea API token from the request cookie.
|
||||
// Returns the token string or an error if the cookie is missing or invalid.
|
||||
func GetToken(r *http.Request, secret string) (string, error) {
|
||||
cookie, err := r.Cookie(cookieName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
token, err := verify(cookie.Value, secret)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// sign creates a signed cookie value: base64(token).base64(hmac-sha256(token))
|
||||
func sign(token string, secret string) string {
|
||||
encodedToken := base64.URLEncoding.EncodeToString([]byte(token))
|
||||
mac := computeHMAC(encodedToken, secret)
|
||||
return fmt.Sprintf("%s.%s", encodedToken, mac)
|
||||
}
|
||||
|
||||
// verify checks the HMAC signature and returns the original token.
|
||||
func verify(signed string, secret string) (string, error) {
|
||||
parts := strings.SplitN(signed, ".", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", ErrMalformedCookie
|
||||
}
|
||||
|
||||
encodedToken := parts[0]
|
||||
providedMAC := parts[1]
|
||||
expectedMAC := computeHMAC(encodedToken, secret)
|
||||
|
||||
if !hmac.Equal([]byte(providedMAC), []byte(expectedMAC)) {
|
||||
return "", ErrInvalidSignature
|
||||
}
|
||||
|
||||
tokenBytes, err := base64.URLEncoding.DecodeString(encodedToken)
|
||||
if err != nil {
|
||||
return "", ErrMalformedCookie
|
||||
}
|
||||
|
||||
return string(tokenBytes), nil
|
||||
}
|
||||
|
||||
// computeHMAC generates a base64-encoded HMAC-SHA256 of the given data.
|
||||
func computeHMAC(data string, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(data))
|
||||
return base64.URLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
Reference in New Issue
Block a user