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

oauth

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package oauth implements the OAuth2 / social-login flows that back a Laravel-Socialite-style "log in with Google/GitHub" experience, using only the standard library.

Design rules (secure-by-default):

  • The authorization-code flow always carries PKCE (RFC 7636, S256). The verifier is a high-entropy random value; only its SHA-256 challenge travels in the redirect, so an intercepted authorization code is useless without the verifier.
  • A CSRF state parameter binds the redirect to the originating session. NewState mints an opaque random value; SignedState mints a stateless, HMAC-signed value (with an embedded expiry) that the callback can verify without server-side storage, in constant time.
  • Token exchange and userinfo calls go through an injected *http.Client so transports can be stubbed in tests; nil falls back to http.DefaultClient.
  • Every outbound request honours the caller's context.Context.

A Provider holds the endpoint URLs plus a mapper that normalises the provider's userinfo payload into a common User. Prebuilt constructors fill the well-known endpoints for Google and GitHub; Generic accepts any OIDC/OAuth2 endpoint set.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrTokenResponse is returned when the token endpoint replies with a
	// non-2xx status or an OAuth2 error object.
	ErrTokenResponse = errors.New("oauth: token endpoint error")
	// ErrUserInfoResponse is returned when the userinfo endpoint replies with
	// a non-2xx status.
	ErrUserInfoResponse = errors.New("oauth: userinfo endpoint error")
	// ErrInvalidState is returned when a signed state fails verification
	// (bad signature, malformed, or expired).
	ErrInvalidState = errors.New("oauth: invalid state")
)

Errors returned by the flow.

Functions

func Challenge

func Challenge(verifier string) string

Challenge derives the S256 code challenge for a verifier: base64url(SHA-256(verifier)), without padding.

func NewState

func NewState() (string, error)

NewState returns an opaque, single-use CSRF state value. Store it in the session and compare it against the callback's state parameter.

func NewVerifier

func NewVerifier() (string, error)

NewVerifier returns a fresh PKCE code verifier: a high-entropy, base64url-without-padding string. Keep it in the session and pass it to Exchange after the callback.

Types

type Config

type Config struct {
	ClientID     string
	ClientSecret string
	RedirectURL  string
	// Scopes are space-joined into the "scope" parameter. The constructors
	// supply provider defaults when this is empty.
	Scopes []string
}

Config carries the per-application credentials and the redirect URL. It is embedded into a Provider by the prebuilt constructors.

type Provider

type Provider struct {
	Config
	// AuthURL is the authorization (consent) endpoint.
	AuthURL string
	// TokenURL is the token-exchange endpoint.
	TokenURL string
	// UserInfoURL is the endpoint that returns the authenticated identity.
	UserInfoURL string
	// contains filtered or unexported fields
}

Provider is a configured OAuth2 endpoint set plus the userinfo mapper. Build one with Google, GitHub, or Generic; the zero value is not usable.

Example

ExampleProvider runs the full authorization-code + PKCE flow against a fake OAuth2 provider served by httptest: it shows the consent URL carries the PKCE S256 challenge, then exchanges a code for a token and maps the userinfo response into a normalised User. Only stable fields are printed (never the random verifier/state).

package main

import (
	"context"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"

	"github.com/devituz/lagodev/auth/oauth"
)

func main() {
	// Fake provider: a token endpoint and an OIDC userinfo endpoint.
	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		switch r.URL.Path {
		case "/token":
			w.Header().Set("Content-Type", "application/json")
			fmt.Fprint(w, `{"access_token":"at-123","token_type":"Bearer","expires_in":3600}`)
		case "/userinfo":
			w.Header().Set("Content-Type", "application/json")
			fmt.Fprint(w, `{"sub":"909","name":"Ada Lovelace","email":"ada@example.com"}`)
		default:
			http.NotFound(w, r)
		}
	}))
	defer srv.Close()

	p := oauth.Generic(
		oauth.Config{ClientID: "demo-client", RedirectURL: "https://app.test/callback"},
		srv.URL+"/auth", srv.URL+"/token", srv.URL+"/userinfo", nil,
	).WithHTTPClient(srv.Client())

	// Build the consent URL with a fixed verifier so the output is
	// deterministic (a real app uses oauth.NewVerifier()).
	verifier := "fixed-test-verifier-value-for-example"
	challenge := oauth.Challenge(verifier)
	authURL := p.AuthCodeURL("state-xyz", challenge)
	fmt.Println("url has S256:", strings.Contains(authURL, "code_challenge_method=S256"))
	fmt.Println("url has challenge:", strings.Contains(authURL, "code_challenge="+challenge))

	// Callback returns a code; exchange it (presenting the verifier).
	tok, err := p.Exchange(context.Background(), "auth-code-abc", verifier)
	if err != nil {
		panic(err)
	}
	fmt.Println("token type:", tok.TokenType)
	fmt.Println("token valid:", tok.Valid())

	// Map the provider identity into the common User shape.
	user, err := p.User(context.Background(), tok)
	if err != nil {
		panic(err)
	}
	fmt.Println("user id:", user.ID)
	fmt.Println("user email:", user.Email)

}
Output:
url has S256: true
url has challenge: true
token type: Bearer
token valid: true
user id: 909
user email: ada@example.com

