package wristband import ( "context" "time" ) // OIDCClientRecord is the persisted shape of an administrator-registered // OpenID Connect client. The OIDC flavour reads clients only through // OIDCClientStore and refuses any record whose Kind is not ClientKindOIDC // (D-08, T-19.4-05). type OIDCClientRecord struct { ID uint ClientID string Name string Kind string // SecretHash is the SHA-256 hex digest of the client secret (secrets // carry OIDCOptions.ClientSecretPrefix and are never stored in clear). SecretHash *string RedirectURIs []string // Scopes is the set of scopes the client may request. Scopes []string IsActive bool // IsDev lets an http loopback redirect URI match a registered one that // differs only in the port (T-19.4-01). IsDev bool // OfflineAccess allows the client to receive refresh tokens when the // server has OIDCOptions.RefreshEnabled (D-17). OfflineAccess bool // SubjectType is SubjectTypePublic (or empty) or SubjectTypePairwise. SubjectType string // SectorIdentifier is the pairwise sector an administrator set. Empty // means the single host of RedirectURIs (D-15). SectorIdentifier string } // AuthRequestRecord is a parked authorization request. It deliberately has // no user column: the user is resolved again on every resume, and the // consent token digest binds a pending decision to one user (T-19.4-04). type AuthRequestRecord struct { ID uint RequestID string ClientID string RedirectURI string Scopes []string State *string Nonce *string CodeChallenge string CodeChallengeMethod string // ConsentTokenHash is SHA-256 hex of token + "|" + user ID while a // consent page is outstanding, nil otherwise. ConsentTokenHash *string ExpiresAt time.Time ConsumedAt *time.Time } // OIDCCodeRecord is an issued authorization code, stored by the SHA-256 hex // digest of the code. type OIDCCodeRecord struct { ID uint CodeHash string ClientID string UserID uint RedirectURI string Scopes []string CodeChallenge string CodeChallengeMethod string Nonce *string ExpiresAt time.Time ConsumedAt *time.Time } // OIDCAccessTokenRecord is an opaque access token, stored by the SHA-256 // hex digest of the token. AuthCodeID links tokens issued from a code so a // replayed code can revoke them (T-19.4-03). type OIDCAccessTokenRecord struct { ID uint TokenHash string ClientID string UserID uint AuthCodeID *uint Scopes []string ExpiresAt time.Time RevokedAt *time.Time LastUsedAt *time.Time } // OIDCRefreshTokenRecord is a refresh token (D-17), stored by the SHA-256 // hex digest of the token. A rotated token points at its successor through // RotatedToID and stays as replay evidence (T-19.4-12). type OIDCRefreshTokenRecord struct { ID uint TokenHash string ClientID string UserID uint AccessTokenID *uint Scopes []string ExpiresAt time.Time RevokedAt *time.Time RotatedToID *uint } // ConsentRecord is the one row per (user, client) pair holding every scope // the user consented to. type ConsentRecord struct { ID uint UserID uint ClientID string Scopes []string CreatedAt time.Time LastUsedAt *time.Time } // OIDCClientStore reads OIDC clients. Not found is (nil, nil). type OIDCClientStore interface { OIDCClientByClientID(ctx context.Context, clientID string) (*OIDCClientRecord, error) } // AuthRequestStore persists parked authorization requests. Not found is // (nil, nil). type AuthRequestStore interface { // CreateAuthRequest stores rec and fills rec.ID. CreateAuthRequest(ctx context.Context, rec *AuthRequestRecord) error AuthRequestByRequestID(ctx context.Context, requestID string) (*AuthRequestRecord, error) // AuthRequestByRequestIDForUpdate reads the row under a row lock, so a // consent token is verified and cleared by one caller only (T-19.4-04). AuthRequestByRequestIDForUpdate(ctx context.Context, requestID string) (*AuthRequestRecord, error) // ConsumeAuthRequest marks the request consumed atomically (UPDATE ... // WHERE id = ? AND consumed_at IS NULL AND expires_at > now) and // reports whether exactly one row moved. The loser of a race gets // false. ConsumeAuthRequest(ctx context.Context, id uint, now time.Time) (bool, error) // SetConsentTokenHash replaces (or, with nil, clears) the consent token // digest of the request. SetConsentTokenHash(ctx context.Context, id uint, hash *string) error } // OIDCCodeStore persists authorization codes. Not found is (nil, nil). type OIDCCodeStore interface { // CreateCode stores rec and fills rec.ID. CreateCode(ctx context.Context, rec *OIDCCodeRecord) error // ConsumeCode marks the code consumed atomically (UPDATE ... WHERE // code_hash = ? AND consumed_at IS NULL AND expires_at > now) and // returns the row when exactly one row moved, nil otherwise. The token // endpoint calls it before any other check (burn then check, // T-19.4-03). ConsumeCode(ctx context.Context, codeHash string, now time.Time) (*OIDCCodeRecord, error) // CodeByHash reads a code regardless of its state, so a replay of a // consumed code can be told apart from an unknown one. CodeByHash(ctx context.Context, codeHash string) (*OIDCCodeRecord, error) } // OIDCAccessTokenStore persists opaque access tokens. Not found is // (nil, nil). type OIDCAccessTokenStore interface { // CreateAccessToken stores rec and fills rec.ID. CreateAccessToken(ctx context.Context, rec *OIDCAccessTokenRecord) error AccessTokenByHash(ctx context.Context, tokenHash string) (*OIDCAccessTokenRecord, error) // TouchAccessToken stamps LastUsedAt. TouchAccessToken(ctx context.Context, id uint, now time.Time) error // RevokeAccessTokensForCode revokes every access token issued from the // code, and every refresh token linked to one of them (T-19.4-03). RevokeAccessTokensForCode(ctx context.Context, codeID uint, now time.Time) error } // ConsentStore persists consents, one row per (user, client) pair. Not // found is (nil, nil). type ConsentStore interface { ConsentFor(ctx context.Context, userID uint, clientID string) (*ConsentRecord, error) // SaveConsent merges scopes into the pair's row (creating it when // absent) and stamps LastUsedAt with now. SaveConsent(ctx context.Context, userID uint, clientID string, scopes []string, now time.Time) error // TouchConsent stamps LastUsedAt. TouchConsent(ctx context.Context, id uint, now time.Time) error } // SubjectStore allocates subjects (D-15, T-19.4-13). type SubjectStore interface { // SubjectFor returns the stored subject of (userID, sector), inserting // candidate (a random UUIDv4 the engine generated) only when none // exists. The public sector is "". SubjectFor(ctx context.Context, userID uint, sector, candidate string) (string, error) } // IdentityStore hands the user's identity claims to the engine. type IdentityStore interface { // IdentityClaims returns the claims the scopes release (name, picture, // email, ...). ok is false when the user no longer exists. The engine // filters the map through OIDCOptions.IdentityClaims and sets sub // itself. IdentityClaims(ctx context.Context, userID uint, scopes []string) (claims map[string]any, ok bool, err error) } // OIDCRefreshTokenStore persists refresh tokens (D-17). Not found is // (nil, nil). type OIDCRefreshTokenStore interface { // CreateRefreshToken stores rec and fills rec.ID. CreateRefreshToken(ctx context.Context, rec *OIDCRefreshTokenRecord) error // RefreshTokenByHashForUpdate reads the row under a row lock, so one // refresh token rotates once (T-19.4-12). RefreshTokenByHashForUpdate(ctx context.Context, tokenHash string) (*OIDCRefreshTokenRecord, error) // MarkRefreshRotated links a rotated token to its successor. MarkRefreshRotated(ctx context.Context, id, successorID uint) error // RevokeRefreshLineage revokes startID and every token it was rotated // to (following RotatedToID), and the access token linked to each. RevokeRefreshLineage(ctx context.Context, startID uint, now time.Time) error } // OIDCTx bundles every OIDC store on one transaction-scoped handle. type OIDCTx interface { OIDCClientStore AuthRequestStore OIDCCodeStore OIDCAccessTokenStore ConsentStore SubjectStore IdentityStore OIDCRefreshTokenStore } // OIDCBackend opens one transaction per call. When fn returns an error the // transaction rolls back; the engine returns nil from fn whenever a write // must commit although the protocol answer is an error (a replayed code // revokes its tokens, T-19.4-03). type OIDCBackend interface { WithinOIDCTx(ctx context.Context, fn func(OIDCTx) error) error }