Documentation
¶
Overview ¶
Package oauth is Stella's OAuth2 authorization server (issue #613). Stella issues scoped, revocable tokens to third-party clients acting on behalf of a user via authorization_code + PKCE and refresh_token. client_credentials is deliberately out of scope: Stella isolates on user_id and a userless token has no subject.
Guardrail: access tokens are OPAQUE stella_oat_ credentials minted through credential.Service.IssueOAuthAccess and resolved through the single credential front door. No JWT access token is ever issued, and the protocol details that are error-prone (PKCE verification, token response shape, error codes) lean on github.com/zitadel/oidc/v3/pkg/oidc rather than being hand-derived.
Index ¶
- Constants
- Variables
- type AccessIssuer
- type AuthCode
- type AuthCodeCreate
- type AuthorizeContext
- type AuthorizeRequest
- type AuthorizedApp
- type Client
- type ClientCreate
- type ClientRegistration
- type Config
- type PostgresStore
- func (o *PostgresStore) ConsumeCode(ctx context.Context, codeHash string) (AuthCode, bool, error)
- func (o *PostgresStore) ConsumeRefresh(ctx context.Context, publicID, replacedByID string) (RefreshRecord, bool, error)
- func (o *PostgresStore) CreateClient(ctx context.Context, c ClientCreate) (Client, error)
- func (o *PostgresStore) CreateCode(ctx context.Context, c AuthCodeCreate) error
- func (o *PostgresStore) CreateFamily(ctx context.Context, userID, clientID string) (string, error)
- func (o *PostgresStore) CreateOAuthAccess(ctx context.Context, rec credential.OAuthAccessRecord) (credential.OAuthAccessRecord, error)
- func (o *PostgresStore) CreateRefresh(ctx context.Context, r RefreshCreate) (RefreshRecord, error)
- func (o *PostgresStore) DisableClient(ctx context.Context, clientID, ownerUserID string) (int64, error)
- func (o *PostgresStore) GetClient(ctx context.Context, clientID string) (Client, error)
- func (o *PostgresStore) GetOAuthAccessByPublicID(ctx context.Context, publicID string) (credential.OAuthAccessRecord, error)
- func (o *PostgresStore) GetRefreshByPublicID(ctx context.Context, publicID string) (RefreshRecord, error)
- func (o *PostgresStore) ListAuthorizedApps(ctx context.Context, userID string) ([]AuthorizedApp, error)
- func (o *PostgresStore) ListClientsByOwner(ctx context.Context, ownerUserID string) ([]Client, error)
- func (o *PostgresStore) RevokeCodesForUserClient(ctx context.Context, userID, clientID string) error
- func (o *PostgresStore) RevokeFamiliesForUserClient(ctx context.Context, userID, clientID string) error
- func (o *PostgresStore) RevokeFamily(ctx context.Context, familyID string) error
- func (o *PostgresStore) TouchOAuthAccessLastUsed(ctx context.Context, id string) (int64, error)
- func (o *PostgresStore) UpdateClientSecret(ctx context.Context, clientID, ownerUserID, secretHash string) (int64, error)
- type RedirectError
- type RefreshCreate
- type RefreshRecord
- type Service
- func (s *Service) Authorize(ctx context.Context, req AuthorizeRequest) (*AuthorizeContext, error)
- func (s *Service) DisableClient(ctx context.Context, ownerUserID, clientID string) (bool, error)
- func (s *Service) Exchange(ctx context.Context, req TokenRequest) (*TokenResult, error)
- func (s *Service) IssueCode(ctx context.Context, userID string, req AuthorizeRequest, ...) (string, error)
- func (s *Service) ListAuthorizedApps(ctx context.Context, userID string) ([]AuthorizedApp, error)
- func (s *Service) ListClients(ctx context.Context, ownerUserID string) ([]Client, error)
- func (s *Service) RegisterClient(ctx context.Context, ownerUserID string, reg ClientRegistration) (Client, string, error)
- func (s *Service) RevokeGrant(ctx context.Context, userID, clientID string) error
- func (s *Service) RotateSecret(ctx context.Context, ownerUserID, clientID string) (string, error)
- type Store
- type TokenRequest
- type TokenResult
Constants ¶
const ( DefaultCodeTTL = 60 * time.Second DefaultAccessTTL = time.Hour DefaultRefreshTTL = 30 * 24 * time.Hour )
Default token lifetimes.
const ( ClientTypeConfidential = "confidential" ClientTypePublic = "public" )
ClientType values.
Variables ¶
var ( ErrClientNotFound = errors.New("oauth: client not found") ErrPublicClientNoSecret = errors.New("oauth: public clients have no secret") )
Functions ¶
This section is empty.
Types ¶
type AccessIssuer ¶
type AccessIssuer interface {
IssueOAuthAccess(ctx context.Context, userID, clientID string, scopes []string, refreshFamilyID string, ttl time.Duration) (plaintext string, err error)
}
AccessIssuer is the subset of credential.Service the flow depends on: minting the opaque access token. Kept as an interface so the flow never reaches around the front door to emit its own token.
type AuthCode ¶
type AuthCode struct {
ClientID string
UserID string
RedirectURI string
Scopes []string
CodeChallenge string
CodeChallengeMethod string
ExpiresAt time.Time
}
AuthCode is a stored authorization code (the hash, never the plaintext).
type AuthCodeCreate ¶
type AuthCodeCreate struct {
CodeHash string
ClientID string
UserID string
RedirectURI string
Scopes []string
CodeChallenge string
CodeChallengeMethod string
ExpiresAt time.Time
}
AuthCodeCreate is the input for persisting a fresh authorization code.
type AuthorizeContext ¶
type AuthorizeContext struct {
Client Client
Scopes []string // the concrete scopes that will be granted on approval
}
AuthorizeContext is the validated result used to render the consent screen.
type AuthorizeRequest ¶
type AuthorizeRequest struct {
ClientID string
RedirectURI string
ResponseType string
Scopes []string
State string
CodeChallenge string
CodeChallengeMethod string
}
AuthorizeRequest is the parsed /oauth/authorize query.
type AuthorizedApp ¶
type AuthorizedApp struct {
ClientID string
ClientName string
FamilyID string
Scopes []string
GrantedAt time.Time
}
AuthorizedApp is one entry in a user's authorized-apps list.
type Client ¶
type Client struct {
ClientID string
Name string
ClientSecretHash string
ClientType string
RedirectURIs []string
GrantTypes []string
Scopes []string
OwnerUserID string
Disabled bool
CreatedAt time.Time
}
Client is a registered third-party application.
type ClientCreate ¶
type ClientCreate struct {
ClientID string
Name string
ClientSecretHash string
ClientType string
RedirectURIs []string
GrantTypes []string
Scopes []string
OwnerUserID string
}
ClientCreate is the input for registering a client.
type ClientRegistration ¶
type ClientRegistration struct {
Name string
ClientType string
RedirectURIs []string
Scopes []string
}
ClientRegistration is the input for registering a client.
type Config ¶
type Config struct {
Store Store
Issuer AccessIssuer
Now func() time.Time
Logger *slog.Logger
CodeTTL time.Duration
AccessTTL time.Duration
RefreshTTL time.Duration
}
Config wires the flow's dependencies.
type PostgresStore ¶
type PostgresStore struct {
// contains filtered or unexported fields
}
PostgresStore adapts the sqlc queries to both credential.OAuthAccessStore (the opaque access-token front door) and oidc.Store (the authorization-server client/code/refresh storage). One adapter, two interfaces: access tokens stay owned by credential, while the flow tables stay in internal/oidc. The composition root constructs it once and hands it to credential.NewService (as OAuth) and oidc.NewService (as Store).
func NewPostgresStore ¶
func NewPostgresStore(db *pgxpool.Pool) *PostgresStore
NewPostgresStore builds the OAuth access + authorization-server store over the shared pool.
func (*PostgresStore) ConsumeCode ¶
func (*PostgresStore) ConsumeRefresh ¶
func (o *PostgresStore) ConsumeRefresh(ctx context.Context, publicID, replacedByID string) (RefreshRecord, bool, error)
func (*PostgresStore) CreateClient ¶
func (o *PostgresStore) CreateClient(ctx context.Context, c ClientCreate) (Client, error)
func (*PostgresStore) CreateCode ¶
func (o *PostgresStore) CreateCode(ctx context.Context, c AuthCodeCreate) error
func (*PostgresStore) CreateFamily ¶
func (*PostgresStore) CreateOAuthAccess ¶
func (o *PostgresStore) CreateOAuthAccess(ctx context.Context, rec credential.OAuthAccessRecord) (credential.OAuthAccessRecord, error)
func (*PostgresStore) CreateRefresh ¶
func (o *PostgresStore) CreateRefresh(ctx context.Context, r RefreshCreate) (RefreshRecord, error)
func (*PostgresStore) DisableClient ¶
func (*PostgresStore) GetOAuthAccessByPublicID ¶
func (o *PostgresStore) GetOAuthAccessByPublicID(ctx context.Context, publicID string) (credential.OAuthAccessRecord, error)
func (*PostgresStore) GetRefreshByPublicID ¶
func (o *PostgresStore) GetRefreshByPublicID(ctx context.Context, publicID string) (RefreshRecord, error)
func (*PostgresStore) ListAuthorizedApps ¶
func (o *PostgresStore) ListAuthorizedApps(ctx context.Context, userID string) ([]AuthorizedApp, error)
func (*PostgresStore) ListClientsByOwner ¶
func (*PostgresStore) RevokeCodesForUserClient ¶
func (o *PostgresStore) RevokeCodesForUserClient(ctx context.Context, userID, clientID string) error
func (*PostgresStore) RevokeFamiliesForUserClient ¶
func (o *PostgresStore) RevokeFamiliesForUserClient(ctx context.Context, userID, clientID string) error
func (*PostgresStore) RevokeFamily ¶
func (o *PostgresStore) RevokeFamily(ctx context.Context, familyID string) error
func (*PostgresStore) TouchOAuthAccessLastUsed ¶
func (*PostgresStore) UpdateClientSecret ¶
type RedirectError ¶
type RedirectError struct {
RedirectURI string
State string
Code string // oauth error code, e.g. invalid_scope
Description string
}
RedirectError is a flow error that should be reported to the client by redirecting to its redirect_uri with error/error_description (RFC 6749 4.1.2.1), as opposed to a validation error shown directly to the user (bad client / redirect_uri, where redirecting would be unsafe).
func (*RedirectError) Error ¶
func (e *RedirectError) Error() string
type RefreshCreate ¶
type RefreshCreate struct {
PublicID string
TokenHash string
ClientID string
UserID string
Scopes []string
FamilyID string
ExpiresAt time.Time
}
RefreshCreate is the input for persisting a fresh refresh token.
type RefreshRecord ¶
type RefreshRecord struct {
ID string
PublicID string
TokenHash string
ClientID string
UserID string
Scopes []string
FamilyID string
ExpiresAt time.Time
Consumed bool
FamilyRevoked bool
}
RefreshRecord is a stored refresh token. FamilyRevoked reflects the joined family's revoked state -- the single source of truth for revocation.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the authorization-server flow.
func NewService ¶
NewService builds the authorization-server flow.
func (*Service) Authorize ¶
func (s *Service) Authorize(ctx context.Context, req AuthorizeRequest) (*AuthorizeContext, error)
Authorize validates an authorization request. A returned *RedirectError means the caller should redirect to the client with the error; any other error is a hard validation failure to render as a page (never redirect to an unvalidated URI).
func (*Service) DisableClient ¶
DisableClient disables a client the caller owns. It reports whether a row changed (false = not found or already disabled).
func (*Service) Exchange ¶
func (s *Service) Exchange(ctx context.Context, req TokenRequest) (*TokenResult, error)
Exchange handles grant_type=authorization_code and refresh_token. It returns an *oidc.Error on protocol failure so the handler can serialize a spec error.
func (*Service) IssueCode ¶
func (s *Service) IssueCode(ctx context.Context, userID string, req AuthorizeRequest, grantedScopes []string) (string, error)
IssueCode creates a single-use authorization code after the user consents. The plaintext code is returned to redirect to the client; only its hash is stored.
func (*Service) ListAuthorizedApps ¶
ListAuthorizedApps returns the apps a user has active grants for.
func (*Service) ListClients ¶
ListClients returns the caller's registered clients.
func (*Service) RegisterClient ¶
func (s *Service) RegisterClient(ctx context.Context, ownerUserID string, reg ClientRegistration) (Client, string, error)
RegisterClient validates and creates a client. For confidential clients the plaintext secret is returned once (empty for public clients).
func (*Service) RevokeGrant ¶
RevokeGrant revokes a user's grant to a client: every family the user holds for that client (which covers all its refresh + access tokens via the resolve-time family check) plus any outstanding authorization codes, so an in-flight code cannot re-establish the grant. Idempotent.
type Store ¶
type Store interface {
// Clients.
CreateClient(ctx context.Context, c ClientCreate) (Client, error)
GetClient(ctx context.Context, clientID string) (Client, error)
ListClientsByOwner(ctx context.Context, ownerUserID string) ([]Client, error)
UpdateClientSecret(ctx context.Context, clientID, ownerUserID, secretHash string) (int64, error)
DisableClient(ctx context.Context, clientID, ownerUserID string) (int64, error)
// Authorization codes (single-use).
CreateCode(ctx context.Context, c AuthCodeCreate) error
// ConsumeCode atomically marks a code consumed and returns it. found=false
// means the code was unknown or already consumed (replay).
ConsumeCode(ctx context.Context, codeHash string) (code AuthCode, found bool, err error)
// RevokeCodesForUserClient burns any outstanding codes for a user+client, so a
// grant revoke cannot be undone by exchanging an in-flight code.
RevokeCodesForUserClient(ctx context.Context, userID, clientID string) error
// Refresh families (the revocation unit).
CreateFamily(ctx context.Context, userID, clientID string) (familyID string, err error)
// RevokeFamily kills one family (reuse detection or a single-grant revoke).
RevokeFamily(ctx context.Context, familyID string) error
// RevokeFamiliesForUserClient kills every family a user holds for a client
// (user-initiated app disconnect).
RevokeFamiliesForUserClient(ctx context.Context, userID, clientID string) error
// Refresh tokens (rotating within a family).
CreateRefresh(ctx context.Context, r RefreshCreate) (RefreshRecord, error)
// GetRefreshByPublicID returns the token joined with its family's revoked
// state (RefreshRecord.FamilyRevoked), so callers see family revocation at
// read time without a second query.
GetRefreshByPublicID(ctx context.Context, publicID string) (RefreshRecord, error)
// ConsumeRefresh atomically consumes the presented token and links it to its
// replacement. found=false means it was already consumed (replay); the caller
// then revokes the family. Family-level revocation is checked separately via
// GetRefreshByPublicID.
ConsumeRefresh(ctx context.Context, publicID, replacedByID string) (rec RefreshRecord, found bool, err error)
ListAuthorizedApps(ctx context.Context, userID string) ([]AuthorizedApp, error)
}
Store is the authorization-server storage the flow needs: clients, single-use authorization codes, refresh families (the revocation unit), and the rotating refresh tokens. Opaque access-token creation and resolution do NOT live here -- those go through credential.Service so the /api front door stays single. Revocation is a single flag on the family: killing a family (reuse detection or a user disconnect) revokes every access + refresh token under it, enforced at resolve time by joining the family. The concrete implementation is PostgresStore in store_pg.go.