Created
November 10, 2025 22:38
-
-
Save Luigi-Pizzolito/e54aa9a383ef4f13047320f587457481 to your computer and use it in GitHub Desktop.
Hank Auth Go Middleware
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package auth | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "net/http" | |
| "strings" | |
| "github.com/gin-gonic/gin" | |
| ) | |
| // ** Sample Usage ** | |
| // // Initialize Hanko validator from env | |
| // hankoURL := os.Getenv("HANKO_API_URL") | |
| // var validator auth.SessionValidator | |
| // if hankoURL != "" { | |
| // validator = auth.NewHankoSessionValidator(hankoURL) | |
| // } else { | |
| // log.Fatalf("HANKO_API_URL not set; cannot start") | |
| // } | |
| // // Protected routes require valid hanko cookie | |
| // protected := api.Group("") | |
| // protected.Use(auth.Middleware(validator)) | |
| // SessionValidator defines the interface for session validation | |
| // and optional user extraction. | |
| // We keep it minimal per Hanko quickstart. | |
| type SessionValidator interface { | |
| ValidateSession(token string) (bool, *Claims, error) | |
| } | |
| type HankoSessionValidator struct { | |
| APIURL string | |
| // Optional: HTTP client injection for testing | |
| Client *http.Client | |
| } | |
| type ValidationResponse struct { | |
| IsValid bool `json:"is_valid"` | |
| ExpirationTime *string `json:"expiration_time,omitempty"` | |
| UserID *string `json:"user_id,omitempty"` | |
| Claims *Claims `json:"claims,omitempty"` | |
| } | |
| // Claims is a subset of Hanko claims we care about. | |
| type Claims struct { | |
| Subject string `json:"subject"` | |
| IssuedAt string `json:"issued_at"` | |
| Expiration string `json:"expiration"` | |
| Issuer string `json:"issuer"` | |
| // Username fields (presence depends on Hanko config/claims mapping) | |
| Username string `json:"username,omitempty"` | |
| PreferredUsername string `json:"preferred_username,omitempty"` | |
| Name string `json:"name,omitempty"` | |
| Email *struct { | |
| Address string `json:"address"` | |
| IsPrimary bool `json:"is_primary"` | |
| IsVerified bool `json:"is_verified"` | |
| } `json:"email,omitempty"` | |
| SessionID string `json:"session_id"` | |
| } | |
| func NewHankoSessionValidator(apiURL string) *HankoSessionValidator { | |
| return &HankoSessionValidator{APIURL: strings.TrimRight(apiURL, "/"), Client: http.DefaultClient} | |
| } | |
| func (v *HankoSessionValidator) ValidateSession(token string) (bool, *Claims, error) { | |
| payload := strings.NewReader(fmt.Sprintf(`{"session_token":"%s"}`, token)) | |
| req, err := http.NewRequest(http.MethodPost, v.APIURL+"/sessions/validate", payload) | |
| if err != nil { | |
| return false, nil, fmt.Errorf("failed to create request: %w", err) | |
| } | |
| req = req.WithContext(context.Background()) | |
| req.Header.Add("Content-Type", "application/json") | |
| resp, err := v.Client.Do(req) | |
| if err != nil { | |
| return false, nil, fmt.Errorf("failed to send request: %w", err) | |
| } | |
| defer resp.Body.Close() | |
| b, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| return false, nil, fmt.Errorf("failed to read response: %w", err) | |
| } | |
| if resp.StatusCode != http.StatusOK { | |
| return false, nil, fmt.Errorf("hanko validate status %d: %s", resp.StatusCode, string(b)) | |
| } | |
| var vr ValidationResponse | |
| if err := json.Unmarshal(b, &vr); err != nil { | |
| return false, nil, fmt.Errorf("failed to parse response: %w", err) | |
| } | |
| return vr.IsValid, vr.Claims, nil | |
| } | |
| // Gin middleware that enforces valid hanko session cookie. | |
| // On success, sets c.Set("auth", true). You may extend to set user info. | |
| func Middleware(validator SessionValidator) gin.HandlerFunc { | |
| return func(c *gin.Context) { | |
| cookie, err := c.Request.Cookie("hanko") | |
| if err != nil || cookie == nil || cookie.Value == "" { | |
| c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "unauthorized", "message": "missing hanko cookie"}}) | |
| return | |
| } | |
| ok, claims, err := validator.ValidateSession(cookie.Value) | |
| if err != nil { | |
| c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": gin.H{"code": "auth_error", "message": err.Error()}}) | |
| return | |
| } | |
| if !ok { | |
| c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": gin.H{"code": "unauthorized", "message": "invalid session"}}) | |
| return | |
| } | |
| c.Set("auth", true) | |
| if claims != nil { | |
| c.Set("user.subject", claims.Subject) | |
| if claims.Email != nil { | |
| c.Set("user.email", claims.Email.Address) | |
| c.Set("user.email_verified", claims.Email.IsVerified) | |
| } | |
| // Populate a display name if available from claims | |
| displayName := "" | |
| if claims.Username != "" { | |
| displayName = claims.Username | |
| } else if claims.PreferredUsername != "" { | |
| displayName = claims.PreferredUsername | |
| } else if claims.Name != "" { | |
| displayName = claims.Name | |
| } | |
| if displayName != "" { | |
| c.Set("user.name", displayName) | |
| } | |
| c.Set("user.session_id", claims.SessionID) | |
| } | |
| c.Next() | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment