Auth
Session-based authentication, the prebuilt login form, and the User plugin pattern from production.
Nibiru ships a session-based authentication core (Nibiru\Auth) and a users module that gives you a working login form, an authorisation check, and the database schema in three commands.
The Auth core
Section titled “The Auth core”Auth::auth($login, $password) is the lowest-level call. It:
- Looks up the user by
user_login. - Decrypts the stored password using the salt from
[SECURITY] password_hash. - On match, replaces
$_SESSIONwith['auth' => ['session_id' => …, 'user_id' => …, 'login' => …]].
$auth = new \Nibiru\Auth();if ($auth->auth($_POST['login'], $_POST['password'])) { View::forwardTo('/dashboard');} else { View::assign(['error' => 'Invalid credentials.']);}The Users module
Section titled “The Users module”Generate it once with the CLI:
./nibiru -m usersThen run the matching migrations:
./nibiru -mi localYou now have:
application/module/users/— the module folder.userstable — created by005-user.sql.Userplugin —Nibiru\Module\Users\Plugin\UserwithisAuthorized(),loginForm(),currentUser().
A complete login flow
Section titled “A complete login flow”application/controller/loginController.php:
<?phpnamespace Nibiru;use Nibiru\Adapter\Controller;use Nibiru\Module\Users\Plugin\User;
class loginController extends Controller{ private User $user;
public function __construct() { parent::__construct(); $this->user = new User(); }
public function pageAction() { if ($this->user->isAuthorized()) { View::forwardTo('/'); return; } View::assign([ 'title' => 'Sign in', 'loginForm' => $this->user->loginForm(), ]); }
public function submitAction() { if ($_SERVER['REQUEST_METHOD'] !== 'POST') return; $auth = new Auth(); if ($auth->auth($_POST['login'] ?? '', $_POST['password'] ?? '')) { View::forwardTo('/'); } else { View::assign(['error' => 'Invalid login.']); } }
public function logoutAction() { unset($_SESSION['auth']); session_regenerate_id(true); View::forwardTo('/login'); }
public function navigationAction() { JsonNavigation::getInstance()->loadJsonNavigationArray(); }}application/view/templates/login.tpl:
{include 'shared/header.tpl'}<body>{include file="navigation.tpl"}<main class="container"> <h1>{$title}</h1> {if $error}<div class="alert alert-danger">{$error}</div>{/if} {$loginForm nofilter}</main>{include 'shared/footer.tpl'}</body>Form::action="/login/submit" is set inside loginForm() so the form posts to the right action.
Guarding controllers
Section titled “Guarding controllers”The simple check:
public function pageAction() { if (!$this->user->isAuthorized()) { View::forwardTo('/login'); return; } / ...}For role-aware checks, the showcase apps use the Acl plugin from the same module:
use Nibiru\Module\Users\Plugin\Acl;
if (!Acl::can('edit', 'documents')) { View::forwardTo('/forbidden'); return;}Tables acl, user_to_acl, acl-data (migrations 001, 008, 011) form the role/permission base.
Hardening
Section titled “Hardening”For production, replace Auth::auth() with a hardened version:
namespace Nibiru;use Nibiru\Pdo;
class HardenedAuth{ public function auth(string $login, string $password): bool { $row = Pdo::fetchRow( 'SELECT user_id, user_pass FROM "user" WHERE user_login = :l AND user_account_active = 1', [':l' => $login] ); if (!$row || !password_verify($password, $row['user_pass'])) { return false; } if (password_needs_rehash($row['user_pass'], PASSWORD_ARGON2ID)) { Pdo::update('user', [ 'user_pass' => password_hash($password, PASSWORD_ARGON2ID), ], ['user_id' => $row['user_id']]); } session_regenerate_id(true); $_SESSION['auth'] = [ 'session_id' => session_id(), 'user_id' => $row['user_id'], 'login' => $login, ]; return true; }}Migrate existing rows on first login with password_needs_rehash.
Nibiru doesn’t generate CSRF tokens for you. Add them yourself:
public function pageAction() { if (!isset($_SESSION['csrf'])) { $_SESSION['csrf'] = bin2hex(random_bytes(16)); } View::assign(['csrf' => $_SESSION['csrf']]);}
public function submitAction() { if (!hash_equals($_SESSION['csrf'] ?? '', $_POST['csrf'] ?? '')) { http_response_code(419); return; } / ...handle submission...}Embed <input type="hidden" name="csrf" value="{$csrf}"> in your form.
QR-code login (TPMS pattern)
Section titled “QR-code login (TPMS pattern)”Production apps include a QR-code-based magic-link login that issues short-lived tokens. The flow:
- User scans QR → URL is
/login/token/<one-time-token>. tokenActionvalidates and creates a session.
The framework already depends on bacon/bacon-qr-code and picqer/php-barcode-generator via Composer, so you can render QR codes inline:
$writer = new \BaconQrCode\Writer(new \BaconQrCode\Renderer\ImageRenderer( new \BaconQrCode\Renderer\RendererStyle\RendererStyle(220), new \BaconQrCode\Renderer\Image\SvgImageBackEnd()));$svg = $writer->writeString('https://app.example.com/login/token/' . $token);View::assign(['qr' => $svg]);Common pitfalls
Section titled “Common pitfalls”$_SESSIONis replaced, not merged by the defaultAuth::auth(). Anything you stored before login is lost. Save and restore explicitly if needed.- No rate limiting. Add Fail2Ban or a middleware-style observer.
- Session cookies need flags. Set
session.cookie_secure = 1,session.cookie_httponly = 1, andsession.cookie_samesite = "Lax"in php.ini for production.