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})
}))