Documentation
¶
Overview ¶
Package authpg implements the auth stores over the tables `rig setup-project` creates.
The auth packages take interfaces because an application might keep its accounts somewhere unexpected. Almost none do: they run the migrations rig wrote, against the schema rig designed, and then have to write four hundred lines of SQL to connect the two. This is that SQL, written once.
If you changed the foundation schema, implement the interfaces yourself. If you did not, wiring authentication is New and nothing else.
Index ¶
- type APIKeyStore
- func (s *APIKeyStore) ByKeyID(ctx context.Context, keyID string) (*apikey.Key, error)
- func (s *APIKeyStore) Find(ctx context.Context, tenantID, id uuid.UUID) (*apikey.Key, error)
- func (s *APIKeyStore) InTx(ctx context.Context, fn func(ctx context.Context) error) error
- func (s *APIKeyStore) Insert(ctx context.Context, k *apikey.Key) error
- func (s *APIKeyStore) List(ctx context.Context, tenantID uuid.UUID) ([]*apikey.Key, error)
- func (s *APIKeyStore) Revoke(ctx context.Context, tenantID, id uuid.UUID, at time.Time) error
- func (s *APIKeyStore) SetExpiry(ctx context.Context, tenantID, id uuid.UUID, at time.Time) error
- func (s *APIKeyStore) TouchLastUsed(ctx context.Context, id uuid.UUID, at time.Time) error
- type AccountStore
- func (s *AccountStore) AccountForIdentity(ctx context.Context, tenantID, identityID uuid.UUID) (*account.Account, error)
- func (s *AccountStore) AccountsForIdentity(ctx context.Context, identityID uuid.UUID) ([]*account.Account, error)
- func (s *AccountStore) ChargeVerificationAttempt(ctx context.Context, id uuid.UUID, max int, at time.Time) (int, bool, error)
- func (s *AccountStore) ConsumeVerification(ctx context.Context, id uuid.UUID, at time.Time) (bool, error)
- func (s *AccountStore) CreateVerification(ctx context.Context, v *account.Verification) error
- func (s *AccountStore) FindByID(ctx context.Context, tenantID, id uuid.UUID) (*account.Account, error)
- func (s *AccountStore) FindIdentityByEmail(ctx context.Context, lowercased string) (*account.Identity, error)
- func (s *AccountStore) FindIdentityByID(ctx context.Context, id uuid.UUID) (*account.Identity, error)
- func (s *AccountStore) InTx(ctx context.Context, fn func(ctx context.Context) error) error
- func (s *AccountStore) Insert(ctx context.Context, a *account.Account) error
- func (s *AccountStore) InsertIdentity(ctx context.Context, i *account.Identity) error
- func (s *AccountStore) InsertTenant(ctx context.Context, t *account.Tenant) error
- func (s *AccountStore) InvitationByID(ctx context.Context, id uuid.UUID) (*account.Invitation, error)
- func (s *AccountStore) InvitationByToken(ctx context.Context, hash []byte) (*account.Invitation, error)
- func (s *AccountStore) InvitationSlot(ctx context.Context, tenantID, identityID uuid.UUID) (*account.Invitation, error)
- func (s *AccountStore) InvitationsForIdentity(ctx context.Context, identityID uuid.UUID) ([]account.Invitation, error)
- func (s *AccountStore) LastAccountForIdentity(ctx context.Context, identityID uuid.UUID) (*account.Account, error)
- func (s *AccountStore) LiveVerification(ctx context.Context, identityID uuid.UUID, kind account.VerificationKind) (*account.Verification, error)
- func (s *AccountStore) MarkIdentityVerified(ctx context.Context, identityID uuid.UUID, at time.Time) error
- func (s *AccountStore) PendingInvitations(ctx context.Context, tenantID uuid.UUID) ([]account.Invitation, error)
- func (s *AccountStore) RevokeVerification(ctx context.Context, id uuid.UUID, at time.Time) (bool, error)
- func (s *AccountStore) TenantDomains(ctx context.Context, tenantID uuid.UUID) ([]string, error)
- func (s *AccountStore) TenantsForIdentity(ctx context.Context, identityID uuid.UUID) ([]account.Membership, error)
- func (s *AccountStore) VerificationByHash(ctx context.Context, hash []byte) (*account.Verification, error)
- func (s *AccountStore) VerificationByID(ctx context.Context, id uuid.UUID) (*account.Verification, error)
- type Log
- type OAuthStore
- func (s *OAuthStore) FindAccount(ctx context.Context, tenantID, identityID uuid.UUID) (uuid.UUID, error)
- func (s *OAuthStore) FindIdentityByEmail(ctx context.Context, lowercased string) (uuid.UUID, error)
- func (s *OAuthStore) FindLink(ctx context.Context, provider, subject string) (*oauth.Link, error)
- func (s *OAuthStore) JoinTenant(ctx context.Context, in oauth.JoinInput) (uuid.UUID, error)
- func (s *OAuthStore) LinkIdentity(ctx context.Context, in oauth.LinkInput) (*oauth.Link, error)
- func (s *OAuthStore) ProvisionIdentity(ctx context.Context, in oauth.ProvisionInput) (*oauth.Link, error)
- type OutboxStore
- func (s *OutboxStore) Abandon(ctx context.Context, ids []uuid.UUID, by uuid.UUID, _ time.Time) error
- func (s *OutboxStore) Claim(ctx context.Context, by uuid.UUID, now time.Time, ttl time.Duration, limit int) ([]account.Delivery, error)
- func (s *OutboxStore) Enqueue(ctx context.Context, d *account.Delivery) error
- func (s *OutboxStore) MarkFailed(ctx context.Context, id uuid.UUID, reason string, _ time.Time) error
- func (s *OutboxStore) MarkSent(ctx context.Context, id uuid.UUID, at time.Time) error
- func (s *OutboxStore) MarkSkipped(ctx context.Context, id uuid.UUID, reason string, _ time.Time) error
- func (s *OutboxStore) Prune(ctx context.Context, olderThan time.Duration, now time.Time) (int, error)
- func (s *OutboxStore) ReleaseClaims(ctx context.Context, by uuid.UUID, _ time.Time) (int, error)
- func (s *OutboxStore) Retry(ctx context.Context, id uuid.UUID, at time.Time, reason string, _ time.Time) error
- func (s *OutboxStore) RotateToken(ctx context.Context, verificationID uuid.UUID, hash []byte, ...) (bool, error)
- type SessionStore
- func (s *SessionStore) Families(ctx context.Context, tenantID, accountID uuid.UUID) ([]session.Family, error)
- func (s *SessionStore) Find(ctx context.Context, id uuid.UUID) (*session.Token, error)
- func (s *SessionStore) FindIdentitySession(ctx context.Context, id uuid.UUID) (*session.Identity, error)
- func (s *SessionStore) InTx(ctx context.Context, fn func(ctx context.Context) error) error
- func (s *SessionStore) Insert(ctx context.Context, t *session.Token) error
- func (s *SessionStore) InsertIdentitySession(ctx context.Context, in *session.Identity) error
- func (s *SessionStore) Lock(ctx context.Context, id uuid.UUID) (*session.Token, error)
- func (s *SessionStore) MarkRotated(ctx context.Context, id uuid.UUID, at time.Time) error
- func (s *SessionStore) RevokeFamily(ctx context.Context, rootID uuid.UUID, at time.Time) ([]uuid.UUID, error)
- func (s *SessionStore) RevokeIdentitySession(ctx context.Context, id uuid.UUID, at time.Time) error
- func (s *SessionStore) RevokeIdentitySessionsFor(ctx context.Context, identityID uuid.UUID, at time.Time) (int, error)
- func (s *SessionStore) TenantFamilies(ctx context.Context, tenantID uuid.UUID) ([]session.Family, error)
- type Stores
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type APIKeyStore ¶
type APIKeyStore struct {
// contains filtered or unexported fields
}
APIKeyStore keeps machine credentials in rig_api_key.
func (*APIKeyStore) ByKeyID ¶
ByKeyID implements apikey.Store.
func (*APIKeyStore) Find ¶
Find implements apikey.Store.
func (*APIKeyStore) InTx ¶
InTx implements apikey.Store.
func (*APIKeyStore) Insert ¶
Insert implements apikey.Store.
func (*APIKeyStore) List ¶
List implements apikey.Store.
func (*APIKeyStore) Revoke ¶
Revoke implements apikey.Store.
func (*APIKeyStore) SetExpiry ¶
SetExpiry implements apikey.Store.
func (*APIKeyStore) TouchLastUsed ¶
TouchLastUsed implements apikey.Store.
type AccountStore ¶
type AccountStore struct {
// contains filtered or unexported fields
}
AccountStore keeps identities, accounts, credentials, and single-use links.
Two tables where there used to be one: identity is the person, global and unique on their address, and account is that person inside one tenant. Every query below belongs to one side or the other, and the ones that cross say so.
func (*AccountStore) AccountForIdentity ¶
func (s *AccountStore) AccountForIdentity(ctx context.Context, tenantID, identityID uuid.UUID) (*account.Account, error)
AccountForIdentity implements account.Store.
func (*AccountStore) AccountsForIdentity ¶
func (s *AccountStore) AccountsForIdentity(ctx context.Context, identityID uuid.UUID) ([]*account.Account, error)
AccountsForIdentity implements account.Store.
The one query in this package with no tenant predicate, deliberately: its whole purpose is to reach the tenants the caller is not in. It is unexported business — nothing here is returned over HTTP — and what it feeds is revoking every session a person has.
func (*AccountStore) ChargeVerificationAttempt ¶ added in v0.6.0
func (s *AccountStore) ChargeVerificationAttempt( ctx context.Context, id uuid.UUID, max int, at time.Time, ) (int, bool, error)
ChargeVerificationAttempt implements account.Store.
One statement, which is the whole requirement. A read followed by a write is a window two concurrent guesses both fit through, and against a six-digit secret that window is free attempts.
The revocation is in the same UPDATE rather than a second one, so a code that has just run out of attempts is dead by the time the row is unlocked. Revoked rather than a state of its own: account.Verification.Usable already refuses a revoked row and so does the mail queue's rotation, so nothing else has to learn that a ceiling exists.
func (*AccountStore) ConsumeVerification ¶
func (s *AccountStore) ConsumeVerification(ctx context.Context, id uuid.UUID, at time.Time) (bool, error)
ConsumeVerification implements account.Store.
The `consumed_at IS NULL` clause is what makes it single-use under concurrency: two requests racing to redeem one link both run the UPDATE, and exactly one of them affects a row.
func (*AccountStore) CreateVerification ¶
func (s *AccountStore) CreateVerification(ctx context.Context, v *account.Verification) error
CreateVerification implements account.Store.
func (*AccountStore) FindByID ¶
func (s *AccountStore) FindByID(ctx context.Context, tenantID, id uuid.UUID) (*account.Account, error)
FindByID implements account.Store.
func (*AccountStore) FindIdentityByEmail ¶
func (s *AccountStore) FindIdentityByEmail(ctx context.Context, lowercased string) (*account.Identity, error)
FindIdentityByEmail implements account.Store.
The comparison is lower(email_address) with no tenant in sight, which matches the unique index the foundation creates — so this is an index lookup rather than a scan, and it agrees with what the database will enforce on insert.
func (*AccountStore) FindIdentityByID ¶
func (s *AccountStore) FindIdentityByID(ctx context.Context, id uuid.UUID) (*account.Identity, error)
FindIdentityByID implements account.Store.
func (*AccountStore) InTx ¶
InTx implements account.Store.
func (*AccountStore) Insert ¶
Insert implements account.Store.
The audit columns are written from the input rather than defaulted, because the caller knows who asked and this does not: a provisioning request through an API key has both an account and a key to record, and a seed has neither.
func (*AccountStore) InsertIdentity ¶
InsertIdentity implements account.Store.
func (*AccountStore) InsertTenant ¶
InsertTenant implements account.Store.
func (*AccountStore) InvitationByID ¶ added in v0.6.0
func (s *AccountStore) InvitationByID(ctx context.Context, id uuid.UUID) (*account.Invitation, error)
InvitationByID implements account.Store.
func (*AccountStore) InvitationByToken ¶ added in v0.6.0
func (s *AccountStore) InvitationByToken(ctx context.Context, hash []byte) (*account.Invitation, error)
InvitationByToken implements account.Store.
func (*AccountStore) InvitationSlot ¶ added in v0.6.0
func (s *AccountStore) InvitationSlot( ctx context.Context, tenantID, identityID uuid.UUID, ) (*account.Invitation, error)
InvitationSlot implements account.Store.
The predicate is the unique index's, minus the two conditions invitations already applies: rig_identity_verification_live_invitation_key is (invited_to_tenant_id, identity_id) where the row is neither consumed nor revoked. Expiry is not in the index and must not be here — an expired invitation still occupies the slot, and this is what finds it so that inviting again can clear it.
func (*AccountStore) InvitationsForIdentity ¶
func (s *AccountStore) InvitationsForIdentity(ctx context.Context, identityID uuid.UUID) ([]account.Invitation, error)
InvitationsForIdentity implements account.Store.
No tenant predicate, deliberately, and it is the one query in this package where that is the whole point: somebody who belongs to no tenant is asking which ones have asked for them. Scoping it by tenant would be scoping it to a tenant they are not in yet.
func (*AccountStore) LastAccountForIdentity ¶ added in v0.5.0
func (s *AccountStore) LastAccountForIdentity( ctx context.Context, identityID uuid.UUID, ) (*account.Account, error)
LastAccountForIdentity implements account.Store.
Session roots only — `root_token_id = id` — because a root is one per sign-in where the rest of the family is one per rotation, and "where was I last" is a question about sign-ins. That predicate is also what the partial index in the last_tenant migration is on, so this is an index probe per account rather than a sort over every token an account has ever held. Consumed tokens are kept rather than deleted, so that difference only grows.
The two joins filter what the caller would have to filter anyway: an account or a tenant that has gone away is not somewhere to land, and answering with one would land somebody where AccountStore.TenantsForIdentity does not list.
func (*AccountStore) LiveVerification ¶ added in v0.6.0
func (s *AccountStore) LiveVerification( ctx context.Context, identityID uuid.UUID, kind account.VerificationKind, ) (*account.Verification, error)
LiveVerification implements account.Store.
func (*AccountStore) MarkIdentityVerified ¶
func (s *AccountStore) MarkIdentityVerified(ctx context.Context, identityID uuid.UUID, at time.Time) error
MarkIdentityVerified implements account.Store.
func (*AccountStore) PendingInvitations ¶
func (s *AccountStore) PendingInvitations(ctx context.Context, tenantID uuid.UUID) ([]account.Invitation, error)
PendingInvitations implements account.Store.
Live means not consumed, not revoked and not expired. It used to also mean "and for an account that is still there", because an account existed from the moment somebody was invited; there is none now until they accept, which is what makes this a list of people who are *not* in the tenant.
func (*AccountStore) RevokeVerification ¶
func (s *AccountStore) RevokeVerification(ctx context.Context, id uuid.UUID, at time.Time) (bool, error)
RevokeVerification implements account.Store.
The two IS NULL clauses are what make it safe under concurrency: somebody accepting an invitation at the same moment sets consumed_at, and exactly one of the two statements affects a row.
func (*AccountStore) TenantDomains ¶
TenantDomains implements account.Store.
func (*AccountStore) TenantsForIdentity ¶
func (s *AccountStore) TenantsForIdentity(ctx context.Context, identityID uuid.UUID) ([]account.Membership, error)
TenantsForIdentity implements account.Store.
The one join in this package. It has no tenant predicate for the same reason AccountStore.AccountsForIdentity has none — the question is which tenants a person belongs to — and a deleted tenant is left out, because being a member of something that no longer exists is not somewhere anybody can go.
func (*AccountStore) VerificationByHash ¶
func (s *AccountStore) VerificationByHash(ctx context.Context, hash []byte) (*account.Verification, error)
VerificationByHash implements account.Store.
func (*AccountStore) VerificationByID ¶
func (s *AccountStore) VerificationByID(ctx context.Context, id uuid.UUID) (*account.Verification, error)
VerificationByID implements account.Store.
type Log ¶
type Log struct {
// contains filtered or unexported fields
}
Log is rig_auth_log: the writer, the reader, and the prune.
One type for three contracts that a caller sees separately, because they are one table and splitting them here would mean three structs holding the same connection. Which of the three a caller is given is the interface it takes: authlog.Log for the twenty call sites that record, authlog.Reader for the endpoint that shows the trail, authlog.Pruner for the retention task.
func (*Log) Prune ¶
Prune implements authlog.Pruner.
In batches, and it keeps going until a batch comes back short. The subselect is what makes each statement bounded; ordering it lets the index do the work rather than the planner choosing whichever rows it happens to reach first.
func (*Log) Read ¶
Read implements authlog.Reader.
Two statements, the page and the count, and the count is the expensive half: it is `count(*)` over the tenant's slice, which is what a client asking for the total is asking for. The generated list endpoints pay the same price for the same reason, so this is not a new cost, only one worth knowing about on a table that grows with every login.
The default page rides `rig_auth_log_tenant_created_idx`. Narrowing by event scans the tenant's slice instead — the index that would fix it, `(tenant_id, event, created_at DESC)`, is a write cost on the hottest write path in the system for the benefit of a screen somebody opens twice a month, so it is not there. Add it when a real log argues for it.
func (*Log) Write ¶
Write implements authlog.Log.
A failure here is swallowed, deliberately. A login that succeeded and then returned 500 because the log was unreachable has turned an observability problem into an outage — and the rate limiter reading a slightly incomplete log is a smaller harm than the alternative.
type OAuthStore ¶
type OAuthStore struct {
// contains filtered or unexported fields
}
OAuthStore keeps provider links in rig_identity_oauth.
func (*OAuthStore) FindAccount ¶
func (s *OAuthStore) FindAccount(ctx context.Context, tenantID, identityID uuid.UUID) (uuid.UUID, error)
FindAccount implements oauth.Store.
func (*OAuthStore) FindIdentityByEmail ¶
FindIdentityByEmail implements oauth.Store.
func (*OAuthStore) FindLink ¶
FindLink implements oauth.Store.
func (*OAuthStore) JoinTenant ¶
JoinTenant implements oauth.Store.
This is the door, so the tenant's domain list is checked here. A provider will happily authenticate anybody with a Google account, so "sign in with Google" plus provisioning is an open door into a customer's tenant unless something says which addresses belong there.
func (*OAuthStore) LinkIdentity ¶
LinkIdentity implements oauth.Store.
The conflict target is (provider, subject), which the foundation makes unique. Two sign-ins racing on a first link both insert; the second updates the address instead of failing, and both end up with the same row.
It also stamps email_verified_at, because the link is only ever offered on an address the provider has verified — [oauth.Handler.identity] refuses otherwise — and that is the same evidence OAuthStore.ProvisionIdentity acts on. Recording the link and not the evidence is what left somebody who signed up with a password and later signed in with Google unverified, on a column rather than on anything they did.
It runs on every provider sign-in that carries a verified address rather than only the first, so the upsert is doing real work here: it lands on the row already present, moves updated_at, and refreshes the address the provider reports. The stamp is the part that has to be conditional, and the CTE below is where that condition lives.
One statement rather than a transaction of its own, because OAuthStore.ProvisionIdentity calls this from inside one. Postgres runs a data-modifying CTE exactly once whether or not anything selects from it, so the update happens even though only the insert is read back. Three details the shape depends on: the update reads identity_id back out of the insert rather than trusting the input, so a conflicting upsert stamps whoever actually owns the link; `email_verified_at IS NULL` means an address verified long ago keeps its original timestamp instead of having it moved by every sign-in; and the flag is a parameter rather than a branch in Go so that the unverified case is still one round trip.
`deleted_at IS NULL` because the input carries an identifier a caller chose. Every other read here filters it, and a soft-deleted person is not somebody to record anything new about.
func (*OAuthStore) ProvisionIdentity ¶
func (s *OAuthStore) ProvisionIdentity(ctx context.Context, in oauth.ProvisionInput) (*oauth.Link, error)
ProvisionIdentity implements oauth.Store.
The person and the link go in together. Half of this — an identity with no link — is somebody who can never sign in and never shows up in a support query, which is the worst kind of leftover.
No tenant is involved and no domain is checked here, because nothing has been granted yet: an identity on its own reaches nothing. OAuthStore.JoinTenant is where the door is.
type OutboxStore ¶
type OutboxStore struct {
// contains filtered or unexported fields
}
OutboxStore is an account.Outbox over the foundation's tables.
func (*OutboxStore) Abandon ¶
func (s *OutboxStore) Abandon(ctx context.Context, ids []uuid.UUID, by uuid.UUID, _ time.Time) error
Abandon implements account.Outbox.
The attempt is given back with the claim, and that is the part worth spelling out. Claim charges every row in the batch an attempt before anything is sent, so a row released without a send would have paid for one it never got — and MaxAttempts of those, on a provider slow enough to abandon the tail of every batch, would fail a delivery no notifier had ever been asked about.
claimed_by is in the WHERE clause for the same reason: past a lease that expired anyway, the row may be somebody else's now, and the attempt to give back would be theirs.
func (*OutboxStore) Claim ¶
func (s *OutboxStore) Claim(ctx context.Context, by uuid.UUID, now time.Time, ttl time.Duration, limit int) ([]account.Delivery, error)
Claim implements account.Outbox, in the one statement that makes this contention-free.
The inner SELECT does the work: SKIP LOCKED walks past whatever another claimant is taking rather than queueing behind it, and the outer UPDATE stamps the lease. `claimed_at IS NULL OR claimed_at < now() - ttl` is what makes a crashed process recoverable at all — the row is still Pending with a stale claim, and the next dispatcher past it takes it.
func (*OutboxStore) Enqueue ¶
Enqueue implements account.Outbox.
Through conn, so it joins the transaction that wrote the verification row. The two belong together: a link with no delivery is one nobody will ever mail, and it is invisible except as an invitation in a listing that was never sent.
func (*OutboxStore) MarkFailed ¶
func (s *OutboxStore) MarkFailed(ctx context.Context, id uuid.UUID, reason string, _ time.Time) error
MarkFailed implements account.Outbox.
func (*OutboxStore) MarkSent ¶
MarkSent implements account.Outbox.
func (*OutboxStore) MarkSkipped ¶
func (s *OutboxStore) MarkSkipped(ctx context.Context, id uuid.UUID, reason string, _ time.Time) error
MarkSkipped implements account.Outbox.
func (*OutboxStore) Prune ¶
func (s *OutboxStore) Prune(ctx context.Context, olderThan time.Duration, now time.Time) (int, error)
Prune deletes deliveries that are done and older than olderThan, in the same task that dispatches them.
Only the terminal states, and never a Pending row: a schema that grows forever is the state most of rig's tables are still in, and this one is written at the rate people forget their passwords. The verification rows are left alone — they are the audit trail of who was invited and when, and pruning those is a decision this queue does not get to make.
func (*OutboxStore) ReleaseClaims ¶
ReleaseClaims implements account.Outbox.
The attempts stay where they are, which is the one thing Abandon does that this deliberately does not: the send it is giving up on was made.
func (*OutboxStore) Retry ¶
func (s *OutboxStore) Retry(ctx context.Context, id uuid.UUID, at time.Time, reason string, _ time.Time) error
Retry implements account.Outbox.
func (*OutboxStore) RotateToken ¶
func (s *OutboxStore) RotateToken(ctx context.Context, verificationID uuid.UUID, hash []byte, expiresAt time.Time) (bool, error)
RotateToken implements account.Outbox.
One statement, because the check and the write cannot be two: between reading "not consumed" and writing a hash, another pass could redeem the link, and both would then believe they held the live token. The WHERE clause is the check.
type SessionStore ¶
type SessionStore struct {
// contains filtered or unexported fields
}
SessionStore keeps tokens in rig_account_token.
func (*SessionStore) Families ¶
func (s *SessionStore) Families(ctx context.Context, tenantID, accountID uuid.UUID) ([]session.Family, error)
Families implements session.Store.
The last-used time is derived from the newest token in each family rather than kept in a column, so nothing has to be written on the hot path to keep a "signed in devices" screen honest.
func (*SessionStore) Find ¶
Find implements session.Store.
func (*SessionStore) FindIdentitySession ¶
func (s *SessionStore) FindIdentitySession(ctx context.Context, id uuid.UUID) (*session.Identity, error)
FindIdentitySession implements session.IdentityStore.
func (*SessionStore) InTx ¶
InTx implements session.Store.
func (*SessionStore) Insert ¶
Insert implements session.Store.
func (*SessionStore) InsertIdentitySession ¶
InsertIdentitySession implements session.IdentityStore.
func (*SessionStore) Lock ¶
Lock implements session.Store.
FOR UPDATE is the whole point. Rotation reads a token, decides whether this is a first use, a retry, or a replay, and writes — and two requests doing that at once would both see an unconsumed token and both mint a child.
func (*SessionStore) MarkRotated ¶
MarkRotated implements session.Store.
The WHERE clause is what makes the leeway safe. Without `rotated_at IS NULL` a replay would push the timestamp forward, and somebody replaying every twenty seconds would hold a thirty-second window open indefinitely.
func (*SessionStore) RevokeFamily ¶
func (s *SessionStore) RevokeFamily(ctx context.Context, rootID uuid.UUID, at time.Time) ([]uuid.UUID, error)
RevokeFamily implements session.Store.
RETURNING rather than a row count, because what the caller does with it is tell every replica to forget those tokens — and one statement that both kills them and says which is one statement fewer than a count plus a second read, with no window in between for a token to be missed.
func (*SessionStore) RevokeIdentitySession ¶
RevokeIdentitySession implements session.IdentityStore.
`revoked_at IS NULL` keeps the first revocation's timestamp, which is the one that says when the session actually ended.
func (*SessionStore) RevokeIdentitySessionsFor ¶
func (s *SessionStore) RevokeIdentitySessionsFor(ctx context.Context, identityID uuid.UUID, at time.Time) (int, error)
RevokeIdentitySessionsFor implements session.IdentityStore.
func (*SessionStore) TenantFamilies ¶
func (s *SessionStore) TenantFamilies(ctx context.Context, tenantID uuid.UUID) ([]session.Family, error)
TenantFamilies implements session.Store.
type Stores ¶
type Stores struct {
Accounts *AccountStore
Sessions *SessionStore
APIKeys *APIKeyStore
Log *Log
// Outbox is the mail queue. Handing it to account.Config is what turns
// queueing on; leaving it out keeps the inline path.
Outbox *OutboxStore
}
Stores are the implementations, ready to hand to the auth packages.
func New ¶
New builds every store over one connection.
The connection is usually a pool. Where a transaction is needed the stores open one themselves, and a repository call made inside somebody else's transaction joins it — that is what dbx.Conn is for.
func (*Stores) OAuth ¶
func (s *Stores) OAuth() *OAuthStore
OAuth builds the store a sign-in needs.
It is separate from New because a project can skip the OAuth part of the foundation, and a store over a table that does not exist is a runtime error waiting for its first sign-in.