Add middleware and route
This commit is contained in:
parent
f87fe06255
commit
ed5dff832b
3 changed files with 276 additions and 0 deletions
146
sfauthgin/sfauth.go
Normal file
146
sfauthgin/sfauth.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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 != ""
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue