markbates / goth

Package goth provides a simple, clean, and idiomatic way to write authentication packages for Go web applications.
https://blog.gobuffalo.io/goth-needs-a-new-maintainer-626cd47ca37b
MIT License
5.42k stars 579 forks source link

Session returns empty array #567

Open cantutar opened 2 months ago

cantutar commented 2 months ago

Hi,

Im having a hard time to figure out why package is not working as expected.(Maybe im using wrong lol) but my current expected behavior put google and make it work with vite-react spa app. currently it redirects succesfully but not sets any session or cannot read any session with it so im using go 1.22.4 with;

(im not sure what causes the issue but after fighting straight 3 days with the package to make it work with gin and not getting a result is kinda bummer, i added redirects with sending vales to front as a workaround but its no use after no session to check basically. :( )

Edit: currently using with dev mode

my service for auth.go:

const (
    key    = "secret"                               // TODO: change this to a more secure key
    MaxAge = int(time.Hour * 24 * 30 / time.Second) // This converts time.Duration to seconds.
)

var BaseUrl string

// var PORT string

func GetBaseURL() string {
    BaseUrl = os.Getenv("BACKEND_URL")
    // PORT = os.Getenv("PORT")
    // return fmt.Sprintf("%s:%s", BaseUrl, PORT)
    return BaseUrl
}

// buildAuthCallbackURL builds the callback URL for the authentication provider
// based on the provider name and remember to change the api version if it changes.
func buildAuthCallbackURL(provider string) string {
    baseUrl := GetBaseURL()
    url := fmt.Sprintf("%s/api/v1/auth/%s/callback", baseUrl, provider)
    fmt.Println(url)
    return url
}

func NewAuth() {
    googleClientID := os.Getenv("GOOGLE_CLIENT_ID")
    googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")

    store := sessions.NewCookieStore([]byte(key))
    fmt.Println("MaxAge", MaxAge)
    store.MaxAge(MaxAge)

    store.Options.Path = "/"
    store.Options.HttpOnly = true
    store.Options.Secure = helpers.IsProduction()

    store.Options.SameSite = http.SameSiteLaxMode
    if helpers.IsProduction() {
        store.Options.SameSite = http.SameSiteStrictMode
    }

    gothic.Store = store

    goth.UseProviders(
        google.New(googleClientID, googleClientSecret, buildAuthCallbackURL("google")),
    )
}

public routes to handle callback etc: public.go:

var (
    once         sync.Once
    websiteURL   string
    dashURL      string
    onboardURL   string
    errorPageURL string
)

// initializeURLs is used to initialize all URLs once.
func initializeURLs() {
    websiteURL = os.Getenv("WEB_APP_URL")
    if websiteURL == "" {
        log.Fatal("WEB_APP_URL is not set, terminating application.")
    }
    dashURL = fmt.Sprintf("%s/", websiteURL)
    onboardURL = fmt.Sprintf("%s/onboarding", websiteURL)
    errorPageURL = fmt.Sprintf("%s/auth-error", websiteURL)
}

// getDashboardURL returns the dashboard URL.
func getDashboardURL() string {
    once.Do(initializeURLs) // Initialize the URLs
    return dashURL
}

// getOnboardingURL returns the onboarding URL.
func getOnboardingURL() string {
    once.Do(initializeURLs) // Initialize the URLs
    return onboardURL
}

// getErrorPageURL returns the error page URL with an error message.
func getErrorPageURL(errorMessage string) string {
    once.Do(initializeURLs)                      // Initialize the URLs
    safeMessage := url.QueryEscape(errorMessage) // Protection against XSS attacks
    return fmt.Sprintf("%s?error=%s", errorPageURL, safeMessage)
}

type contextKey string

const providerKey contextKey = "provider"

func GetAuthCallbackFunction(c *gin.Context) {
    provider := c.Param("provider")
    if provider == "" {
        utils.RespondWithError(c, http.StatusBadRequest, "Provider not specified", nil)
        return
    }

    c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), providerKey, provider))

    // Complete user authentication using gothic package
    gothUser, err := gothic.CompleteUserAuth(c.Writer, c.Request)
    if err != nil {
        log.Printf("Authentication failed: %s", err.Error()) // Log the error
        // Show a general error message to the user
        errorMessage := "Authentication failed. Please try again or contact support if the problem persists."
        c.Redirect(http.StatusFound, getErrorPageURL(errorMessage))
        return
    }

    // Now using provider and social ID to check user existence
    userID, exists, err := services.CheckUserExistAndGetID(gothUser.Provider, gothUser.UserID)
    if err != nil {
        utils.RespondWithError(c, http.StatusInternalServerError, "Database operation failed", err)
        return
    }

    // Redirect based on whether the user exists
    var redirectURL string
    if exists {
        // Redirect URL for existing users
        redirectURL = fmt.Sprintf("%s?userID=%s&provider=%s", getDashboardURL(), userID.String(), gothUser.Provider)
    } else {
        // Redirect to the onboarding URL for new users
        redirectURL = fmt.Sprintf("%s?provider=%s&email=%s&picture=%s&socialID=%s",
            getOnboardingURL(), gothUser.Provider, url.QueryEscape(gothUser.Email), url.QueryEscape(gothUser.AvatarURL), gothUser.UserID)
    }

    c.Redirect(http.StatusFound, redirectURL)
}

