title: OpenID Connect: inconsistent refresh token behaviour between different Identity Providers
body: <p>I'm implementing a Service Provider and currently observing inconsistent behaviour by different Identity Providers, regarding getting refresh tokens. I'm going to attach my Service Provider golang code in the bottom, in case it might help someone or clarify my question.</p>
  
  <p>I'm doing the authorization_code flow, by redirecting a login request to <code>*/authn</code> endpoint with query parameter <code>access_type=offline</code>. Then, the second step is receiving the authorization code on the callback endpoint, then calling the <code>*/token</code> endpoint to exchange the code for access and refresh tokens.</p>
  
  <p>I've tried this flow with 3 different Identity Providers and found the following results:</p>
  
  <ol>
  <li><strong>OneLogin</strong> (<a href="https://openid-connect.onelogin.com/oidc" rel="nofollow noreferrer">https://openid-connect.onelogin.com/oidc</a>): it was enough to add the query parameter <code>access_type=offline</code> to receive refresh tokens.</li>
  <li><strong>Okta</strong> (<a href="https://my-company.okta.com" rel="nofollow noreferrer">https://my-company.okta.com</a>): adding <code>access_type=offline</code> was not enough. I needed to add <code>offline_access</code> to Scopes parameter of the request in the first step (authn). This configuration also worked for OneLogin!</li>
  <li><p><strong>Google</strong> (<a href="https://accounts.google.com" rel="nofollow noreferrer">https://accounts.google.com</a>): With Google however, the scope <code>offline_access</code> is not supported and 400 BAD REQUEST is returned: </p>
  
  <blockquote>
    <p>Some requested scopes were invalid. {valid=[openid, <a href="https://www.googleapis.com/auth/userinfo.profile" rel="nofollow noreferrer">https://www.googleapis.com/auth/userinfo.profile</a>, <a href="https://www.googleapis.com/auth/userinfo.email]" rel="nofollow noreferrer">https://www.googleapis.com/auth/userinfo.email]</a>, invalid=[offline_access]} </p>
  </blockquote>
  
  <p>The only thing that worked with Google was removing <code>offline_access</code> from Scopes and adding the query parameter <code>prompt</code> with the value <code>consent</code>. This however, doesn't work with Okta or OneLogin...</p></li>
  </ol>
  
  <p>Am I missing something, or should I provide a custom authorization flow implementation to every IdP, in order to support refresh tokens? </p>
  
  <p>Seems pretty strange, considering that the protocol is fully specced out.</p>
  
  <pre><code>package openidconnect
  
  import (
      "context"
      "encoding/json"
      "net/http"
      "os"
  
      oidc "github.com/coreos/go-oidc"
      "golang.org/x/oauth2"
  )
  var oidcClientID = getEnv("****", "OIDC_CLIENT_ID")
  var oidcClientSecret = getEnv("****", "OIDC_CLIENT_SECRET")
  var oidcProvider = getEnv("****", "OIDC_PROVIDER")
  
  var oidcLoginURI = "/v1/oidc_login"
  var oidcCallbackURI = "/v1/oidc_callback"
  var hostname = getEnv("http://localhost:8080", "HOSTNAME")
  
  func getEnv(defaultValue, key string) string {
      val := os.Getenv(key)
      if val == "" {
          return defaultValue
      }
      return val
  }
  
  //InitOpenIDConnect initiates open ID connect SSO
  func InitOpenIDConnect() error {
      ctx := context.Background()
  
      provider, err := oidc.NewProvider(ctx, oidcProvider)
      if err != nil {
          return err
      }
  
      // Configure an OpenID Connect aware OAuth2 client.
      oidcConfig := oauth2.Config{
          ClientID:     oidcClientID,
          ClientSecret: oidcClientSecret,
          RedirectURL:  hostname + oidcCallbackURI,
  
          // Discovery returns the OAuth2 endpoints.
          Endpoint: provider.Endpoint(),
  
          // "openid" is a required scope for OpenID Connect flows.
  
          Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
          // TODO: For Okta and OneLogin, add oidc.ScopeOfflineAccess Scope for refresh token.
          // Removed for now because Google API returns 400 when it is set.
      }
  
      handleOIDCLogin(&amp;oidcConfig)
      handleOIDCCallback(provider, &amp;oidcConfig)
  
      return nil
  }
  
  var approvalPromptOption = oauth2.SetAuthURLParam("prompt", "consent")
  
  func handleOIDCLogin(config *oauth2.Config) {
      state := "foobar" // Don't do this in production.
  
      http.HandleFunc(oidcLoginURI, func(w http.ResponseWriter, r *http.Request) {
          // approval prompt option is required for getting refresh token from Google API
          redirectURL := config.AuthCodeURL(state, oauth2.AccessTypeOffline, approvalPromptOption)
          http.Redirect(w, r, redirectURL, http.StatusFound)
      })
  }
  
  func handleOIDCCallback(provider *oidc.Provider, config *oauth2.Config) {
      state := "foobar" // Don't do this in production.
      ctx := context.Background()
  
      http.HandleFunc(oidcCallbackURI, func(w http.ResponseWriter, r *http.Request) {
          if r.URL.Query().Get("state") != state {
              http.Error(w, "state did not match", http.StatusBadRequest)
              return
          }
  
          code := r.URL.Query().Get("code")
  
          oauth2Token, err := config.Exchange(ctx, code)
          if err != nil {
              http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
              return
          }
  
          tokenSource := config.TokenSource(ctx, oauth2Token)
          refreshedToken, err := tokenSource.Token()
          if err != nil {
              http.Error(w, "Failed to get refresh token: "+err.Error(), http.StatusInternalServerError)
              return
          }
  
          userInfo, err := provider.UserInfo(ctx, oauth2.StaticTokenSource(oauth2Token))
          if err != nil {
              http.Error(w, "Failed to get userinfo: "+err.Error(), http.StatusInternalServerError)
              return
          }
  
          resp := struct {
              OAuth2Token *oauth2.Token
              UserInfo    *oidc.UserInfo
          }{oauth2Token, userInfo}
          data, err := json.MarshalIndent(resp, "", "    ")
          if err != nil {
              http.Error(w, err.Error(), http.StatusInternalServerError)
              return
          }
          w.Write(data)
      })
  }
  </code></pre>