sf-auth-middleware-gin/sfauthgin/sfauth.go

147 lines
3.8 KiB
Go
Raw Normal View History

2026-02-06 17:52:41 -08:00
package sfauthgin
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
const (
sessionUserIDKey = "sf_user_id"
sessionUsernameKey = "sf_username"
authenticateURLTemplate = "https://snazzyfellas.com/api/redirect/authenticate?redirect_uri=%s"
validateURL = "https://snazzyfellas.com/api/redirect/validate"
)
type validationRequest struct {
UserID string `json:"user_id"`
Key string `json:"key"`
}
type validationResponse struct {
Valid bool `json:"valid"`
UserID string `json:"user_id"`
}
// NewMiddleware creates a Gin middleware that checks for sf-auth session values
// and redirects to the Snazzyfellas authenticate endpoint when missing.
func NewMiddleware(redirectURL func(*gin.Context) string) gin.HandlerFunc {
return func(c *gin.Context) {
session := sessions.Default(c)
if session == nil {
c.String(http.StatusInternalServerError, "session middleware not configured")
c.Abort()
return
}
userID := session.Get(sessionUserIDKey)
username := session.Get(sessionUsernameKey)
if !isNonEmptyString(userID) || !isNonEmptyString(username) {
target := ""
if redirectURL != nil {
target = redirectURL(c)
}
if target == "" {
c.String(http.StatusInternalServerError, "redirect URL is required")
c.Abort()
return
}
redirect := fmt.Sprintf(authenticateURLTemplate, url.QueryEscape(target))
c.Redirect(http.StatusFound, redirect)
c.Abort()
return
}
c.Next()
}
}
// CreateAuthCallbackHandler returns a handler for the sf-auth callback route.
// It validates the one-time key, stores session values, and redirects to redirectTo.
func CreateAuthCallbackHandler(redirectTo string) gin.HandlerFunc {
return func(c *gin.Context) {
if redirectTo == "" {
c.String(http.StatusInternalServerError, "redirect destination is required")
return
}
userID := c.Query("user_id")
username := c.Query("username")
key := c.Query("key")
if userID == "" || username == "" || key == "" {
c.String(http.StatusBadRequest, "missing required query parameters")
return
}
if err := validateAuth(c, userID, key); err != nil {
c.String(http.StatusUnauthorized, err.Error())
return
}
session := sessions.Default(c)
if session == nil {
c.String(http.StatusInternalServerError, "session middleware not configured")
return
}
session.Set(sessionUserIDKey, userID)
session.Set(sessionUsernameKey, username)
if err := session.Save(); err != nil {
c.String(http.StatusInternalServerError, "failed to save session")
return
}
c.Redirect(http.StatusFound, redirectTo)
}
}
func validateAuth(c *gin.Context, userID string, key string) error {
payload := validationRequest{UserID: userID, Key: key}
body, err := json.Marshal(payload)
if err != nil {
return errors.New("failed to encode validation payload")
}
req, err := http.NewRequestWithContext(c.Request.Context(), http.MethodPost, validateURL, bytes.NewReader(body))
if err != nil {
return errors.New("failed to create validation request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return errors.New("failed to reach validation service")
}
defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
return fmt.Errorf("validation service returned status %d", resp.StatusCode)
}
var result validationResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return errors.New("invalid validation response")
}
if !result.Valid || result.UserID != userID {
return errors.New("invalid auth response")
}
return nil
}
func isNonEmptyString(value interface{}) bool {
text, ok := value.(string)
if !ok {
return false
}
return text != ""
}