Kyrux
Back

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