Search code examples
gooauth-2.0oauthgoogle-oauth

No refresh token from Google OAuth2


I can't seem to obtain refresh token from oauth2.Exchange() for an app under testing.

config.Exchange(ctx, code, oauth2.AccessTypeOffline, oauth2.ApprovalForce)

returns an access token with no refresh_token. What's even more annoying is that oauth2.ApprovalForce does not prompt an OAuth consent screen for some reason. It only prompts for the very first time, and even after consenting for the first time, does not return a refresh token.

Checking the request through the debugger, the following is sent in the body of the request:

access_type=offline&client_id=...&client_secret=...&grant_type=authorization_code&prompt=consent&redirect_uri=...

So the library should be working fine. I've added a custom option

SetAuthURLParam("approval_prompt", "force")

and that also doesn't seem to work

Edit: Add code snippets

Initiating the OAuth flow:

func TestRegister(ctx context.Context, c *app.RequestContext) {
    var err error
    var req register.RegisterReq

    // ...

    var oauthState string
    if oauthState, err = getOAuthState(c); err != nil {
        c.JSON(400, map[string]any{"Error": err.Error()})
        return
    }

    url := config.AuthCodeURL(oauthState)
    c.Redirect(http.StatusTemporaryRedirect, []byte(url))
    return
}

func getOAuthState(c *app.RequestContext) (string, error) {
    expiration := 60 * 60 * 24
    cookieName := "authstate"
    b := make([]byte, 16)
    var err error
    if _, err = rand.Read(b); err != nil {
        return "", err
    }
    state := base64.URLEncoding.EncodeToString(b)
    c.SetCookie(cookieName, state, expiration, "/", "localhost", protocol.CookieSameSiteLaxMode, false, false)
    return state, nil
}

OAuth callback

func TestOauthCallback(ctx context.Context, c *app.RequestContext) {
    var err error
    var req register.OauthCallbackReq

    // ...

    queryArgs := &register.OauthCallbackReq{}
    if err = c.BindQuery(queryArgs); err != nil {
        c.JSON(500, map[string]any{"Error": err.Error()})
        return
    }

    var token *oauth2.Token
    if token, err = oauthAuthorize(ctx, queryArgs); err != nil {
        c.JSON(500, map[string]any{"Error": err.Error()})
        return
    }

    _ = token

    // ...
}

func oauthAuthorize(ctx context.Context, queryArgs *register.OauthCallbackReq) (*oauth2.Token, error) {
    var token *oauth2.Token
    var err error
    if token, err = config.Exchange(ctx, code, oauth2.AccessTypeOffline, oauth2.ApprovalForce); err != nil {
        return nil, err
    }
    return token, err
}

Edit 2

It seems that you're supposed to provide the oauth2.AccessTypeOffline and oauth2.ApprovalForce args in AuthCodeURL(), not when requesting for the access token 🤡


Solution

  • It seems that you're supposed to provide the oauth2.AccessTypeOffline and oauth2.ApprovalForce args in AuthCodeURL() when initiating the OAuth process, not when requesting for the access token 🤡

    url := googleproject.OauthConfig.AuthCodeURL(oauthState, oauth2.ApprovalForce, oauth2.AccessTypeOffline)
    

    @LindaLawton-DaImTo Thanks for the example