Help us shape the future of Go! Take the 2026 Go Developer Survey

github

package
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package github is the GitHub OAuth provider: login with PKCE (S256) and a state bound to the server-side session, team checks (AllowedTeams, AdminTeams, RelevantTeams), logout with best-effort token revocation, a team refresh route and a session.Refresher, and an HTTP client that calls the GitHub API with the session's token.

The OAuth token never leaves the server: it is stored in the session under "github.token". The identity in the session carries Provider "github", Subject = the numeric GitHub user ID, Username = the login, and Teams = the user's teams that appear in AllowedTeams, AdminTeams or RelevantTeams ("org/team-slug", lower-case). Members of an AdminTeams team get auth.RoleAdmin.

Mount Routes on the server, put the Provider into session.GuardOptions.Refreshers so team changes reach running sessions within the RefreshInterval, and protect the application with the Guard. The package never logs; outcomes go to Options.Observer.

Example

Example wires the GitHub provider into a server: its routes, a Guard that sends unauthenticated browsers to the GitHub login and refreshes teams every minute, and an application route behind it.

package main

import (
	"fmt"
	"net/http"

	"github.com/govengers/auth"
	"github.com/govengers/auth/github"
	"github.com/govengers/auth/session"
)

func main() {
	backend, err := session.NewMemoryBackend(nil)
	if err != nil {
		fmt.Println(err)
		return
	}
	key, err := session.ParseKey("MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=") // from configuration in production
	if err != nil {
		fmt.Println(err)
		return
	}
	store, err := session.NewStore(&session.StoreOptions{Backend: backend, EncryptionKey: key})
	if err != nil {
		fmt.Println(err)
		return
	}
	provider, err := github.New(&github.Options{
		ClientID:      "Iv1.0123456789abcdef",
		ClientSecret:  "from-configuration",
		ExternalURL:   "https://app.example.com",
		AllowedTeams:  []string{"acme/developers"},
		AdminTeams:    []string{"acme/admins"},
		RelevantTeams: []string{"acme/ops"},
		Sessions:      store,
	})
	if err != nil {
		fmt.Println(err)
		return
	}
	guard, err := session.NewGuard(&session.GuardOptions{
		Store:      store,
		LoginURL:   provider.LoginURL(),
		Refreshers: []session.Refresher{provider},
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	serveMux := http.NewServeMux()
	auth.RegisterRoutes(serveMux, provider.Routes())
	serveMux.Handle("GET /auth/me", guard.RequireAPI()(auth.IdentityHandler()))
	serveMux.Handle("/", guard.RequireUI()(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		identity := auth.IdentityFrom(request.Context())
		fmt.Fprintf(writer, "hello %s, teams %v", identity.Username, identity.Teams)
	})))

	for _, route := range provider.Routes() {
		fmt.Println(route.Method, route.Path)
	}
	fmt.Println(provider.LoginURL())
}
Output:
GET /auth/github/login
GET /auth/github/callback
GET /auth/github/logout
POST /auth/github/refresh-teams
https://app.example.com/auth/github/login

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Options

type Options struct {
	ClientID, ClientSecret string          // required: the GitHub OAuth app
	Scopes                 []string        // default ["read:org"]; must include read:org or a scope that also lists the user's teams (write:org, admin:org, user, repo)
	AuthorizeURL           string          // default "https://github.com/login/oauth/authorize"
	TokenURL               string          // default "https://github.com/login/oauth/access_token"
	APIURL                 string          // default "https://api.github.com"; GHE e.g. https://ghe.example.com/api/v3; no query or fragment
	ExternalURL            string          // required: absolute public base URL; redirect URI = ExternalURL + PathPrefix + "/callback"
	PathPrefix             string          // default "/auth/github"; segments of A-Z a-z 0-9 . _ ~ -, no trailing "/", no "." or ".." segment
	PostLogoutURL          string          // default ExternalURL + "/"; path or absolute http(s) URL, no fragment; a path is made absolute with ExternalURL
	ErrorURL               string          // optional: callback failures (refusals and code=internal) redirect to ErrorURL?code=<code> instead of answering JSON; same rules as PostLogoutURL
	AllowedTeams           []string        // required, at least one, "org/team-slug", case-insensitive
	AdminTeams             []string        // members get auth.RoleAdmin (they still need an AllowedTeams membership)
	RelevantTeams          []string        // extra teams kept in Identity.Teams
	Sessions               *session.Store  // required; not SameSite Strict (the callback would arrive without the cookie)
	HTTPClient             *http.Client    // default 10 s timeout; token exchange, GitHub API, revocation (a redirect fails every call of the provider; HTTPClient(ctx, session) keeps this client's policy)
	OnLogin                auth.Hook       // optional: maps or refuses the identity before the session exists
	OnLogout               auth.LogoutHook // optional
	Observer               auth.Observer   // optional
}

