Kyrux
Voltar

Autenticação

O Kyrux oferece dois modelos de autenticação: SSR por sessão (views HTML) e JWT stateless (APIs). Ambos coexistem.

Model de usuário padrão

auth.User é o model de usuário do sistema, disponível em kyrux/core/security/auth.

type User struct {
    ID        int64     `kyrux:"column:id,pk"`
    UUID      string    `kyrux:"column:uuid,size:36"`
    FirstName string    `kyrux:"column:first_name,size:150"`
    LastName  string    `kyrux:"column:last_name,size:150"`
    Username  string    `kyrux:"column:username,size:150,unique,login"` // campo de login padrão
    Email     *string   `kyrux:"column:email,size:254,unique"`           // opcional quando não é login
    Password  string    `kyrux:"column:password,size:128"`
    Group     string    `kyrux:"column:user_group,size:100"`
    IsAdmin   bool      `kyrux:"column:is_admin"`
    IsStaff   bool      `kyrux:"column:is_staff"`
    IsActive  bool      `kyrux:"column:is_active,default:true"` // nova conta ativa por padrão
    CreatedAt time.Time `kyrux:"column:created_at"`
    UpdatedAt time.Time `kyrux:"column:updated_at"`
}

A tag login marca qual campo é usado para autenticação. Apenas um campo pode ter essa tag. Trocar o campo após o primeiro migrate exige uma nova migration — trate como decisão de schema.

Email *string é ponteiro porque é opcional quando Username é o campo de login. O banco permite múltiplos NULL sem violar o índice UNIQUE. Quando Email tiver a tag login, Username passa a ser o campo opcional.

auth.LoginFieldName() retorna o nome do campo Go marcado com login ("Username" ou "Email"). Útil para adaptar formulários e lógica de autenticação dinamicamente:

field := auth.LoginFieldName() // "Username" — determinado pelas tags do model

O hash de senha usa Argon2id (64 MB, 3 iterações, 4 threads) com pepper definido em PASSWORD_PEPPER.

// E-mail é *string — use ponteiro ou nil
email := "[email protected]"
user := &auth.User{Username: "joao", Email: &email}
user.SetPassword("minha-senha-forte")   // hash Argon2id + pepper
user.CheckPassword("minha-senha-forte") // → true
user.FullName()                         // → "João Silva"

// Sem e-mail (campo opcional quando login = username)
user := &auth.User{Username: "joao", Email: nil}

Autenticação SSR (sessão + cookie)

Indicada para views HTML renderizadas no servidor. O campo de login é determinado pela tag kyrux:"login" no model auth.User — por padrão username. Alterar esse campo após o primeiro migrate equivale a uma mudança de schema e exige nova migration.

Login

import "kyrux/core/security/auth"

func LoginView(ctx *router.Context) {
    if ctx.Request.Method == http.MethodGet {
        render.For("auth").Render(ctx, "login.html", nil)
        return
    }

    // Use o nome do campo de login definido no model (tag kyrux:"login")
    loginValue := ctx.Request.FormValue(strings.ToLower(auth.LoginFieldName()))
    password   := ctx.Request.FormValue("password")

    _, err := auth.Login(fw.DB.Use(), fw.Sessions, ctx.Writer, ctx.Request, loginValue, password)
    switch err {
    case nil:
        ctx.Redirect("/dashboard/", http.StatusFound)
    case auth.ErrUserNotFound, auth.ErrWrongPassword:
        render.For("auth").Render(ctx, "login.html", map[string]any{
            "erro": "Usuário ou senha inválidos.",
        })
    case auth.ErrInactiveUser:
        render.For("auth").Render(ctx, "login.html", map[string]any{
            "erro": "Conta inativa.",
        })
    default:
        ctx.Error(500)
    }
}

Logout

func LogoutView(ctx *router.Context) {
    auth.Logout(fw.Sessions, ctx.Request, ctx.Writer)
    ctx.Redirect("/login/", http.StatusFound)
}

Obter usuário logado

func DashboardView(ctx *router.Context) {
    user, err := auth.GetUser(fw.DB.Use(), fw.Sessions, ctx.Request)
    if err != nil {
        ctx.Redirect("/login/", http.StatusFound)
        return
    }
    render.For("painel").Render(ctx, "dashboard.html", map[string]any{
        "user": user,
    })
}

Proteger rotas com RequireLogin

import secmiddleware "kyrux/core/security/middleware"

// Rota individual
loginRequired := secmiddleware.RequireLogin(fw.Sessions, "/login/")
r.Handle("GET /dashboard/", loginRequired(DashboardView))

// Todas as rotas do app
r.Use(secmiddleware.RequireLogin(fw.Sessions, "/login/"))

A sessão fica disponível em ctx.Get("session") dentro da view protegida:

sess := ctx.Get("session").(*session.Session)

Redirecionamento ?next= pós-login

RequireLogin adiciona automaticamente ?next=<URL atual> ao redirect para o login. Após autenticar, use auth.NextURL para redirecionar o usuário de volta:

func LoginView(ctx *router.Context) {
    if ctx.Request.Method == http.MethodPost {
        _, err := auth.Login(fw.DB.Use(), fw.Sessions, ctx.Writer, ctx.Request,
            ctx.Request.FormValue("username"),
            ctx.Request.FormValue("password"),
        )
        if err == nil {
            // auth.NextURL lê ?next= e valida que é URL relativa (proteção open redirect)
            dest := auth.NextURL(ctx.Request, "/dashboard/")
            ctx.Redirect(dest, http.StatusFound)
            return
        }
        // ... tratar erro ...
    }
    render.For("auth").Render(ctx, "login.html", nil)
}

auth.NextURL(r, fallback) aceita apenas URLs relativas começando com / (não //) — qualquer tentativa de open redirect é ignorada e o fallback é retornado.


Autenticação JWT (APIs stateless)

Indicada para APIs consumidas por clientes externos (mobile, SPA, etc.).

Gerar token

func LoginAPIView(ctx *router.Context) {
    // ... validar usuário ...
    token, err := fw.Auth.GenerateToken(strconv.FormatInt(user.ID, 10), 24*time.Hour)
    if err != nil {
        ctx.Error(500)
        return
    }
    ctx.JSON(200, map[string]string{"token": token})
}

Validar token manualmente

claims, err := fw.Auth.ValidateToken(token)
// claims.UserID    string
// claims.ExpiresAt time.Time

Middleware RequireAuth

O token deve vir no header Authorization: Bearer <token>. As claims ficam em ctx.Get("claims").

r.Handle("GET /api/perfil/", secmiddleware.RequireAuth(fw.Auth)(func(ctx *router.Context) {
    claims := ctx.Get("claims").(*auth.Claims)
    ctx.JSON(200, map[string]string{"user_id": claims.UserID})
}))