Skip to main content

Go — goth

The simplest Go integration. goth wraps provider discovery, state, PKCE, and token exchange into two route handlers.

Install

go get github.com/markbates/goth
go get github.com/markbates/goth/providers/openidConnect
go get github.com/gorilla/sessions

Full Example

Handles login, callback, logout (RP-initiated), and token refresh.

package main

import (
"net/http"
"net/url"
"os"

"github.com/gorilla/sessions"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/openidConnect"
)

// appStore holds the application session (user info + tokens).
// In production, use a persistent session store (e.g. Redis).
var appStore = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))

func init() {
gothic.Store = sessions.NewCookieStore([]byte(os.Getenv("SESSION_SECRET")))

goth.UseProviders(
openidConnect.New(
os.Getenv("ZERO_CLIENT_ID"),
os.Getenv("ZERO_CLIENT_SECRET"),
"https://yourapp.com/auth/callback",
"https://v1.0account.com",
"openid", "profile", "email", "offline_access",
),
)
}

// GET /auth/login?provider=openidConnect
func handleLogin(w http.ResponseWriter, r *http.Request) {
gothic.BeginAuthHandler(w, r)
}

// GET /auth/callback?provider=openidConnect
func handleCallback(w http.ResponseWriter, r *http.Request) {
user, err := gothic.CompleteUserAuth(w, r)
if err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}

// user.UserID, user.Email, user.Name, user.AvatarURL
// user.AccessToken, user.RefreshToken, user.IDToken, user.ExpiresAt
//
// goth keeps the full userinfo response in user.RawData, which is where
// custom fields are. A field titled "Pet name" arrives as petName.
var petName string
if fields, ok := user.RawData["https://0account.com/claims/fields"].(map[string]any); ok {
petName, _ = fields["petName"].(string)
}
_ = petName

// TODO: upsert user into your database by user.UserID

sess, _ := appStore.Get(r, "app")
sess.Values["user_id"] = user.UserID
sess.Values["email"] = user.Email
sess.Values["id_token"] = user.IDToken
sess.Values["access_token"] = user.AccessToken
sess.Values["refresh_token"] = user.RefreshToken
sess.Save(r, w)

http.Redirect(w, r, "/dashboard", http.StatusFound)
}

func handleLogout(w http.ResponseWriter, r *http.Request) {
sess, _ := appStore.Get(r, "app")
idToken, _ := sess.Values["id_token"].(string)

// Clear application session
sess.Options.MaxAge = -1
sess.Save(r, w)
gothic.Logout(w, r)

if idToken != "" {
// Server-to-server: terminate the session on 0account's side without a browser redirect.
http.PostForm("https://v1.0account.com/oauth/logout", url.Values{ //nolint:errcheck
"id_token_hint": {idToken},
})
}
http.Redirect(w, r, "/", http.StatusFound)
}

Refreshing the access token — only with offline_access

Access tokens last an hour. Skip this entirely if your app only needs to know who signed in.

// refreshTokens refreshes the access token using the stored refresh token.
// Call this when user.ExpiresAt is in the past before making API requests.
func refreshTokens(w http.ResponseWriter, r *http.Request) error {
sess, _ := appStore.Get(r, "app")
refreshToken, _ := sess.Values["refresh_token"].(string)
if refreshToken == "" {
return http.ErrNoCookie
}

provider, err := goth.GetProvider("openidConnect")
if err != nil {
return err
}
newToken, err := provider.RefreshToken(refreshToken)
if err != nil {
return err
}

sess.Values["access_token"] = newToken.AccessToken
if newToken.RefreshToken != "" {
sess.Values["refresh_token"] = newToken.RefreshToken // accept rotated refresh token
}
return sess.Save(r, w)
}
func main() {
http.HandleFunc("GET /auth/login", handleLogin)
http.HandleFunc("GET /auth/callback", handleCallback)
http.HandleFunc("GET /auth/logout", handleLogout)
http.ListenAndServe(":8080", nil)
}

goth generates and verifies state and PKCE automatically.

Environment Variables

ZERO_CLIENT_ID=your-0account-client-id
ZERO_CLIENT_SECRET=your-0account-client-secret
SESSION_SECRET=your-random-secret

Going further

This page covers sign-in with 0account and nothing else. For everything the library itself does — middleware, adapters, session storage, multiple providers — see the goth documentation.