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 ¶
- Variables
- func Challenge(verifier string) string
- func NewState() (string, error)
- func NewVerifier() (string, error)
- type Config
- type Provider
- func (p *Provider) AuthCodeURL(state, challenge string, extra ...url.Values) string
- func (p *Provider) Exchange(ctx context.Context, code, verifier string) (*Token, error)
- func (p *Provider) User(ctx context.Context, tok *Token) (User, error)
- func (p Provider) WithHTTPClient(c *http.Client) Provider
- type StateSigner
- type Token
- type User
- type UserMapper
Examples ¶
Constants ¶
This section is empty.
Variables ¶
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 ¶
Challenge derives the S256 code challenge for a verifier: base64url(SHA-256(verifier)), without padding.
func NewState ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.
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.
type User ¶
User is the normalised identity returned by User. Raw preserves the provider's full payload for fields outside the common shape.
type UserMapper ¶
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.