oidc

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package oidc adds sign-in with an external identity provider — Google, GitHub, your company's SSO — on top of authit's own accounts.

What it does and does not verify

It runs the OAuth 2.0 authorization code flow with PKCE and state, and then asks the provider who the user is by calling the provider's userinfo endpoint with the freshly issued access token, over TLS.

It deliberately does not parse or verify an OpenID Connect ID token. Verifying one properly means fetching the provider's JWKS, caching it, handling key rotation, and getting issuer, audience, expiry and nonce checks all right — a meaningful amount of security-critical machinery to carry. A direct TLS call to the provider's userinfo endpoint establishes the same fact with the same trust, works for providers that are OAuth 2.0 but not OIDC (GitHub is the obvious one), and has no signature checking to get wrong. The cost is one HTTP round trip per sign-in.

If you need ID token verification specifically — because a provider asserts claims only there, or because you must avoid the round trip — this package is not it.

What it refuses to decide for you

Whether a social sign-in may take over an existing account is the security question in social login, and it is answered by LinkingPolicy — not by a default that quietly does the convenient thing. Read that type before configuring this package.

Index

Constants

View Source
const DefaultHTTPTimeout = 10 * time.Second

DefaultHTTPTimeout bounds a call to a provider.

Variables

View Source
var (
	// ErrUnknownProvider means no Provider with that ID was registered.
	ErrUnknownProvider = errors.New("authit/oidc: unknown provider")
	// ErrProviderMisconfigured means a Provider is missing something it
	// cannot work without. Returned from NewService, at startup.
	ErrProviderMisconfigured = errors.New("authit/oidc: provider misconfigured")
	// ErrProviderUnreachable means the provider could not be talked to, or
	// answered with something other than success. It is the provider's
	// fault or the network's, not the caller's: a 502, not a 401.
	ErrProviderUnreachable = errors.New("authit/oidc: provider unreachable")
	// ErrIdentity means the provider answered but the response could not be
	// turned into an Identity.
	ErrIdentity = errors.New("authit/oidc: provider returned an unusable identity")
	// ErrStateMismatch means the state parameter the provider sent back is
	// not the one this flow started with. That is a cross-site request
	// forgery attempt, or a badly resumed flow; either way the callback is
	// refused before any token is exchanged.
	ErrStateMismatch = errors.New("authit/oidc: state does not match")
	// ErrExchange means the authorization code could not be exchanged.
	ErrExchange = errors.New("authit/oidc: authorization code exchange failed")

	// ErrAccountNotLinked means the provider identity is genuine and
	// unknown, an authit account already exists with the same email
	// address, and the configured LinkingPolicy will not join them
	// automatically.
	//
	// This is not a failure. It is the safe outcome, and the host's move is
	// to tell the user "this address already has an account — sign in and
	// connect Google from your settings", then call Link.
	ErrAccountNotLinked = errors.New("authit/oidc: an account with this email exists but is not linked to this provider")
	// ErrSignUpDisabled means the identity is unknown, no account matches,
	// and Config.DisableSignUp is set.
	ErrSignUpDisabled = errors.New("authit/oidc: sign-up via this provider is disabled")
	// ErrProviderEmailUnverified means LinkingVerifiedEmail is configured
	// and the provider did not claim to have verified the address.
	ErrProviderEmailUnverified = errors.New("authit/oidc: provider did not verify the email address")
	// ErrAlreadyLinked means the provider identity is already linked, to
	// this user or another one.
	ErrAlreadyLinked = errors.New("authit/oidc: this provider identity is already linked to an account")
	// ErrLastCredential means unlinking would leave the user with no way to
	// sign in at all: no password, and no other linked provider.
	ErrLastCredential = errors.New("authit/oidc: cannot unlink the account's only remaining credential")
	// ErrNoEmail means the provider returned no email address, and one is
	// needed to create an account.
	ErrNoEmail = errors.New("authit/oidc: provider returned no email address")
)

Functions

func AuthorizationURLFor

func AuthorizationURLFor(a Authorization) (*url.URL, error)

AuthorizationURLFor is a convenience for hosts that build their own redirect: it parses Begin's URL so callers can inspect or adjust it.

Types

type Authorization

type Authorization struct {
	// URL is where to redirect the browser.
	URL string
	// State must be given back to Complete as ExpectedState. It is what
	// makes the callback verifiably the continuation of this request
	// rather than one an attacker started.
	State string
	// CodeVerifier is the PKCE secret. It must reach Complete and must
	// never reach the provider until the exchange.
	CodeVerifier string
}

