Saltar a contenido

Especificación Técnica: trx_refresh_tokens

Esquema: core.identidad
Base de Datos: core
Servicio: svc-identidad
Propósito: Tokenización de sesiones: almacén de refresh tokens con rotación y revocación.


1. Justificación y Mejoras de Arquitectura

  • token_hash (SHA-256) evita almacenar tokens en claro.
  • Ausencia de FK textual por desempeño; unicidad del hash y revocación lógica.
  • RLS por empresa actual aislada la rotación entre empresas.
  • Solo created_at como auditoría transaccional ligera.

2. Definiciones de Implementación

Table core.identidad.trx_refresh_tokens {
    id                         uuid          [pk, default: `gen_random_uuid()`]
    empresa_id                 bigint        [not null]
    token_hash                 varchar(64)   [not null]
    usuario_id                 uuid          [not null, ref: > mae_usuarios.id]
    expiracion                 timestamptz   [not null]
    revocado                   boolean       [default: `false`]
    revocado_en                timestamptz  
    user_agent                 varchar(300) 
    ip                         varchar(45)  

    // Auditoría General
    created_at              timestamptz   [not null, default: `now()`, note: 'Fecha de creación']

    Indexes {
        (token_hash) [unique, name: 'uq_trx_refresh_tokens_token_hash']
    }

    // RLS por empresa actual
}
CREATE TABLE IF NOT EXISTS core.identidad.trx_refresh_tokens (
    id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY,
    empresa_id BIGINT NOT NULL,
    token_hash VARCHAR(64) NOT NULL,
    usuario_id UUID NOT NULL,
    expiracion TIMESTAMPTZ NOT NULL,
    revocado BOOLEAN DEFAULT false,
    revocado_en TIMESTAMPTZ,
    user_agent VARCHAR(300),
    ip VARCHAR(45),
    created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
    CONSTRAINT uq_trx_refresh_tokens_token_hash UNIQUE (token_hash),
    CONSTRAINT fk_trx_refresh_tokens_usuario FOREIGN KEY (usuario_id) REFERENCES core.identidad.mae_usuarios (id)
);

-- Row-Level Security
ALTER TABLE core.identidad.trx_refresh_tokens ENABLE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_empresa ON core.identidad.trx_refresh_tokens
    FOR ALL
    USING (empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);

-- Comentarios
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.empresa_id IS 'Empresa de la sesión';
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.token_hash IS 'Hash SHA-256 del token';
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.usuario_id IS 'Usuario propietario del token';
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.expiracion IS 'Fecha de expiración';
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.revocado IS 'Token revocado';
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.revocado_en IS 'Fecha de revocación';
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.user_agent IS 'User-Agent del cliente';
COMMENT ON COLUMN core.identidad.trx_refresh_tokens.ip IS 'IP origen (IPv4/IPv6)';
from datetime import datetime, date
from typing import Optional
import uuid

from sqlalchemy import (
    BigInteger,
    Boolean,
    CheckConstraint,
    Date,
    DateTime,
    ForeignKey,
    Identity,
    Index,
    Integer,
    SmallInteger,
    String,
    Text,
    UniqueConstraint,
    text,
)
from sqlalchemy.dialects import postgresql
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column


class Base(DeclarativeBase):
    pass

class TrxRefreshToken(Base):
    __tablename__ = 'trx_refresh_tokens'
    __table_args__ = (
        UniqueConstraint('token_hash', name='uq_trx_refresh_tokens_token_hash'),
        {"schema": 'core.identidad'},
    )

    id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        primary_key=True,
        server_default=text("gen_random_uuid()"),
    )
    empresa_id: Mapped[int] = mapped_column(
        BigInteger,
        nullable=False,
        comment='Empresa de la sesión',
    )
    token_hash: Mapped[str] = mapped_column(
        String(64),
        nullable=False,
        comment='Hash SHA-256 del token',
    )
    usuario_id: Mapped[uuid.UUID] = mapped_column(
        UUID(as_uuid=True),
        nullable=False,
        ForeignKey("mae_usuarios.id"),
        comment='Usuario propietario del token',
    )
    expiracion: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        nullable=False,
        comment='Fecha de expiración',
    )
    revocado: Mapped[bool | None] = mapped_column(
        Boolean,
        server_default=text("false"),
        comment='Token revocado',
    )
    revocado_en: Mapped[datetime | None] = mapped_column(
        DateTime(timezone=True),
        comment='Fecha de revocación',
    )
    user_agent: Mapped[str | None] = mapped_column(
        String(300),
        comment='User-Agent del cliente',
    )
    ip: Mapped[str | None] = mapped_column(
        String(45),
        comment='IP origen (IPv4/IPv6)',
    )

    # Auditoría General
    created_at: Mapped[datetime] = mapped_column(
        DateTime(timezone=True),
        nullable=False,
        server_default=text("now()"),
        comment='Fecha de creación',
    )
"""revision: core_id_0011
create table core.identidad.trx_refresh_tokens
"""

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql


revision: str = 'core_id_0011'
down_revision: str | None = 'core_id_0010'
branch_labels: str | None = None
depends_on: str | None = None


def upgrade() -> None:
    op.create_table(
        'trx_refresh_tokens',
        sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
        sa.Column("empresa_id", sa.BigInteger(), nullable=False),
        sa.Column("token_hash", sa.String(length=64), nullable=False),
        sa.Column("usuario_id", postgresql.UUID(as_uuid=True), nullable=False),
        sa.Column("expiracion", sa.DateTime(timezone=True), nullable=False),
        sa.Column("revocado", sa.Boolean(), server_default=sa.text("false")),
        sa.Column("revocado_en", sa.DateTime(timezone=True)),
        sa.Column("user_agent", sa.String(length=300)),
        sa.Column("ip", sa.String(length=45)),
        sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")),
        sa.UniqueConstraint('token_hash', name='uq_trx_refresh_tokens_token_hash'),
        sa.ForeignKeyConstraint(['usuario_id'], ['core.identidad.mae_usuarios.id'], name='fk_trx_refresh_tokens_usuario'),
        schema='core.identidad',
    )

    op.execute(
        """
        ALTER TABLE core.identidad.trx_refresh_tokens ENABLE ROW LEVEL SECURITY;
        CREATE POLICY aislamiento_empresa
            ON core.identidad.trx_refresh_tokens
            FOR ALL
            USING (empresa_id = NULLIF(current_setting('app.current_empresa_id', true), '')::BIGINT);
        """
    )


def downgrade() -> None:
    op.drop_table('trx_refresh_tokens', schema='core.identidad')