Authentication
Kyrux offers two authentication models: session-based SSR (HTML views) and stateless JWT (APIs). Both coexist.
Default user model
auth.User is the system's user model, available at 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"` // default login field
Email *string `kyrux:"column:email,size:254,unique"` // optional when it's not the login field
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"` // new accounts are active by default
CreatedAt time.Time `kyrux:"column:created_at"`
UpdatedAt time.Time `kyrux:"column:updated_at"`
}
The login tag marks which field is used for authentication. Only one field can carry this tag. Changing the field after the first migrate requires a new migration — treat it as a schema decision.
Email *string is a pointer because it's optional when Username is the login field. The database allows multiple NULLs without violating the UNIQUE index. When Email carries the login tag, Username becomes the optional field.
auth.LoginFieldName() returns the name of the Go field marked with login ("Username" or "Email"). Useful for adapting forms and authentication logic dynamically:
field := auth.LoginFieldName() // "Username" — determined by the model's tags
Password hashing uses Argon2id (64 MB, 3 iterations, 4 threads) with a pepper defined in PASSWORD_PEPPER.
// Email is *string — use a pointer or nil
email := "[email protected]"
user := &auth.User{Username: "joao", Email: &email}
user.SetPassword("my-strong-password") // Argon2id + pepper hash
user.CheckPassword("my-strong-password") // → true
user.FullName() // → "João Silva"
// Without an email (optional field when login = username)
user := &auth.User{Username: "joao", Email: nil}
SSR authentication (session + cookie)
Suited for server-rendered HTML views. The login field is determined by the kyrux:"login" tag on the auth.User model — username by default. Changing this field after the first migrate is equivalent to a schema change and requires a new 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 the login field name defined on the model (kyrux:"login" tag)
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": "Invalid user or password.",
})
case auth.ErrInactiveUser:
render.For("auth").Render(ctx, "login.html", map[string]any{
"erro": "Inactive account.",
})
default:
ctx.Error(500)
}
}
Logout
func LogoutView(ctx *router.Context) {
auth.Logout(fw.Sessions, ctx.Request, ctx.Writer)
ctx.Redirect("/login/", http.StatusFound)
}
Getting the logged-in user
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,
})
}
Protecting routes with RequireLogin
import secmiddleware "kyrux/core/security/middleware"
// Individual route
loginRequired := secmiddleware.RequireLogin(fw.Sessions, "/login/")
r.Handle("GET /dashboard/", loginRequired(DashboardView))
// All routes in the app
r.Use(secmiddleware.RequireLogin(fw.Sessions, "/login/"))
The session is available at ctx.Get("session") inside the protected view:
sess := ctx.Get("session").(*session.Session)
?next= redirect after login
RequireLogin automatically appends ?next=<current URL> to the redirect to the login page. After authenticating, use auth.NextURL to send the user back:
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 reads ?next= and validates that it's a relative URL (open redirect protection)
dest := auth.NextURL(ctx.Request, "/dashboard/")
ctx.Redirect(dest, http.StatusFound)
return
}
// ... handle the error ...
}
render.For("auth").Render(ctx, "login.html", nil)
}
auth.NextURL(r, fallback) only accepts relative URLs starting with / (not //) — any open-redirect attempt is ignored and the fallback is returned.
JWT authentication (stateless APIs)
Suited for APIs consumed by external clients (mobile, SPA, etc.).
Generating a token
func LoginAPIView(ctx *router.Context) {
// ... validate the user ...
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})
}
Validating a token manually
claims, err := fw.Auth.ValidateToken(token)
// claims.UserID string
// claims.ExpiresAt time.Time
RequireAuth middleware
The token must come in the Authorization: Bearer <token> header. The claims are available at 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})
}))