Options configure a Provider.

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider is the GitHub OAuth provider. AuthorizeURL, TokenURL and APIURL must use https, except on loopback hosts (localhost, 127.0.0.0/8, ::1). It is safe for concurrent use.

func New

func New(options *Options) (*Provider, error)

New validates options and returns a Provider working on a copy of them: the caller's struct and slices are never modified, and changing them afterwards has no effect. New does no I/O.

func (*Provider) HTTPClient

func (p *Provider) HTTPClient(ctx context.Context, current session.Session) (*http.Client, error)

HTTPClient returns a client that sends the session's GitHub token to the GitHub API (APIURL's scheme and host) and to nothing else: a redirect to another host, e.g. an archive download, goes without it. The token source is static: an expired or revoked token makes GitHub answer 401 and the next refresh ends the session. A session without a GitHub token (another provider's, or none): ErrUnauthenticated. The client uses Options.HTTPClient's transport and timeout.

Example

ExampleProvider_HTTPClient calls the GitHub API with the session's token from an application handler.

package main

import (
	"net/http"

	"github.com/govengers/auth"
	"github.com/govengers/auth/github"
	"github.com/govengers/auth/session"
)

func main() {
	var (
		store    *session.Store   // the store the provider was built with
		provider *github.Provider // from github.New
	)
	repositories := http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
		current, err := store.Open(request)
		if err != nil {
			auth.WriteError(writer, err)
			return
		}
		client, err := provider.HTTPClient(request.Context(), current)
		if err != nil {
			auth.WriteError(writer, err)
			return
		}
		apiRequest, err := http.NewRequestWithContext(request.Context(), http.MethodGet, "https://api.github.com/user/repos", nil)
		if err != nil {
			auth.WriteError(writer, err)
			return
		}
		response, err := client.Do(apiRequest)
		if err != nil {
			auth.WriteError(writer, err)
			return
		}
		defer response.Body.Close()
		writer.WriteHeader(response.StatusCode)
	})
	_ = repositories
}

func (*Provider) LoginURL

func (p *Provider) LoginURL() string

LoginURL returns ExternalURL + PathPrefix + "/login", the absolute URL that starts a login (use it as session.GuardOptions.LoginURL).

func (*Provider) Name

func (p *Provider) Name() string

Name returns "github", the Identity.Provider of every identity this provider creates.

func (*Provider) Refresh

func (p *Provider) Refresh(ctx context.Context, current session.Session, identity *auth.Identity) (*auth.Identity, error)

Refresh implements session.Refresher: it refetches the user's teams with the session's GitHub token and returns a copy of identity with Teams replaced and auth.RoleAdmin added or removed; every other field stays. GitHub rejecting the token or no token: ErrUnauthenticated; no allowed team any more: ErrForbidden (the Guard ends the session on both); any other failure is a plain error. It does not save the identity.

func (*Provider) Routes

func (p *Provider) Routes() []auth.Route

Routes returns the provider's endpoints: GET {PathPrefix}/login, GET {PathPrefix}/callback, GET {PathPrefix}/logout and POST {PathPrefix}/refresh-teams. Every handler reports exactly one Outcome and answers with Cache-Control: no-store.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL