The
go-github library provides a Go client for GitHub’s REST API. Current releases use a versioned module path and accept a context for each API call. In v91,
NewClient takes functional options and returns both a client and an error.
There are several authentication choices. A personal access token works well for your own scripts. An OAuth app lets a user authorize access through a browser. For an integration installed on accounts or repositories, consider a
GitHub App and its more granular permissions.
Use a personal access token
Create a token with the permissions your task needs and provide it through the GITHUB_TOKEN environment variable. Do not put it in source code.
Add the client dependency to your module. The examples use the v91 API:
go get github.com/google/go-github/v91/github
Here is a complete program that prints the authenticated user’s login:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/google/go-github/v91/github"
)
func main() {
token := os.Getenv("GITHUB_TOKEN")
if token == "" {
log.Fatal("set GITHUB_TOKEN")
}
client, err := github.NewClient(github.WithAuthToken(token), github.WithTimeout(15*time.Second))
if err != nil {
log.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
user, _, err := client.Users.Get(ctx, "")
if err != nil {
log.Fatal(err)
}
fmt.Println(user.GetLogin())
}
Public API endpoints can also be called without authentication, subject to GitHub’s rate limits. An access token’s permissions determine which private data it can read or modify.
Authorize an OAuth app
Register an
OAuth app and configure its callback URL. Keep the client secret on the server. The client ID identifies the app and is not itself a secret.
The
GitHub web authorization flow redirects the user to GitHub, then returns a temporary code to your callback. Use a fresh random
state for each attempt, bind it to the initiating browser, and check it before exchanging the code. Use PKCE as well to bind the exchange to the original authorization request.
The following handler example uses Go 1.24’s
crypto/rand.Text and the PKCE helpers in
golang.org/x/oauth2. Add the dependency:
go get golang.org/x/oauth2
Save this in package main alongside your web server. Set GITHUB_CLIENT_ID, GITHUB_CLIENT_SECRET, and GITHUB_REDIRECT_URL before starting it. The redirect URL must match your registered HTTPS callback, ending in /github_oauth_cb.
package main
import (
"context"
"crypto/rand"
"crypto/subtle"
"fmt"
"net/http"
"os"
"time"
"github.com/google/go-github/v91/github"
"golang.org/x/oauth2"
githuboauth "golang.org/x/oauth2/github"
)
var oauthConf = &oauth2.Config{
ClientID: os.Getenv("GITHUB_CLIENT_ID"),
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
RedirectURL: os.Getenv("GITHUB_REDIRECT_URL"),
Scopes: []string{"read:user"},
Endpoint: githuboauth.Endpoint,
}
func oauthCookie(w http.ResponseWriter, name, value string, maxAge int) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: value,
Path: "/",
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
MaxAge: maxAge,
})
}
func handleGitHubLogin(w http.ResponseWriter, r *http.Request) {
state := rand.Text()
verifier := oauth2.GenerateVerifier()
oauthCookie(w, "__Host-oauth-state", state, 600)
oauthCookie(w, "__Host-oauth-verifier", verifier, 600)
w.Header().Set("Cache-Control", "no-store")
target := oauthConf.AuthCodeURL(state, oauth2.S256ChallengeOption(verifier))
http.Redirect(w, r, target, http.StatusSeeOther)
}
func handleGitHubCallback(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Referrer-Policy", "no-referrer")
state, stateErr := r.Cookie("__Host-oauth-state")
verifier, verifierErr := r.Cookie("__Host-oauth-verifier")
oauthCookie(w, "__Host-oauth-state", "", -1)
oauthCookie(w, "__Host-oauth-verifier", "", -1)
query := r.URL.Query()
if stateErr != nil || verifierErr != nil || state.Value == "" || verifier.Value == "" ||
subtle.ConstantTimeCompare([]byte(query.Get("state")), []byte(state.Value)) != 1 {
http.Error(w, "invalid OAuth state; start again", http.StatusBadRequest)
return
}
if query.Get("error") != "" || query.Get("code") == "" {
http.Error(w, "authorization was not completed", http.StatusBadRequest)
return
}
ctx, cancel := context.WithTimeout(r.Context(), 15*time.Second)
defer cancel()
token, err := oauthConf.Exchange(ctx, query.Get("code"), oauth2.VerifierOption(verifier.Value))
if err != nil {
http.Error(w, "token exchange failed", http.StatusBadGateway)
return
}
client, err := github.NewClient(github.WithHTTPClient(oauthConf.Client(ctx, token)))
if err != nil {
http.Error(w, "GitHub client setup failed", http.StatusInternalServerError)
return
}
user, _, err := client.Users.Get(ctx, "")
if err != nil {
http.Error(w, "GitHub request failed", http.StatusBadGateway)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprintf(w, "Authorized as %s\n", user.GetLogin())
}
Register the handlers on your existing HTTPS server (method-qualified routes require Go 1.22 or later):
mux.HandleFunc("GET /login", handleGitHubLogin)
mux.HandleFunc("GET /github_oauth_cb", handleGitHubCallback)
The cookies deliberately require HTTPS, including during local testing. The __Host- prefix prevents a parent-domain cookie from substituting these values in supporting browsers. This small example allows one pending login per browser; starting another replaces the first attempt’s cookies. A larger application can store expiring, single-use OAuth attempts in its server-side session store.
This demonstrates authorization and one API request. It does not create a persistent login session for your application. After verifying the user, create your own session using the user’s stable numeric GitHub ID rather than a changeable login name.
Store tokens only when needed
If later requests need GitHub access, store the token on the server with access controls appropriate for a credential. Do not log it or expose it to browser JavaScript. JSON serialization is convenient, but does not encrypt the token:
func tokenToJSON(token *oauth2.Token) ([]byte, error) {
return json.Marshal(token)
}
func tokenFromJSON(data []byte) (*oauth2.Token, error) {
var token oauth2.Token
if err := json.Unmarshal(data, &token); err != nil {
return nil, err
}
return &token, nil
}
These helpers need encoding/json and golang.org/x/oauth2. Account for expiration, refresh, and revocation according to your app’s token type. Do not keep an OAuth client tied to a completed HTTP request’s context for future background work; construct it using a context with the appropriate lifetime.
The
original sample uses older APIs. Current code should pass contexts explicitly and should never reuse a fixed global OAuth state.