Implement secure cookie handling and session management in Signin API
- Introduced SecureCookieOptions struct to define secure cookie configurations, enhancing cookie security. - Added functions for sending secure cookies, including session and access token cookies, with customizable options. - Updated signin and OAuth authorization URL handling to manage session IDs and state validation, improving security and user experience. - Enhanced error handling for OAuth state and redirect URI management, ensuring robust session management during authentication flows.
This commit is contained in:
parent
0361cfe5fa
commit
219c7d97ed
4 changed files with 528 additions and 12 deletions
|
|
@ -2,6 +2,7 @@ package response
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
|
|
@ -178,6 +179,264 @@ func RespondWithError(c *gin.Context, statusCode int, err *ErrorResponse) {
|
|||
c.JSON(statusCode, err)
|
||||
}
|
||||
|
||||
// SecureCookieOptions defines options for secure cookie configuration
|
||||
type SecureCookieOptions struct {
|
||||
// MaxAge specifies the max age for the cookie in seconds (0 = session cookie, negative = delete cookie)
|
||||
// MaxAge takes precedence over Expires if both are set
|
||||
MaxAge int
|
||||
// Expires specifies the absolute expiration time for the cookie
|
||||
// If MaxAge is 0 and Expires is set, Expires will be used
|
||||
Expires *time.Time
|
||||
// Path specifies the cookie path (default: "/")
|
||||
Path string
|
||||
// Domain specifies the cookie domain (empty for current domain)
|
||||
Domain string
|
||||
// SameSite specifies the SameSite attribute ("Strict", "Lax", or "None")
|
||||
SameSite string
|
||||
// UseHostPrefix determines if __Host- prefix should be used (most secure)
|
||||
UseHostPrefix bool
|
||||
// UseSecurePrefix determines if __Secure- prefix should be used
|
||||
UseSecurePrefix bool
|
||||
}
|
||||
|
||||
// NewSecureCookieOptions creates a new SecureCookieOptions with secure defaults
|
||||
func NewSecureCookieOptions() *SecureCookieOptions {
|
||||
return &SecureCookieOptions{
|
||||
MaxAge: 0, // Session cookie by default
|
||||
Path: "/", // Root path
|
||||
Domain: "", // Current domain
|
||||
SameSite: "Lax", // Default SameSite policy
|
||||
UseHostPrefix: true, // Use most secure __Host- prefix
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxAge sets the MaxAge in seconds
|
||||
func (o *SecureCookieOptions) WithMaxAge(maxAge int) *SecureCookieOptions {
|
||||
o.MaxAge = maxAge
|
||||
return o
|
||||
}
|
||||
|
||||
// WithExpires sets the absolute expiration time
|
||||
func (o *SecureCookieOptions) WithExpires(expires time.Time) *SecureCookieOptions {
|
||||
o.Expires = &expires
|
||||
return o
|
||||
}
|
||||
|
||||
// WithDuration sets expiration based on duration from now
|
||||
func (o *SecureCookieOptions) WithDuration(duration time.Duration) *SecureCookieOptions {
|
||||
expires := time.Now().Add(duration)
|
||||
o.Expires = &expires
|
||||
o.MaxAge = int(duration.Seconds())
|
||||
return o
|
||||
}
|
||||
|
||||
// WithPath sets the cookie path
|
||||
func (o *SecureCookieOptions) WithPath(path string) *SecureCookieOptions {
|
||||
o.Path = path
|
||||
return o
|
||||
}
|
||||
|
||||
// WithDomain sets the cookie domain
|
||||
func (o *SecureCookieOptions) WithDomain(domain string) *SecureCookieOptions {
|
||||
o.Domain = domain
|
||||
return o
|
||||
}
|
||||
|
||||
// WithSameSite sets the SameSite attribute
|
||||
func (o *SecureCookieOptions) WithSameSite(sameSite string) *SecureCookieOptions {
|
||||
o.SameSite = sameSite
|
||||
return o
|
||||
}
|
||||
|
||||
// WithSecurePrefix uses __Secure- prefix instead of __Host-
|
||||
func (o *SecureCookieOptions) WithSecurePrefix() *SecureCookieOptions {
|
||||
o.UseHostPrefix = false
|
||||
o.UseSecurePrefix = true
|
||||
return o
|
||||
}
|
||||
|
||||
// WithoutPrefix disables security prefixes
|
||||
func (o *SecureCookieOptions) WithoutPrefix() *SecureCookieOptions {
|
||||
o.UseHostPrefix = false
|
||||
o.UseSecurePrefix = false
|
||||
return o
|
||||
}
|
||||
|
||||
// SendSecretCookie sends a secure cookie to the client with RFC 6265bis compliance
|
||||
// For sensitive data like session_id, access_token, etc.
|
||||
func SendSecretCookie(c *gin.Context, key string, value string) {
|
||||
options := &SecureCookieOptions{
|
||||
MaxAge: 0, // Session cookie by default
|
||||
Path: "/", // Root path
|
||||
Domain: "", // Current domain
|
||||
SameSite: "Lax", // Default SameSite policy
|
||||
UseHostPrefix: true, // Use most secure __Host- prefix
|
||||
}
|
||||
SendSecureCookieWithOptions(c, key, value, options)
|
||||
}
|
||||
|
||||
// SendSecureCookieWithOptions sends a secure cookie with custom options
|
||||
func SendSecureCookieWithOptions(c *gin.Context, key string, value string, options *SecureCookieOptions) {
|
||||
// Apply RFC 6265bis prefix requirements
|
||||
cookieName := key
|
||||
cookiePath := options.Path
|
||||
cookieDomain := options.Domain
|
||||
|
||||
if options.UseHostPrefix {
|
||||
// __Host- prefix: Requires Secure flag, no Domain attribute, Path=/
|
||||
cookieName = "__Host-" + key
|
||||
cookiePath = "/" // Must be "/" for __Host- prefix
|
||||
cookieDomain = "" // Must be empty for __Host- prefix
|
||||
} else if options.UseSecurePrefix {
|
||||
// __Secure- prefix: Requires Secure flag, allows Domain and Path
|
||||
cookieName = "__Secure-" + key
|
||||
}
|
||||
|
||||
// Ensure secure defaults
|
||||
if cookiePath == "" {
|
||||
cookiePath = "/"
|
||||
}
|
||||
|
||||
// Determine effective MaxAge
|
||||
effectiveMaxAge := options.MaxAge
|
||||
if effectiveMaxAge == 0 && options.Expires != nil {
|
||||
// If MaxAge is 0 but Expires is set, calculate MaxAge from Expires
|
||||
duration := time.Until(*options.Expires)
|
||||
if duration > 0 {
|
||||
effectiveMaxAge = int(duration.Seconds())
|
||||
} else {
|
||||
effectiveMaxAge = -1 // Expired cookie
|
||||
}
|
||||
}
|
||||
|
||||
// Set the cookie with secure flags
|
||||
// Gin's SetCookie: (name, value, maxAge, path, domain, secure, httpOnly)
|
||||
c.SetCookie(
|
||||
cookieName, // name (with security prefix if specified)
|
||||
value, // value
|
||||
effectiveMaxAge, // maxAge (calculated from Expires if needed)
|
||||
cookiePath, // path
|
||||
cookieDomain, // domain
|
||||
true, // secure (HTTPS only) - required for security prefixes
|
||||
true, // httpOnly (prevent XSS access)
|
||||
)
|
||||
|
||||
// Get existing Set-Cookie headers for additional attributes
|
||||
cookies := c.Writer.Header()["Set-Cookie"]
|
||||
if len(cookies) > 0 {
|
||||
lastCookie := cookies[len(cookies)-1]
|
||||
|
||||
// Add SameSite attribute if specified
|
||||
if options.SameSite != "" {
|
||||
lastCookie += "; SameSite=" + options.SameSite
|
||||
}
|
||||
|
||||
// Add Expires attribute if specified and MaxAge is not used
|
||||
if options.Expires != nil && options.MaxAge == 0 {
|
||||
lastCookie += "; Expires=" + options.Expires.UTC().Format(time.RFC1123)
|
||||
}
|
||||
|
||||
// Replace the last Set-Cookie header with enhanced version
|
||||
cookies[len(cookies)-1] = lastCookie
|
||||
c.Writer.Header()["Set-Cookie"] = cookies
|
||||
}
|
||||
}
|
||||
|
||||
// SendSessionCookie sends a session cookie with __Host- prefix for maximum security
|
||||
func SendSessionCookie(c *gin.Context, sessionID string) {
|
||||
options := NewSecureCookieOptions().WithSameSite("Lax")
|
||||
SendSecureCookieWithOptions(c, "session_id", sessionID, options)
|
||||
}
|
||||
|
||||
// SendAccessTokenCookie sends an access token cookie with appropriate security settings
|
||||
func SendAccessTokenCookie(c *gin.Context, accessToken string, maxAge int) {
|
||||
options := NewSecureCookieOptions().
|
||||
WithMaxAge(maxAge).
|
||||
WithSameSite("Strict")
|
||||
SendSecureCookieWithOptions(c, "access_token", accessToken, options)
|
||||
}
|
||||
|
||||
// SendAccessTokenCookieWithExpiry sends an access token cookie with absolute expiration time
|
||||
func SendAccessTokenCookieWithExpiry(c *gin.Context, accessToken string, expires time.Time) {
|
||||
options := NewSecureCookieOptions().
|
||||
WithExpires(expires).
|
||||
WithSameSite("Strict")
|
||||
SendSecureCookieWithOptions(c, "access_token", accessToken, options)
|
||||
}
|
||||
|
||||
// SendAccessTokenCookieWithDuration sends an access token cookie with duration-based expiration
|
||||
func SendAccessTokenCookieWithDuration(c *gin.Context, accessToken string, duration time.Duration) {
|
||||
options := NewSecureCookieOptions().
|
||||
WithDuration(duration).
|
||||
WithSameSite("Strict")
|
||||
SendSecureCookieWithOptions(c, "access_token", accessToken, options)
|
||||
}
|
||||
|
||||
// SendRefreshTokenCookie sends a refresh token cookie with strict security settings
|
||||
func SendRefreshTokenCookie(c *gin.Context, refreshToken string, maxAge int) {
|
||||
options := NewSecureCookieOptions().
|
||||
WithMaxAge(maxAge).
|
||||
WithPath("/auth").
|
||||
WithSameSite("Strict")
|
||||
SendSecureCookieWithOptions(c, "refresh_token", refreshToken, options)
|
||||
}
|
||||
|
||||
// SendRefreshTokenCookieWithExpiry sends a refresh token cookie with absolute expiration time
|
||||
func SendRefreshTokenCookieWithExpiry(c *gin.Context, refreshToken string, expires time.Time) {
|
||||
options := NewSecureCookieOptions().
|
||||
WithExpires(expires).
|
||||
WithPath("/auth").
|
||||
WithSameSite("Strict")
|
||||
SendSecureCookieWithOptions(c, "refresh_token", refreshToken, options)
|
||||
}
|
||||
|
||||
// SendRefreshTokenCookieWithDuration sends a refresh token cookie with duration-based expiration
|
||||
func SendRefreshTokenCookieWithDuration(c *gin.Context, refreshToken string, duration time.Duration) {
|
||||
options := NewSecureCookieOptions().
|
||||
WithDuration(duration).
|
||||
WithPath("/auth").
|
||||
WithSameSite("Strict")
|
||||
SendSecureCookieWithOptions(c, "refresh_token", refreshToken, options)
|
||||
}
|
||||
|
||||
// DeleteSecureCookie deletes a secure cookie by setting it to expire immediately
|
||||
func DeleteSecureCookie(c *gin.Context, key string) {
|
||||
options := NewSecureCookieOptions().WithMaxAge(-1) // Negative MaxAge deletes the cookie
|
||||
SendSecureCookieWithOptions(c, key, "", options)
|
||||
}
|
||||
|
||||
// DeleteAllAuthCookies deletes all authentication-related cookies
|
||||
func DeleteAllAuthCookies(c *gin.Context) {
|
||||
DeleteSecureCookie(c, "session_id")
|
||||
DeleteSecureCookie(c, "access_token")
|
||||
|
||||
// Also delete refresh token with its specific path
|
||||
options := NewSecureCookieOptions().
|
||||
WithMaxAge(-1).
|
||||
WithPath("/auth")
|
||||
SendSecureCookieWithOptions(c, "refresh_token", "", options)
|
||||
}
|
||||
|
||||
// Common duration constants for cookie expiration
|
||||
const (
|
||||
// Session cookies (expires when browser closes)
|
||||
SessionCookie = 0
|
||||
// Short-lived tokens (typically for access tokens)
|
||||
OneHour = 1 * time.Hour
|
||||
TwoHours = 2 * time.Hour
|
||||
SixHours = 6 * time.Hour
|
||||
TwelveHours = 12 * time.Hour
|
||||
// Medium-lived tokens
|
||||
OneDay = 24 * time.Hour
|
||||
OneWeek = 7 * 24 * time.Hour
|
||||
TwoWeeks = 14 * 24 * time.Hour
|
||||
// Long-lived tokens (typically for refresh tokens)
|
||||
OneMonth = 30 * 24 * time.Hour
|
||||
ThreeMonths = 90 * 24 * time.Hour
|
||||
SixMonths = 180 * 24 * time.Hour
|
||||
OneYear = 365 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// RespondWithAuthorizationError sends an authorization endpoint error via redirect
|
||||
func RespondWithAuthorizationError(c *gin.Context, redirectURI string, err *ErrorResponse, state string) {
|
||||
// Build error redirect URL
|
||||
|
|
|
|||
|
|
@ -4,20 +4,34 @@ import (
|
|||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/yaoapp/gou/session"
|
||||
"github.com/yaoapp/gou/store"
|
||||
"github.com/yaoapp/kun/log"
|
||||
"github.com/yaoapp/kun/maps"
|
||||
"github.com/yaoapp/yao/openapi/oauth/types"
|
||||
"github.com/yaoapp/yao/openapi/response"
|
||||
"github.com/yaoapp/yao/openapi/utils"
|
||||
)
|
||||
|
||||
// OAuthAuthorizationURLResponse represents the response for OAuth authorization URL
|
||||
type OAuthAuthorizationURLResponse struct {
|
||||
AuthorizationURL string `json:"authorization_url"`
|
||||
State string `json:"state"`
|
||||
}
|
||||
|
||||
// Attach attaches the signin handlers to the router
|
||||
func Attach(group *gin.RouterGroup, oauth types.OAuth) {
|
||||
group.GET("/signin", getConfig)
|
||||
group.POST("/signin", signin)
|
||||
group.GET("/signin/authback/:id", authback)
|
||||
group.POST("/signin/authback/:provider", authback)
|
||||
group.GET("/signin/oauth/:provider/authorize", getOAuthAuthorizationURL)
|
||||
group.POST("/signin/oauth/:provider/authorize/prepare", authbackPrepare) // Receive the post data and forward to the authback handler
|
||||
}
|
||||
|
||||
// getConfig is the handler for get signin configuration
|
||||
|
|
@ -28,6 +42,13 @@ func getConfig(c *gin.Context) {
|
|||
// Get public configuration for the specified locale
|
||||
config := GetPublicConfig(locale)
|
||||
|
||||
// Set session id if not exists
|
||||
sid := utils.GetSessionID(c)
|
||||
if sid == "" {
|
||||
sid = generateSessionID()
|
||||
response.SendSessionCookie(c, sid)
|
||||
}
|
||||
|
||||
// If no configuration found, return error
|
||||
if config == nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
|
|
@ -46,12 +67,73 @@ func getConfig(c *gin.Context) {
|
|||
func signin(c *gin.Context) {}
|
||||
|
||||
// authback is the handler for authback
|
||||
func authback(c *gin.Context) {}
|
||||
func authbackPrepare(c *gin.Context) {
|
||||
code := c.PostForm("code")
|
||||
state := c.PostForm("state")
|
||||
providerID := c.Param("provider")
|
||||
redirectURI, err := getRedirectURI(providerID, state)
|
||||
if err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Failed to get redirect URI",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// OAuthAuthorizationURLResponse represents the response for OAuth authorization URL
|
||||
type OAuthAuthorizationURLResponse struct {
|
||||
AuthorizationURL string `json:"authorization_url"`
|
||||
State string `json:"state"`
|
||||
// Remove the redirect URI from the session
|
||||
err = removeRedirectURI(providerID, state)
|
||||
if err != nil {
|
||||
log.Warn("Failed to remove redirect URI: %v", err)
|
||||
}
|
||||
|
||||
params := url.Values{}
|
||||
params.Add("code", code)
|
||||
params.Add("state", state)
|
||||
c.Redirect(http.StatusFound, redirectURI+"?"+params.Encode())
|
||||
}
|
||||
|
||||
// authback is the handler for authback
|
||||
func authback(c *gin.Context) {
|
||||
sid := utils.GetSessionID(c)
|
||||
providerID := c.Param("provider")
|
||||
state := c.PostForm("state")
|
||||
|
||||
if state == "" {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "State is required",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateState(providerID, sid, state); err != nil {
|
||||
log.With(log.F{"sid": sid, "state": state}).Error("Invalid state")
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Invalid state",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusBadRequest, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Remove the state from the session
|
||||
err := removeState(providerID, sid)
|
||||
if err != nil {
|
||||
log.With(log.F{"sid": sid, "providerID": providerID}).Error("Failed to remove state")
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Failed to remove state",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Respond with success
|
||||
response.RespondWithSuccess(c, response.StatusOK, maps.Map{
|
||||
"state": state,
|
||||
})
|
||||
}
|
||||
|
||||
// getOAuthAuthorizationURL generates OAuth authorization URL for a provider
|
||||
|
|
@ -71,8 +153,6 @@ func getOAuthAuthorizationURL(c *gin.Context) {
|
|||
state := c.Query("state")
|
||||
locale := c.Query("locale")
|
||||
|
||||
fmt.Println("redirect_uri", redirectURI)
|
||||
|
||||
// Get full configuration
|
||||
config := GetFullConfig(locale)
|
||||
if config == nil {
|
||||
|
|
@ -140,9 +220,6 @@ func getOAuthAuthorizationURL(c *gin.Context) {
|
|||
params.Add("redirect_uri", redirectURI)
|
||||
params.Add("state", state)
|
||||
|
||||
fmt.Println("client_id", provider.ClientID)
|
||||
fmt.Println("redirectURI", redirectURI)
|
||||
|
||||
// Add scopes
|
||||
if len(provider.Scopes) > 0 {
|
||||
params.Add("scope", strings.Join(provider.Scopes, " "))
|
||||
|
|
@ -155,6 +232,52 @@ func getOAuthAuthorizationURL(c *gin.Context) {
|
|||
params.Add("response_mode", provider.ResponseMode)
|
||||
}
|
||||
|
||||
// Set session id if not exists
|
||||
sid := utils.GetSessionID(c)
|
||||
if sid == "" {
|
||||
sid = generateSessionID()
|
||||
response.SendSessionCookie(c, sid)
|
||||
}
|
||||
|
||||
// Save the state to the session for 20 minutes
|
||||
err := saveState(providerID, sid, state)
|
||||
if err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Failed to save OAuth state",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// if response mode is form_post, save the redirect URI to the session
|
||||
if provider.ResponseMode == "form_post" {
|
||||
err := saveRedirectURI(providerID, state, redirectURI)
|
||||
if err != nil {
|
||||
errorResp := &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Failed to save OAuth redirect URI",
|
||||
}
|
||||
response.RespondWithError(c, response.StatusInternalServerError, errorResp)
|
||||
return
|
||||
}
|
||||
|
||||
// Replace the redirectURI to
|
||||
pathname := c.Request.URL.Path + "/prepare"
|
||||
newRedirectURI, err := reconstructRedirectURI(redirectURI, pathname, c)
|
||||
if err != nil {
|
||||
log.Error("Failed to reconstruct redirectURI: %v", err)
|
||||
response.RespondWithError(c, response.StatusBadRequest, &response.ErrorResponse{
|
||||
Code: response.ErrInvalidRequest.Code,
|
||||
ErrorDescription: "Invalid redirect URI format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
params.Set("redirect_uri", newRedirectURI)
|
||||
}
|
||||
|
||||
// Build the authorization URL
|
||||
authorizationURL := fmt.Sprintf("%s?%s", provider.Endpoints.Authorization, params.Encode())
|
||||
|
||||
// Return the authorization URL and state
|
||||
|
|
@ -181,3 +304,80 @@ func getScheme(c *gin.Context) string {
|
|||
}
|
||||
return "http"
|
||||
}
|
||||
|
||||
// reconstructRedirectURI reconstructs redirectURI with new path while preserving the original host
|
||||
func reconstructRedirectURI(originalRedirectURI, newPath string, c *gin.Context) (string, error) {
|
||||
// Parse the original redirectURI to extract host
|
||||
parsedURL, err := url.Parse(originalRedirectURI)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse redirectURI: %v", err)
|
||||
}
|
||||
|
||||
// Reconstruct with the original host and new path
|
||||
newRedirectURI := fmt.Sprintf("%s://%s%s", getScheme(c), parsedURL.Host, newPath)
|
||||
return newRedirectURI, nil
|
||||
}
|
||||
|
||||
// generateSessionID generates a session ID
|
||||
func generateSessionID() string {
|
||||
return session.ID()
|
||||
}
|
||||
|
||||
// saveState saves the state to the session
|
||||
func saveState(providerID, sid, state string) error {
|
||||
return session.Global().ID(sid).SetWithEx(fmt.Sprintf("oauth_state_%s", providerID), state, 20*time.Minute)
|
||||
}
|
||||
|
||||
// saveRedirectURI saves the redirect URI to the session
|
||||
func saveRedirectURI(providerID, state, redirectURI string) error {
|
||||
key := fmt.Sprintf("oauth_redirect_uri_%s_%s", providerID, state)
|
||||
store, err := store.Get("__yao.oauth.cache")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.Set(key, redirectURI, 20*time.Minute)
|
||||
return nil
|
||||
}
|
||||
|
||||
// getRedirectURI gets the redirect URI from the session
|
||||
func getRedirectURI(providerID, state string) (string, error) {
|
||||
key := fmt.Sprintf("oauth_redirect_uri_%s_%s", providerID, state)
|
||||
store, err := store.Get("__yao.oauth.cache")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
value, ok := store.Get(key)
|
||||
if !ok || value == nil {
|
||||
return "", fmt.Errorf("redirect URI not found")
|
||||
}
|
||||
return value.(string), nil
|
||||
}
|
||||
|
||||
func removeRedirectURI(providerID, state string) error {
|
||||
key := fmt.Sprintf("oauth_redirect_uri_%s_%s", providerID, state)
|
||||
store, err := store.Get("__yao.oauth.cache")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.Del(key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeState(providerID, sid string) error {
|
||||
return session.Global().ID(sid).Del(fmt.Sprintf("oauth_state_%s", providerID))
|
||||
}
|
||||
|
||||
// validateState validates the state from the session
|
||||
func validateState(providerID, sid, state string) error {
|
||||
value, err := session.Global().ID(sid).Get(fmt.Sprintf("oauth_state_%s", providerID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if value != state {
|
||||
return fmt.Errorf("invalid state")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -377,7 +377,7 @@ func createPublicConfig(fullConfig *Config) Config {
|
|||
for key, value := range fullConfig.Form.Captcha.Options {
|
||||
// Only include non-sensitive fields
|
||||
switch key {
|
||||
case "sitekey", "theme", "size", "action", "cdata":
|
||||
case "sitekey", "theme", "size", "action", "cdata", "response_mode":
|
||||
// These are safe to expose to frontend
|
||||
publicOptions[key] = value
|
||||
case "secret":
|
||||
|
|
|
|||
57
openapi/utils/session.go
Normal file
57
openapi/utils/session.go
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SessionGuard is a guard that checks if the session ID is valid
|
||||
func SessionGuard(c *gin.Context) {
|
||||
sid := GetSessionID(c)
|
||||
if sid != "" {
|
||||
c.Set("__sid", sid)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSessionID retrieves the session ID from cookies
|
||||
// Tries to get session ID from different possible cookie names (with and without security prefixes)
|
||||
func GetSessionID(c *gin.Context) string {
|
||||
return getCookieWithPrefixes(c, "session_id")
|
||||
}
|
||||
|
||||
// GetAccessToken retrieves the access token from cookies
|
||||
func GetAccessToken(c *gin.Context) string {
|
||||
return getCookieWithPrefixes(c, "access_token")
|
||||
}
|
||||
|
||||
// GetRefreshToken retrieves the refresh token from cookies (checks /auth path)
|
||||
func GetRefreshToken(c *gin.Context) string {
|
||||
return getCookieWithPrefixes(c, "refresh_token")
|
||||
}
|
||||
|
||||
// getCookieWithPrefixes tries to get a cookie value from different possible names with security prefixes
|
||||
func getCookieWithPrefixes(c *gin.Context, baseName string) string {
|
||||
// Try to get cookie from different naming conventions (most secure first)
|
||||
cookieNames := []string{
|
||||
"__Host-" + baseName, // Most secure with __Host- prefix
|
||||
"__Secure-" + baseName, // With __Secure- prefix
|
||||
baseName, // Plain cookie name (fallback)
|
||||
}
|
||||
|
||||
for _, cookieName := range cookieNames {
|
||||
if value, err := c.Cookie(cookieName); err == nil && value != "" {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// HasValidSession checks if there's a valid session ID in the cookies
|
||||
func HasValidSession(c *gin.Context) bool {
|
||||
return GetSessionID(c) != ""
|
||||
}
|
||||
|
||||
// GetTokenFromCookie gets any named token from cookies with security prefix support
|
||||
func GetTokenFromCookie(c *gin.Context, tokenName string) string {
|
||||
return getCookieWithPrefixes(c, tokenName)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue