SmartAuth

SmartAuth is the JWT authentication module for Dolibarr, designed for SmartMaker applications.

Why SmartAuth?

Natively, Dolibarr offers an API where each user has a single API key granting access to their whole functional scope.

This is a problem: if you develop a mobile application that should only have access to the user's agenda, the native key also lets the application reach the invoices and everything else.

Our approach

With SmartAuth, a user can have as many API keys as they want, each key with its own permissions. If a key is tied to an application, it cannot be reused by another one.

Benefits:

  • Access partitioning: each application has its own key
  • Targeted revocation: if a device is stolen, delete only its key
  • Traceability: connection logs per device

List of SmartAuth access tokens: application, creation and last connection dates, state

Connection logs

Open the logs to spot incorrect actions and manage the accesses:

Connection log: third party, IP address, method, status code and called URL

Installation

Download SmartAuth for free on the DoliStore: https://www.dolistore.com/product.php?id=2509&l=fr

JWT authentication flow

Overall diagram

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│   Mobile    │         │   api.php   │         │  Dolibarr   │
│   (React)   │         │   (JWT)     │         │   (PHP)     │
└──────┬──────┘         └──────┬──────┘         └──────┬──────┘
       │                       │                       │
       │  1. POST /login       │                       │
       │  {login, password}    │                       │
       │──────────────────────>│                       │
       │                       │  2. Check user        │
       │                       │──────────────────────>│
       │                       │                       │
       │                       │  3. User valid        │
       │                       │<──────────────────────│
       │  4. {accessToken,     │                       │
       │      refreshToken}    │                       │
       │<──────────────────────│                       │
       │                       │                       │
       │  5. GET /items        │                       │
       │  Authorization: Bearer│                       │
       │──────────────────────>│                       │
       │                       │  6. Validate token    │
       │                       │  + load user          │
       │                       │──────────────────────>│
       │                       │                       │
       │  7. Data              │                       │
       │<──────────────────────│                       │

JWT tokens

SmartAuth uses two tokens:

Token Lifetime Usage
accessToken 15 minutes Authenticate the API requests
refreshToken 7 days Obtain a new accessToken

Authentication endpoints

Method Route Description Protected
GET /login Fetch the connection info (logo, etc.) No
POST /login Authentication (login + password) No
GET /refresh Renew the accessToken No
POST /logout Sign out (invalidates the refreshToken) Yes
POST /device Register a device for the notifications Yes
POST /qr-pair/{pairingId}/claim Mobile claims a pairing displayed by the PC No
POST /qr-pair/{pairingId}/poll Mobile polls until the PC confirms No

Configuration on the api.php side

Authentication routes

<?php
require_once '../smartmaker-api-prepend.php';

use SmartAuth\Api\AuthController;
use SmartAuth\Api\RouteController as Route;

// Public routes
Route::get('login',     AuthController::class, 'index');      // Connection info
Route::post('login',    AuthController::class, 'login');      // Authentication
Route::get('refresh',   AuthController::class, 'refresh');    // Refresh token

// Protected routes
Route::post('logout',   AuthController::class, 'logout', true);  // Sign out
Route::post('device',   AuthController::class, 'device', true);  // Register device

// Your business routes...
Route::get('items', ItemController::class, 'index', true);

// Fallback
json_reply('Access denied', 403);

The smartmaker-api-prepend.php file

This file (generated by SmartBoot) contains:

  • The mandatory Dolibarr headers
  • The loading of the SmartAuth autoloader
  • The initialization of the JWT layer
  • The autoloader for your module classes

Important

Do not modify this file unless you know what you are doing.

Usage on the React side

Provider configuration

// appConfig.js
export const config = {
  api: {
    prefixUrl: import.meta.env.VITE_API_URL,
    paths: {
      login: "login",
      logout: "logout",
      refresh: "refresh",
    },
  },
};

Login with useApi (manual)

import { useApi, useGlobalStates } from '@cap-rel/smartcommon';

const LoginPage = () => {
  const api = useApi();
  const gst = useGlobalStates();

  const handleLogin = async (data) => {
    try {
      const user = await api.login(data);
      gst.local.set('session', user);
    } catch (error) {
      console.error('Login failed:', error);
    }
  };

  return (
    <form onSubmit={handleSubmit(handleLogin)}>
      {/* ... */}
    </form>
  );
};

Simplified login with LoginComponent

Rather than reinventing the form, smartcommon exposes <LoginComponent>, which wraps the complete flow: email + password + optional entity selection + smartAuth QR pair scan:

import { LoginComponent } from '@cap-rel/smartcommon';

<LoginComponent
  onSuccess={(user) => navigate("/")}
  onError={(err) => log.error(err)}
  // QR pair enabled by default. Disable it explicitly when the backend
  // does not expose /qr-pair (non-smartAuth case).
  enableQrPair
  showRememberMe
  labels={{
    emailLabel: t("login.email"),
    submitLabel: t("login.submit"),
    scanQrLabel: t("login.scan-qr"),
  }}
/>

See Advanced components -> LoginComponent for the labels, styling slots and options of the QR pair flow.

Automatic refresh

The useApi hook handles the token refresh automatically:

// When the accessToken expires, useApi:
// 1. Intercepts the 401 error
// 2. Calls GET /refresh with the refreshToken
// 3. Updates the tokens in the session
// 4. Replays the original request

Logout

const handleLogout = async () => {
  await api.private.post('logout');
  setSession(null);
  navigate('/login');
};

Device management

Registering a device

For push notifications, register the device after the login:

const registerDevice = async (pushToken) => {
  await api.private.post('device', {
    json: {
      token: pushToken,
      platform: 'android', // or 'ios', 'web'
      name: 'My phone',
    },
  });
};

Structure on the Dolibarr side

SmartAuth stores the devices in a dedicated table:

Field Description
fk_user ID of the Dolibarr user
token Push token of the device
platform Platform (android, ios, web)
name Device name
last_used Date of last use

Device identification (DeviceIdentificationComponent)

When a user signs in from a device that is not registered yet, SmartAuth can return a user.deviceOptions listing their existing devices so they can choose to pair or to create a new one.

The <DeviceIdentificationComponent> component wraps that flow:

import { DeviceIdentificationComponent } from '@cap-rel/smartcommon';

<DeviceIdentificationComponent
  onSuccess={() => navigate("/")}
  onError={(err) => toast.error(err.message)}
  labels={{ title: t("device.title"), submitLabel: t("device.submit") }}
/>

On submit, api.identifyDevice({ label, uuid }) is called. The smartAuth endpoint clears user.deviceOptions on the server side. See details.

QR pair (login by scanning)

SmartAuth lets a user sign in on mobile by scanning a QR code displayed by an already authenticated workstation, without typing a password. The mobile becomes a "trusted" device persisted locally.

Endpoints

POST /qr-pair/{pairingId}/claim   // Mobile claims the pairing
POST /qr-pair/{pairingId}/poll    // Mobile polls until confirmation

On the useApi side

useApi() exposes two matching methods:

const api = useApi();

// 1. Mobile claims a pairing_id displayed by the PC
const { claim_token } = await api.claimQrPair(pairingId, {
  device_label: "iPhone Eric",
  device_uuid: "u-1",
});

// 2. Mobile polls until the PC confirms
//    Statuses: 'pending' | 'cancelled' | 'expired' | 'consumed'
const data = await api.pollQrPair(pairingId, claim_token);
// data.status === 'consumed': user persisted automatically in local storage

When pollQrPair returns consumed, the user is persisted automatically (by design: a scanned device is trusted). There is no auth helper to call.

Built-in UX: LoginComponent

So that this flow does not have to be reimplemented in every app, <LoginComponent enableQrPair /> (enabled by default) handles scan -> claim -> poll with:

  • An idempotency guard (avoids the 409 on Android with autofocus)
  • A full-screen overlay with a spinner during claim/poll
  • A "Cancel" button and a global timeout
  • Customisable error mapping through getQrErrorLabel

See details.

Backend reference

~/dev/smartauth/api/QrPairController.php. Error codes exposed through error.apiCode:

  • pairing_not_claimable / 409: QR already used
  • pairing_not_found / 404: invalid QR
  • pairing_expired / 410: expired QR
  • rate_limited / 429: too many attempts
  • invalid_pairing_id / 400: incorrect format