Authorization is what Begin hands back: where to send the user, and the two values the callback must be checked against.

type Callback

type Callback struct {
	// Code is the authorization code from the callback query.
	Code string
	// State is the state parameter from the callback query.
	State string
	// ExpectedState is Authorization.State, as the host stored it.
	ExpectedState string
	// CodeVerifier is Authorization.CodeVerifier, as the host stored it.
	CodeVerifier string
}

Callback is what came back from the provider, plus what the host stored when the flow began.

type Config

type Config struct {
	// Linking decides whether an unknown identity may attach to an
	// existing account. Defaults to LinkingManual. Read LinkingPolicy.
	Linking LinkingPolicy
	// DisableSignUp refuses to create an account for an identity that
	// matches no existing user, for deployments where accounts are
	// provisioned some other way.
	DisableSignUp bool
	// ProviderTokenKey, when set, is a 32-byte AES-256-GCM key used to
	// encrypt the provider's own access and refresh tokens at rest, so
	// they can be used later to call the provider's API. Leave it nil and
	// the tokens are simply not kept, which is the right default: a
	// credential you do not store is one that cannot leak.
	ProviderTokenKey []byte
	// HTTPClient talks to the provider. Defaults to a client with a
	// 10-second timeout -- not http.DefaultClient, which has no timeout at
	// all and would let a hanging provider pin a request goroutine
	// indefinitely.
	HTTPClient *http.Client
	// AuditLogger receives sign-in, link and unlink events. Nil means
	// events are not recorded.
	AuditLogger audit.Logger
}

Config tunes the oidc package.

type Identity

type Identity struct {
	// Provider is the Provider.ID this came from.
	Provider string
	// Subject is the provider's stable identifier for the user. It is what
	// authit keys the link on — never the email address, which users
	// change and which two providers can both assert.
	Subject string
	// Email is normalised, and may be empty: not every provider returns
	// one, and GitHub in particular omits it unless the user has a public
	// address or the "user:email" scope was granted.
	Email string
	// EmailVerified is the provider's claim that it verified the address.
	// It is a claim, not a fact, and providers differ in how much it is
	// worth. See LinkingPolicy.
	EmailVerified bool
	// Name is a display name, if the provider gave one.
	Name string
	// Raw is the decoded userinfo response, for claims this struct does
	// not model.
	Raw map[string]any
}

Identity is who a provider says the user is.

type LinkingPolicy

type LinkingPolicy int

LinkingPolicy decides whether a social sign-in may attach itself to an authit account that already exists with the same email address.

This is the security decision in social login, which is why it has a named type and a deliberately inconvenient default rather than a boolean buried in a config struct.

The attack it governs: an attacker who can make a provider assert an email address they do not own — by registering it at a provider that never verifies addresses, or by changing their address at one that verified a different one — signs in, is silently linked to the victim's existing account, and is now the victim. Every "sign in with X" takeover works this way.

const (
	// LinkingManual never links automatically. An unknown provider
	// identity whose email matches an existing account is refused with
	// ErrAccountNotLinked, and the user has to sign in by the means they
	// already have and link deliberately with Link.
	//
	// This is the zero value, and it is the only policy that is safe
	// regardless of which providers you enable and how much you trust
	// them. It costs one extra step the first time.
	LinkingManual LinkingPolicy = iota

	// LinkingVerifiedEmail links automatically when the provider claims to
	// have verified the address, and refuses otherwise.
	//
	// It is only as good as that claim. "Verified" means the provider
	// checked at some point, not that the address still belongs to this
	// person, and providers vary in how seriously they mean it — a
	// self-hosted or badly run one can assert whatever it likes. Use this
	// when you control the providers, or trust them the way you would
	// trust them to hold your users' sessions. Do not use it with a
	// provider users can register at freely.
	LinkingVerifiedEmail
)

type Provider

type Provider struct {
	// ID names the provider in stored accounts. Changing it orphans every
	// existing link, so pick it once.
	ID string
	// ClientID and ClientSecret identify this application to the provider.
	ClientID     string
	ClientSecret string
	// AuthURL and TokenURL are the provider's OAuth 2.0 endpoints.
	AuthURL  string
	TokenURL string
	// UserInfoURL is called with the access token to learn who signed in.
	UserInfoURL string
	// Scopes are requested at authorization time. Whatever else you add,
	// the set must be enough for UserInfoURL to return a subject.
	Scopes []string
	// Map turns a decoded userinfo response into an Identity. Leave it nil
	// for the OIDC standard claims (sub, email, email_verified, name).
	Map func(raw map[string]any) (Identity, error)
	// AuthURLParams are extra query parameters added to the authorization
	// URL, e.g. {"prompt": "consent"} to force Google to re-issue a
	// refresh token.
	AuthURLParams map[string]string
}

