Documentation

Authentication

Every Aozo product defers identity to one Central Auth server. Here's exactly what that means for your integration.

💡 Core philosophy: pure identity provider

Central Auth strictly handles authentication — verifying who the user is. Authorization (roles and permissions) is left entirely to each downstream product's own database.

The JWT payload

When a user signs in, they receive a signed JWT with basic identity claims — nothing more:

{
  "id": 1,
  "email": "user@domain.com",
  "name": "John Doe",
  "iat": 1690000000,
  "exp": 1690086400
}

There are no roles or permissions inside this token by design. Any product that needs RBAC builds it locally, keyed off email.

How each product actually verifies you

All six products trust the same JWT, but the verification path — and what else they'll accept — differs. This is real, current behaviour per product, not a spec:

ProductPrimary checkFallbackAlso accepts
CRM / AccountingLocal JWT verify (AOZO_JWT_SECRET)Calls account.aozo.in/api/auth/sso/verifyaozo_-prefixed API key, looked up via Prisma
HelpdeskLocal JWT verifyCalls Central Auth verify endpointx-api-key + x-workspace-id header pair
Projects / TeamsLocal JWT verifyCalls Central Auth verify endpointNothing else — Bearer JWT only
ChatSigned session cookie, issued at login by its own /ajax/sso-loginBearer JWT verified locally (no central-server fallback in the main gate)Session cookie is primary; no API-key support

Integrating the callback

The most common point of confusion is the callback. Here's the flow:

  1. Your app — user clicks "Log in". Redirect to https://account.aozo.in/login?redirect=YOUR_APP_CALLBACK_URL.
  2. Central Auth — user enters email/password and signs in.
  3. Central Auth — redirects back to the URL you provided, with the token attached: YOUR_APP_CALLBACK_URL?token=eyJhbG....
  4. Your app — reads the token from the URL, stores it, and sends it as a Bearer token on every API call.

React.js (frontend)

Trigger login:

const handleLoginClick = () => {
  const myCallbackUrl = encodeURIComponent("http://localhost:3000/callback");
  window.location.href = `https://account.aozo.in/login?redirect=${myCallbackUrl}`;
};

Handle the callback (a Callback.jsx route):

import { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';

export default function Callback() {
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    const params = new URLSearchParams(location.search);
    const token = params.get('token');

    if (token) {
      localStorage.setItem('aozo_token', token);
      navigate('/dashboard');
    } else {
      navigate('/login');
    }
  }, [location, navigate]);

  return 
Logging you in… please wait.
; }

Next.js (App Router)

Trigger login:

'use client'
export default function LoginButton() {
  const handleLoginClick = () => {
    const myCallbackUrl = encodeURIComponent("http://localhost:3000/callback");
    window.location.href = `https://account.aozo.in/login?redirect=${myCallbackUrl}`;
  };
  return ;
}

Handle the callback (app/callback/page.tsx):

'use client';
import { useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';

export default function CallbackPage() {
  const router = useRouter();
  const searchParams = useSearchParams();
  const token = searchParams.get('token');

  useEffect(() => {
    if (token) {
      localStorage.setItem('aozo_token', token);
      router.push('/dashboard');
    } else {
      router.push('/login');
    }
  }, [token, router]);

  return 
Authenticating… please wait.
; }

Node.js / Express (backend)

Verify the token using the exact same AOZO_JWT_SECRET as Central Auth:

const jwt = require('jsonwebtoken');

const requireAuth = (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'No token provided' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.AOZO_JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
};

Laravel (PHP backend)

Install firebase/php-jwt and add a middleware:

<?php

namespace App\Http\Middleware;
use Closure;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;

class VerifyAozoToken
{
    public function handle($request, Closure $next)
    {
        $token = $request->bearerToken();
        if (!$token) return response()->json(['error' => 'No token provided'], 401);

        try {
            $secret = env('AOZO_JWT_SECRET');
            $decoded = JWT::decode($token, new Key($secret, 'HS256'));
            $request->attributes->add(['user' => $decoded]);
            return $next($request);
        } catch (Exception $e) {
            return response()->json(['error' => 'Invalid Token'], 401);
        }
    }
}

Building your own RBAC on top

Since Central Auth only handles identity, your product needs its own local tables for roles and permissions. This is the schema every Aozo product uses internally:

-- 1. Roles
CREATE TABLE IF NOT EXISTS role (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) UNIQUE NOT NULL
);

-- 2. Permissions
CREATE TABLE IF NOT EXISTS permission (
    id INT AUTO_INCREMENT PRIMARY KEY,
    permission_group VARCHAR(100) NOT NULL,
    permission VARCHAR(100) UNIQUE NOT NULL
);

-- 3. Users
CREATE TABLE IF NOT EXISTS user (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(255),
    role_id INT,
    FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE SET NULL
);

-- 4. Role → Permission mapping
CREATE TABLE IF NOT EXISTS role_wise_permission (
    id INT AUTO_INCREMENT PRIMARY KEY,
    role_id INT NOT NULL,
    permission_id INT NOT NULL,
    UNIQUE KEY unique_role_permission (role_id, permission_id),
    FOREIGN KEY (role_id) REFERENCES role(id) ON DELETE CASCADE,
    FOREIGN KEY (permission_id) REFERENCES permission(id) ON DELETE CASCADE
);

Then authorize a request in a single optimized query, once the JWT is verified and req.user.email is populated:

SELECT
    u.name,
    u.email,
    r.name AS role_name,
    JSON_ARRAYAGG(p.permission) AS permissions
FROM user u
JOIN role r ON u.role_id = r.id
JOIN role_wise_permission rwp ON r.id = rwp.role_id
JOIN permission p ON rwp.permission_id = p.id
WHERE u.email = ?
GROUP BY u.id;

This returns a clean JSON object with the user's role and every permission it grants — your application logic can grant or deny from there.