Security

Best practices

  • HTTPS mandatory: the JWT tokens travel in clear text
  • Secure storage: use useGlobalStates, which persists in encrypted localStorage
  • Token rotation: the refreshToken is single use
  • Revocation: delete the keys from the Dolibarr interface

If a device is stolen

  1. Sign in to Dolibarr
  2. Go to your user profile
  3. Delete the API key of the stolen device
  4. The associated tokens are invalidated immediately

Password reset

SmartAuth includes a complete token-based password reset system.

Endpoints

Method Route Description Protected
POST /password/reset Reset request (sends an email) No
POST /password/confirm Confirmation with a token No
POST /password/change Password change (signed-in user) Yes

Requesting a reset

// React side
const requestReset = async (email) => {
  await api.public.post('password/reset', {
    json: { email }
  });
};

The server:

  • Checks the user (rate limiting: 3 attempts per 15 minutes)
  • Generates a token with an expiry (1 hour)
  • Sends an email with the reset link

Confirming the reset

const confirmReset = async (token, newPassword) => {
  await api.public.post('password/confirm', {
    json: { token, password: newPassword }
  });
};

Changing the password (signed-in user)

const changePassword = async (oldPassword, newPassword) => {
  await api.private.post('password/change', {
    json: {
      current_password: oldPassword,
      new_password: newPassword
    }
  });
};

Temporary files (SmartTempFile)

SmartAuth provides a temporary file system for downloading binaries (Excel exports, generated PDFs, etc.).

Endpoints

Method Route Description
GET /temp-file/{token} base64 download (JSON)
GET /temp-file/{token}/binary Binary download (stream)
DELETE /temp-file/{token} File deletion

Usage on the PHP side (Controller)

use SmartAuth\Api\SmartTempFile;

public function exportExcel($payload)
{
    // Generate the file
    $filePath = '/tmp/export.xlsx';
    // ... file generation ...

    // Store as a temporary file (default TTL: 1 hour)
    $tempFile = new SmartTempFile();
    $token = $tempFile->store($filePath, [
        'filename' => 'export.xlsx',
        'mimetype' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        'user_id' => $payload['user_id'],
        'entity' => $payload['entity'],
    ]);

    return [['token' => $token], 200];
}

Usage on the React side

// Binary download
const downloadExport = async (token) => {
  const response = await api.get(`temp-file/${token}/binary`, { raw: true });
  const blob = await response.blob();

  // Create a download link
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'export.xlsx';
  a.click();
  URL.revokeObjectURL(url);
};

CORS

SmartAuth handles the CORS headers for cross-origin requests automatically.

Configuration

Dolibarr constant Description Default
SMARTAUTH_CORS_ORIGIN Allowed origin *
SMARTAUTH_CORS_METHODS Allowed methods GET, POST, PUT, DELETE, PATCH, OPTIONS
SMARTAUTH_CORS_HEADERS Allowed headers Content-Type, Authorization, X-DeviceId

The OPTIONS (preflight) requests are handled automatically.

Important

In production, set SMARTAUTH_CORS_ORIGIN to the exact URL of your application instead of *.

Trusted proxies

If your Dolibarr sits behind a reverse proxy (nginx, Apache, etc.), configure the trusted IPs so that SmartAuth detects the client IP address correctly:

Constant Description
SMARTAUTH_TRUSTED_PROXIES Comma-separated list of IPs (e.g. 10.0.0.1,192.168.1.100)

Private IPs (127.x, 10.x, 172.16-31.x, 192.168.x) are automatically treated as trusted proxies.

Local routes (LocalRoutes)

SmartAuth defines local routes that are loaded automatically. They do not need to be declared in your api.php:

Group Routes
Auth /login, /refresh, /logout, /device
Password /password/reset, /password/confirm, /password/change
Files /file/{hash}, /file/{hash}/binary
Temporary files /temp-file/{token}, /temp-file/{token}/binary
Synchronization /sync/register, /sync/pull, /sync/push, /sync/status, /sync/conflicts
PWA /manifest, /icon/{size}

See also