Provider describes one external identity provider.

There is no OIDC discovery here: the endpoints are given explicitly, so a deployment cannot be redirected by a change at a well-known URL, and nothing has to be fetched before the first sign-in.

func GitHub

func GitHub(clientID, clientSecret string) Provider

GitHub returns a Provider for GitHub. GitHub is OAuth 2.0 rather than OIDC, so its userinfo response uses its own field names and its notion of a verified email needs a second call — see the mapper below for what that means in practice.

func Google

func Google(clientID, clientSecret string) Provider

Google returns a Provider for Google Sign-In. Supply your own client credentials.

type Result

type Result struct {
	// User is the authit account this sign-in resolved to. It is not a
	// session: minting one is the host's call, exactly as it is for
	// pat and device.
	User store.User
	// Account is the provider link, as it now stands.
	Account store.Account
	// Identity is what the provider said.
	Identity Identity
	// CreatedUser reports whether this sign-in created the account, so a
	// host can run its own onboarding.
	CreatedUser bool
}

Result is the outcome of a completed sign-in.

type Service

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

Service resolves external identities to authit users.

func NewService

func NewService(stores Stores, providers []Provider, cfg Config) (*Service, error)

NewService constructs a Service over the given providers.

Every provider is validated here rather than at the first sign-in: a missing client secret or a plain-HTTP endpoint should fail at startup, where somebody is watching.

func (*Service) Begin

func (s *Service) Begin(_ context.Context, providerID, redirectURI string) (Authorization, error)

Begin builds the provider's authorization URL along with the state and PKCE verifier the callback will be checked against.

Storing those two is the host's job, deliberately. They belong to one browser for one minute; a short-lived HttpOnly cookie is the usual home (see authithttp.CookieOptions for the attributes that matter). Keeping them here would mean another store port and a cleanup problem, to hold state that already has a natural place to live.

redirectURI must match what is registered with the provider exactly.

func (*Service) Complete

func (s *Service) Complete(ctx context.Context, providerID, redirectURI string, cb Callback) (Result, error)

Complete verifies the callback, exchanges the code, asks the provider who signed in, and resolves that to an authit user.

It does not issue any authit credential. Like pat.Resolve and device.PollDeviceToken, it answers "who is this" and leaves what to hand back — a session, a token pair, something of your own — to the host.

func (s *Service) Link(ctx context.Context, userID string, identity Identity, providerToken *oauth2.Token) (store.Account, error)

Link attaches a provider identity to an already-authenticated user.

This is the deliberate half of LinkingManual: the host authenticates the user by whatever means they already have, runs Begin/Complete's flow, and calls this. Because the user is proven first, no claim from the provider has to be trusted to decide whose account it is.

func (*Service) ListAccounts

func (s *Service) ListAccounts(ctx context.Context, userID string) ([]store.Account, error)

ListAccounts returns the providers a user has linked.

func (*Service) Provider

func (s *Service) Provider(id string) (Provider, error)

Provider returns the registered provider with the given id.

func (*Service) ProviderTokens

func (s *Service) ProviderTokens(a store.Account) (accessToken, refreshToken string, err error)

ProviderTokens decrypts the provider's own tokens for an account, so a host can call the provider's API as the user.

It returns empty strings when nothing was stored, which is the default.

func (s *Service) Unlink(ctx context.Context, userID, providerID string) error

Unlink removes a provider link.

It refuses to remove the last thing a user can sign in with: an account with no password and no other linked provider would still exist and be unreachable by anybody, including its owner. A host that wants that outcome should delete the account instead, which is its own decision to make.

type Stores

type Stores struct {
	Users    store.UserStore
	Accounts store.AccountStore
	// Tx is optional; see store.TxRunner. With it, creating a user and
	// linking their first provider account is atomic, so a crash between
	// the two cannot leave a user who exists and cannot sign in.
	Tx store.TxRunner
}

Stores are the ports the oidc package needs.

Jump to

Keyboard shortcuts

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