func LogoutHandler(c *gin.Context) {
    provider := c.Param("provider")

    c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), providerKey, provider))

    err := gothic.Logout(c.Writer, c.Request)
    if err != nil {
        utils.RespondWithError(c, http.StatusInternalServerError, "Failed to log out", err)
        return
    }

    c.Redirect(http.StatusFound, getDashboardURL())
}

func GetAuthHandler(c *gin.Context) {
    provider := c.Param("provider")

    // Attempt to complete the user authentication from an existing session
    if gothUser, err := gothic.CompleteUserAuth(c.Writer, c.Request); err == nil {
        // User is already authenticated, redirect to home or another appropriate page
        c.JSON(http.StatusFound, gin.H{
            "user":        gothUser,
            "redirectURL": getDashboardURL(), // Suggest where to redirect
        })
        return
    }

    // No valid session, need to authenticate

    // Add 'provider' to the query parameters of the request
    q := c.Request.URL.Query()
    q.Add("provider", provider)
    c.Request.URL.RawQuery = q.Encode()

    // Set the modified request back to context with the provider key
    c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), providerKey, provider))

    // Start the authentication process since no existing session is found
    gothic.BeginAuthHandler(c.Writer, c.Request)
}

// GetAuthStatus checks the user's authentication status.
func GetAuthStatus(c *gin.Context) {
    // Check the user's session
    gothUser, err := gothic.CompleteUserAuth(c.Writer, c.Request)
    fmt.Println(gothUser) // this returns an empty map
    if err != nil {
        utils.RespondWithSuccess(c, gin.H{"isAuthenticated": false}, "User is not authenticated")
    } else {
        utils.RespondWithSuccess(c, gin.H{"isAuthenticated": true, "user": gothUser}, "User is authenticated")
    }
}

routes:

    authGroup := router.Group("/auth")
    {
        authGroup.GET("/status", routes.GetAuthStatus)

        authProviderGroup := authGroup.Group("/:provider")
        {
            authProviderGroup.GET("/callback", routes.GetAuthCallbackFunction)
            authProviderGroup.GET("/logout", routes.LogoutHandler)
            authProviderGroup.GET("/", routes.GetAuthHandler)
        }
    }
Marin260 commented 2 months ago

@cantutar the latest version of gorilla sessions breaks this package.

I didn't really look into it to much, and I know this might not be the best solution but if you want a quick and dirty fix, downgrade the package version.

Related issue: Updating dependencies breaks login: 549

github.com/gorilla/sessions v1.2.2 // indirect
cantutar commented 1 month ago

Thanks for the heads up, I was in hurry to finish the project for tight deadline so switched auth0, maybe later try it again. 👍