oidc

package
v0.60.1 Latest Latest
Warning

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

Go to latest
Published: Jul 21, 2026 License: AGPL-3.0 Imports: 21 Imported by: 0

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

View Source
const (
	DefaultCodeTTL    = 60 * time.Second
	DefaultAccessTTL  = time.Hour
	DefaultRefreshTTL = 30 * 24 * time.Hour
)

Default token lifetimes.

View Source
const (
	ClientTypeConfidential = "confidential"
	ClientTypePublic       = "public"
)

ClientType values.

Variables

View Source
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.

func (Client) IsPublic

func (c Client) IsPublic() bool

IsPublic reports whether the client has no secret and must use PKCE.

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 (o *PostgresStore) ConsumeCode(ctx context.Context, codeHash string) (AuthCode, bool, error)

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 (o *PostgresStore) CreateFamily(ctx context.Context, userID, clientID string) (string, error)

func (*PostgresStore) CreateOAuthAccess

func (*PostgresStore) CreateRefresh

func (o *PostgresStore) CreateRefresh(ctx context.Context, r RefreshCreate) (RefreshRecord, error)

func (*PostgresStore) DisableClient

func (o *PostgresStore) DisableClient(ctx context.Context, clientID, ownerUserID string) (int64, error)

func (*PostgresStore) GetClient

func (o *PostgresStore) GetClient(ctx context.Context, clientID string) (Client, error)

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 (o *PostgresStore) ListClientsByOwner(ctx context.Context, ownerUserID string) ([]Client, error)

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 (o *PostgresStore) TouchOAuthAccessLastUsed(ctx context.Context, id string) (int64, error)

func (*PostgresStore) UpdateClientSecret

func (o *PostgresStore) UpdateClientSecret(ctx context.Context, clientID, ownerUserID, secretHash string) (int64, error)

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

func NewService(cfg Config) *Service

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

func (s *Service) DisableClient(ctx context.Context, ownerUserID, clientID string) (bool, error)

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

func (s *Service) ListAuthorizedApps(ctx context.Context, userID string) ([]AuthorizedApp, error)

ListAuthorizedApps returns the apps a user has active grants for.

func (*Service) ListClients

func (s *Service) ListClients(ctx context.Context, ownerUserID string) ([]Client, error)

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

func (s *Service) RevokeGrant(ctx context.Context, userID, clientID string) error

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.

func (*Service) RotateSecret

func (s *Service) RotateSecret(ctx context.Context, ownerUserID, clientID string) (string, error)

RotateSecret issues a fresh secret for a confidential client the caller owns.

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.

type TokenRequest

type TokenRequest struct {
	GrantType    string
	ClientID     string
	ClientSecret string
	Code         string
	RedirectURI  string
	CodeVerifier string
	RefreshToken string
	Scope        []string
}

TokenRequest is the parsed /oauth/token form.

type TokenResult

type TokenResult struct {
	AccessToken  string
	RefreshToken string
	TokenType    string
	ExpiresIn    int
	Scopes       []string
}

TokenResult is the successful token response plus the plaintext tokens.

Jump to

Keyboard shortcuts

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