func Generic

func Generic(cfg Config, auth, token, userInfo string, mapper UserMapper) Provider

Generic returns a Provider for an arbitrary OIDC/OAuth2 endpoint set. auth, token, and userinfo are the three endpoint URLs; mapper normalises the userinfo payload. A nil mapper falls back to a best-effort OIDC mapper that reads the standard claims (sub, name, email, picture).

func GitHub

func GitHub(cfg Config) Provider

GitHub returns a Provider for GitHub's OAuth2 endpoints. When cfg.Scopes is empty it defaults to "read:user user:email". The token endpoint is asked for JSON via the Accept header set in Exchange.

func Google

func Google(cfg Config) Provider

Google returns a Provider for Google's OIDC endpoints. When cfg.Scopes is empty it defaults to "openid email profile". The userinfo response is the OIDC standard-claims shape.

func (*Provider) AuthCodeURL

func (p *Provider) AuthCodeURL(state, challenge string, extra ...url.Values) string

AuthCodeURL builds the consent URL the user is redirected to. state is the CSRF token (from NewState or SignedState); challenge is the PKCE S256 challenge for the verifier kept by the caller. extra adds or overrides query parameters (e.g. "access_type=offline", "prompt=consent").

func (*Provider) Exchange

func (p *Provider) Exchange(ctx context.Context, code, verifier string) (*Token, error)

Exchange swaps an authorization code for a Token, presenting the PKCE verifier the challenge was derived from. The request is form-encoded per RFC 6749 with the client secret in the body; providers that require basic auth still accept this form.

Errors: ErrTokenResponse wraps any non-2xx status or OAuth2 error object.

func (*Provider) User

func (p *Provider) User(ctx context.Context, tok *Token) (User, error)

User fetches the authenticated identity from the userinfo endpoint using the access token as a bearer credential, then maps it through the provider's UserMapper.

Errors: ErrUserInfoResponse wraps any non-2xx status.

func (Provider) WithHTTPClient

func (p Provider) WithHTTPClient(c *http.Client) Provider

WithHTTPClient returns a shallow copy of p that uses c for outbound requests. Pass a stubbed client in tests. A nil c restores the default.

type StateSigner

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

StateSigner mints and verifies stateless, HMAC-signed state values. Use it when you do not want to persist per-request state server-side: the signed value carries its own expiry and is rejected on tamper or timeout.

func NewStateSigner

func NewStateSigner(key []byte) *StateSigner

NewStateSigner returns a StateSigner keyed by key (reuse APP_KEY bytes; any length, 32 bytes recommended).

func (*StateSigner) SignedState

func (s *StateSigner) SignedState(ttl time.Duration) (string, error)

SignedState returns a stateless state value of the form "<nonce>.<expiry>.<signature>", where signature is base64url HMAC-SHA256 over "<nonce>.<expiry>". ttl<=0 produces a non-expiring value (expiry=0).

func (*StateSigner) VerifyState

func (s *StateSigner) VerifyState(state string) error

VerifyState checks a value produced by SignedState. It returns ErrInvalidState on a bad signature, malformed input, or a passed expiry. The signature is compared in constant time.

type Token

type Token struct {
	AccessToken  string
	TokenType    string
	RefreshToken string
	IDToken      string
	Expiry       time.Time // zero when the provider omits expires_in
}

Token is the result of a successful code exchange. IDToken holds the raw OIDC id_token JWT when the provider returns one (empty otherwise); this package does not verify it.

func (*Token) Valid

func (t *Token) Valid() bool

Valid reports whether the token has an access token that has not expired. A zero Expiry is treated as non-expiring.

type User

type User struct {
	ID     string
	Name   string
	Email  string
	Avatar string
	Raw    map[string]any
}

User is the normalised identity returned by User. Raw preserves the provider's full payload for fields outside the common shape.

type UserMapper

type UserMapper func(raw []byte) (User, error)

UserMapper normalises a provider's raw userinfo JSON into a User. The bytes are the decoded HTTP body; the mapper decides which fields populate the common shape and stashes the lot in User.Raw.

Jump to

Keyboard shortcuts

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