147 lines
3.8 KiB
Go
147 lines
3.8 KiB
Go
|
|
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 != ""
|
||
|
|
}
|