oauth

package
v0.24.0 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 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