package store import ( "context" "database/sql" "errors" "fmt" "time" ) // PutOIDCState stores the (state_hash, code_verifier) pair created // at /auth/oidc/login start. Called once per login attempt. func (s *Store) PutOIDCState(ctx context.Context, stateHash, verifier string, createdAt time.Time) error { _, err := s.db.ExecContext(ctx, `INSERT INTO oidc_state (state_hash, code_verifier, created_at) VALUES (?, ?, ?)`, stateHash, verifier, createdAt.UTC().Format(time.RFC3339Nano)) if err != nil { return fmt.Errorf("store: put oidc state: %w", err) } return nil } // ConsumeOIDCState atomically reads + deletes the row in one go, // returning the code_verifier. Single-use — a re-play returns // ErrNotFound. Used by the OIDC callback handler. func (s *Store) ConsumeOIDCState(ctx context.Context, stateHash string) (string, error) { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return "", fmt.Errorf("store: begin: %w", err) } defer func() { _ = tx.Rollback() }() var verifier string err = tx.QueryRowContext(ctx, `SELECT code_verifier FROM oidc_state WHERE state_hash = ?`, stateHash).Scan(&verifier) if err != nil { if errors.Is(err, sql.ErrNoRows) { return "", ErrNotFound } return "", fmt.Errorf("store: consume oidc state: %w", err) } if _, err := tx.ExecContext(ctx, `DELETE FROM oidc_state WHERE state_hash = ?`, stateHash); err != nil { return "", fmt.Errorf("store: delete oidc state: %w", err) } if err := tx.Commit(); err != nil { return "", fmt.Errorf("store: commit: %w", err) } return verifier, nil } // CleanupExpiredOIDCState removes entries created before cutoff. // Called on the alert engine's 60s tick alongside setup-token sweep. func (s *Store) CleanupExpiredOIDCState(ctx context.Context, cutoff time.Time) (int64, error) { res, err := s.db.ExecContext(ctx, `DELETE FROM oidc_state WHERE created_at < ?`, cutoff.UTC().Format(time.RFC3339Nano)) if err != nil { return 0, fmt.Errorf("store: cleanup oidc state: %w", err) } n, _ := res.RowsAffected() return n, nil }