在 Go Gin 应用程序中管理 Google OAuth2 刷新令牌

问题描述 投票:0回答:1

我正在使用 Gin 框架在 Go 中开发一个 Web 应用程序,需要 Google OAuth2 身份验证和 Google Drive 集成。我当前的实现可以工作,但存在刷新令牌、令牌过期和频繁的重新身份验证提示等问题。

这是我的设置摘要:

OAuth2 设置:我使用 markbates/goth 和 gothic 来处理 Google OAuth2 身份验证。

令牌管理:我将访问令牌和刷新令牌保存到 token.json 文件中。

令牌刷新逻辑:我检查令牌是否过期并尝试使用刷新令牌刷新它。

问题: 未提供刷新令牌:有时,即使在提示用户同意后我也没有收到刷新令牌。

令牌过期处理:当访问令牌过期时,我的应用程序有时无法刷新令牌并需要用户重新进行身份验证。

频繁重新验证提示:每次验证时,应用程序都会提示我选择帐户并再次授予权限。

背景: 我确保设置 access_type=offline 并提示您最初同意获取刷新令牌。

我安全地保存访问令牌和刷新令牌。

在检查令牌是否过期时,我尝试使用刷新令牌刷新访问令牌。

问题: 如何确保我始终收到来自 Google 的刷新令牌?

在我当前的设置中处理令牌过期和刷新的最佳方法是什么?

是否有更安全的方式来管理令牌,尤其是刷新令牌,以避免重新验证提示?

如何避免每次身份验证时都提示选择帐户并授予权限?

任何指导或建议将不胜感激。谢谢!

func InitGoogleAuth() {
    err := godotenv.Load()
    if err != nil) {
        log.Fatalf("Error loading .env file: %v", err)
    }

    googleClientID := os.Getenv("GOOGLE_CLIENT_ID")
    googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET")
    googleScopes := []string{
        "openid",
        "profile",
        "email",
        "https://www.googleapis.com/auth/drive.file",
        "https://www.googleapis.com/auth/drive.readonly",
        "https://www.googleapis.com/auth/drive",
    }

    store := sessions.NewCookieStore([]byte(key))
    store.Options = &sessions.Options{
        Path:     "/",
        MaxAge:   MaxAge,
        HttpOnly: true,
        Secure:   IsProd,
    }
    gothic.Store = store

    log.Println("Initializing Google provider with Client ID:", googleClientID)
    log.Println("Using Scopes:", googleScopes)

    // Initialize Google provider with updated scopes and access type
    googleProvider := google.New(googleClientID, googleClientSecret, "http://localhost:8080/v1/auth/google/callback", googleScopes...)
    googleProvider.SetAccessType("offline")
    googleProvider.SetPrompt("consent")
    goth.UseProviders(googleProvider)

    log.Println("Google provider initialized with scopes and offline access.")
}

func AuthCallback(c *gin.Context) {
    provider := c.Param("provider")
    type contextKey string
    const providerKey contextKey = "provider"
    ctx := context.WithValue(c.Request.Context(), providerKey, provider)
    c.Request = c.Request.WithContext(ctx)

    session, err := gothic.Store.Get(c.Request, "gothic-session")
    if err != nil {
        log.Println("Error retrieving session in AuthCallback:", err)
    } else {
        log.Println("Session retrieved in AuthCallback, session ID:", session.ID)
    }

    user, err := gothic.CompleteUserAuth(c.Writer, c.Request)
    if err != nil {
        c.String(http.StatusBadRequest, fmt.Sprint(err))
        return
    }

    token := &oauth2.Token{
        AccessToken:  user.AccessToken,
        RefreshToken: user.RefreshToken,
        Expiry:       user.ExpiresAt,
    }

    saveToken("token.json", token)

    var existingUser models.User
    err = db.DB.QueryRow("SELECT id, username, password, email FROM users WHERE email = ?", user.Email).Scan(&existingUser.ID, &existingUser.Username, &existingUser.Password, &existingUser.Email)
    if err == nil {
        log.Println("User already exists:", existingUser.Email)
        // Generate tokens for existing user
        accessToken, err := utils.GenerateAccessToken(existingUser.Username)
        if err != nil {
            log.Println("Error generating access token:", err)
            c.String(http.StatusInternalServerError, fmt.Sprintf("Error generating access token: %v", err))
            return
        }

        refreshToken, err := utils.GenerateRefreshToken(existingUser.Username)
        if err != nil {
            log.Println("Error generating refresh token:", err)
            c.String(http.StatusInternalServerError, fmt.Sprintf("Error generating refresh token: %v", err))
            return
        }

        c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("http://localhost:5173/auth/google/callback?email=%s&username=%s&access_token=%s&refresh_token=%s&id=%d", existingUser.Email, existingUser.Username, accessToken, refreshToken, existingUser.ID))
        return
    }

    c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("http://localhost:5173/choose-username?email=%s", user.Email))
}
func refreshAccessToken(tok *oauth2.Token) (*oauth2.Token, error) {
    config := &oauth2.Config{
        ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
        ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
        Scopes:       []string{drive.DriveScope},
        Endpoint:     google.Endpoint,
    }

    // Use the refresh token to get a new access token
    newToken, err := config.TokenSource(context.Background(), tok).Token()
    if err != nil {
        return nil, fmt.Errorf("unable to refresh token: %v", err)
    }

    // Save the new token to the file
    saveToken("token.json", newToken)

    return newToken, nil
}

我尝试过的: 初始设置:使用 access_type=offline 和 SetPrompt("consent") 配置 Google OAuth2。

令牌管理:在 token.json 文件中保存访问令牌和刷新令牌。

令牌刷新逻辑:检查访问令牌是否过期并尝试使用刷新令牌刷新它。

处理过期令牌:尝试在服务器启动期间和进行 API 调用之前刷新令牌。

我的期望: 在用户同意的情况下一致地接收刷新令牌。

使用刷新令牌获取新的访问令牌,无需额外的用户提示。

无缝处理令牌过期,避免频繁的重新身份验证请求。

实际发生的事情: 未提供刷新令牌:有时,即使在提示用户同意后我也没有收到刷新令牌。

令牌过期处理:当访问令牌过期时,我的应用程序有时无法刷新令牌,需要用户重新进行身份验证。

频繁的重新验证提示:每次验证时,应用程序都会提示我选择帐户并再次授予权限,这不是我想要的用户体验。

go oauth-2.0 google-api token go-gin
1个回答
0
投票

每当用户授予您同意时,您都会获得刷新令牌,并且您可以将授权代码交换为访问令牌和刷新令牌(如果您请求离线访问)。

googleProvider.SetAccessType("offline")

如果您刷新刷新令牌以获得新的访问令牌,您将不会总是获得新的刷新令牌。 如果你创建了一个本机桌面应用程序,我认为你总是能得到它,但对于网络应用程序来说,你有时会得到它,但不是每次都能得到它。

确保始终存储最新的刷新令牌。

token.json 请记住,每个用户都需要一个。您确实应该将其与用户帐户一起存储在数据库中。 存储您上次获得的时间,然后您可以使用它来决定是否需要新的。

© www.soinside.com 2019 - 2024. All rights reserved.