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 ¶
- type Options
- type Provider
- func (p *Provider) HTTPClient(ctx context.Context, current session.Session) (*http.Client, error)
- func (p *Provider) LoginURL() string
- func (p *Provider) Name() string
- func (p *Provider) Refresh(ctx context.Context, current session.Session, identity *auth.Identity) (*auth.Identity, error)
- func (p *Provider) Routes() []auth.Route
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 ¶
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 ¶
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
}
Output:
func (*Provider) LoginURL ¶
LoginURL returns ExternalURL + PathPrefix + "/login", the absolute URL that starts a login (use it as session.GuardOptions.LoginURL).
func (*Provider) Name ¶
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.