Skip to content

Modulo Auth

Jose Navarro edited this page Aug 12, 2026 · 1 revision

Módulo auth

AuthController (/api/auth/**, permitAll() a nivel de filtro). No hay validación local de credenciales ni tabla de usuarios propia para login: todo se delega a Supabase Auth (GoTrue) vía RestClient con la anon key. AuthServiceImpl no invoca ningún repositorio JPA.

Endpoints

Método Path Rol Request Response
POST /api/auth/login público CuerpoLogin{username,password} RespuestaLogin{token,refreshToken,rol,nombreCompleto,username}
POST /api/auth/refresh público CuerpoRefresh{refreshToken} RespuestaLogin
POST /api/auth/recuperar público CuerpoRecuperar{email} 200 vacío (siempre, exista o no el email)
POST /api/auth/restablecer público CuerpoRestablecer{accessToken,password} 200 vacío

Flujo de negocio

flowchart TD
    LOGIN["POST /api/auth/login"] --> SUPA_PWD["Supabase: POST /auth/v1/token?grant_type=password"]
    SUPA_PWD -->|"error HTTP"| ERR1["CredencialesInvalidasException (401)"]
    SUPA_PWD -->|"ok, sin access_token (defensivo)"| ERR1
    SUPA_PWD -->|"ok"| MAP1["mapear(): extrae app_metadata.rol (default OPERARIO),\nuser_metadata.nombre_completo, username"]
    MAP1 --> OUT1["RespuestaLogin"]

    REFRESH["POST /api/auth/refresh"] --> SUPA_REF["Supabase: POST /auth/v1/token?grant_type=refresh_token"]
    SUPA_REF -->|"error"| ERR2["SesionExpiradaException (401)"]
    SUPA_REF -->|"ok"| MAP2["mapear() — rota el refresh token en Supabase\n(el usado queda invalidado)"]
    MAP2 --> OUT1
    NOTE1["Sin deduplicación de refresh in-flight en el backend\n(confirmado: no hay lock/cache/mutex en AuthServiceImpl).\nSi el frontend dispara refresh en paralelo,\ncada request es independiente."]

    RECUP["POST /api/auth/recuperar"] --> SUPA_RECUP["Supabase: POST /auth/v1/recover\nredirect_to=<primer origen CORS>/restablecer"]
    SUPA_RECUP -->|"429"| ERR3["ReglaNegocioException\n'Ya se envio un correo hace poco'"]
    SUPA_RECUP -->|"cualquier otro error"| SWALLOW["log WARN, responde 200 igual\n(por diseño: no revela si el email existe)"]
    SUPA_RECUP -->|"ok"| OK3["200 vacío"]

    RESTAB["POST /api/auth/restablecer\nBearer accessToken"] --> SUPA_RESTAB["Supabase: PUT /auth/v1/user {password}"]
    SUPA_RESTAB -->|"401/403"| ERR4["SesionExpiradaException"]
    SUPA_RESTAB -->|"422/400"| ERR5["ReglaNegocioException\n'contraseña no cumple requisitos'"]
    SUPA_RESTAB -->|"otro error"| ERR6["log ERROR + ReglaNegocioException genérica"]
    SUPA_RESTAB -->|"ok"| OK4["200 vacío"]

    style NOTE1 fill:#fde9c8
    style SWALLOW fill:#fde9c8
Loading

Secuencia HTTP: login → uso del token en requests posteriores

sequenceDiagram
    participant C as Cliente (SPA)
    participant RL as FiltroRateLimit
    participant AC as AuthController
    participant AS as AuthServiceImpl
    participant SUPA as Supabase Auth (GoTrue)

    C->>RL: POST /api/auth/login
    Note over RL: límite estricto: 10 intentos/min por IP en /api/auth/**
    RL-->>C: 429 si excede
    RL->>AC: request (permitAll, no pasa por JWT filter)
    AC->>AS: login(cuerpo)
    AS->>SUPA: POST /auth/v1/token?grant_type=password
    alt credenciales inválidas
        SUPA-->>AS: error HTTP
        AS-->>AC: throw CredencialesInvalidasException
        AC-->>C: 401
    else ok
        SUPA-->>AS: access_token (JWT ES256) + refresh_token
        Note over SUPA: JWT contiene app_metadata.rol,<br/>app_metadata.empresa_schema,<br/>user_metadata.nombre_completo/username
        AS-->>AC: RespuestaLogin
        AC-->>C: 200 {token, refreshToken, rol, nombreCompleto, username}
    end

    Note over C: guarda token en localStorage (sicofar_token)

    C->>+RL: request autenticado (ej. GET /api/inventario)\nAuthorization: Bearer &lt;token&gt;
    RL->>RL: valida rate limit global
    RL->>+JWTV: (siguiente filtro en la cadena de Security)
    Note right of JWTV: oauth2ResourceServer valida firma<br/>contra JWKS de Supabase — 401 si expiró
    JWTV-->>-C: 401 si expiró
    JWTV->>C: (si válido, continúa a Authorization + Tenant + Controller)
    deactivate RL

    C->>AC: POST /api/auth/refresh {refreshToken}
    AC->>AS: refrescar(cuerpo)
    AS->>SUPA: POST /auth/v1/token?grant_type=refresh_token
    alt refresh inválido/expirado
        SUPA-->>AS: error
        AS-->>C: 401 SesionExpiradaException → frontend limpia sicofar_* y redirige a "/"
    else ok
        SUPA-->>AS: nuevo access_token + refresh_token (el anterior queda rotado/invalidado)
        AS-->>C: 200 RespuestaLogin
    end
Loading

Notas

  • No existe filtro JWT custom: la validación de firma/expiración la hace oauth2ResourceServer.jwt() de Spring Security contra las JWKS públicas de Supabase (application.yaml, algoritmo ES256).
  • El rol viaja siempre en el JWT (app_metadata.rol); el backend nunca lo persiste ni lo deriva de otra fuente en el flujo de login.
  • recuperar y restablecer nunca revelan si un email existe en el sistema — comportamiento intencional contra enumeración de usuarios.

Clone this wiki locally