Skip to content

Authentication & Login

The Magic e-VERSE platform uses multiple authentication mechanisms across its services:

ServiceAuth MethodProviderSession
Medusa Admin Panels (10 tenants)Email/passwordMedusa emailpassJWT token
Magic PIM AdminEmail/passwordMedusa emailpassJWT token
Magic PortalEmail/passwordCustom (bcrypt)Express session (MySQL)
B2B StorefrontsIP-based gatekeeperCustom APLT moduleCookie

All tenant admin panels and Magic PIM use Medusa v2’s built-in emailpass auth provider.

Admin navigates to /app/login
POST /auth/user/emailpass
{ email, password }
Medusa verifies against provider_identity table
password stored in provider_metadata->>'password'
hashed with scrypt-kdf (logN:15, r:8, p:1)
┌────┴────┐
│ Success │──► JWT token returned → stored in browser
└─────────┘
│ Failure │──► 401 Unauthorized
└─────────┘

Medusa stores auth data in two tables:

auth_identity — Links auth records to user accounts

ColumnPurpose
idAuth identity ID
app_metadataJSON with user_id linking to user table

provider_identity — Stores credentials per auth provider

ColumnPurpose
entity_idUser’s email address
providerAuth provider name (emailpass)
auth_identity_idFK to auth_identity
provider_metadataJSON with password (scrypt-kdf hash, base64)

Medusa uses the scrypt-kdf npm package (not Node.js built-in crypto.scrypt):

// Hashing (inside @medusajs/auth-emailpass)
import * as scrypt_kdf from "scrypt-kdf"
const hash = await scrypt_kdf.kdf(password, { logN: 15, r: 8, p: 1 })
// Stored as base64-encoded 96-byte buffer
// Verification
const buf = Buffer.from(storedHash, "base64")
const valid = await scrypt_kdf.verify(buf, password)

Custom API route die het standaard Medusa endpoint overschrijft.

  1. Admin klikt “Wachtwoord vergeten” op het login scherm

  2. Frontend stuurt request:

    POST /auth/user/emailpass/reset-password
    { "identifier": "user@example.com" }
  3. Backend genereert JWT token via generateResetPasswordTokenWorkflow (15 min geldig)

  4. Branded e-mail wordt verstuurd via nodemailer met reset-link:

    https://admin-{tenant}.magiceverse.online/app/reset-password?token={jwt}&email={email}
  5. Admin klikt de link, voert nieuw wachtwoord in

  6. Frontend stuurt update:

    POST /auth/user/emailpass/update
    Authorization: Bearer {token}
    { "password": "nieuw-wachtwoord" }
  7. Password sync middleware vangt het response op en propageert de hash naar alle tenants (zie volgende sectie)

SettingWaarde
Hostmail.magiceverse.nl
Port587
Securefalse
Userportal@magiceverse.nl
PasswordVia MAIL_PASSWORD env var
From"Magic PIM" <portal@magiceverse.nl>
backend/src/api/auth/[actor_type]/[auth_provider]/reset-password/route.ts

Retourneert altijd 201 Created (ook bij onbekend e-mailadres, voorkomt information leakage).


Admin reset wachtwoord op tenant X
POST /auth/user/emailpass/update (Medusa built-in)
passwordSyncMiddleware intercepts response
Extracts entity_id from JWT token
syncPasswordToAllTenants(entityId)
┌────┴────────────────────────┐
│ Reads new hash from │
│ provider_identity in │
│ current tenant DB │
└─────────────────────────────┘
Updates provider_identity.provider_metadata->>'password'
in ALL other tenant databases (parallel via Promise.allSettled)

De sync raakt alle 11 databases op dezelfde PostgreSQL server:

DatabaseTenant
master_magicMaster template
magic_pimMagic PIM
magic_b2b_developmentDevelopment
magic_b2b_defaultDefault
magic_b2b_demoDemo
magic_b2b_brinxxBrinxx
magic_b2b_spranzSpranz
magic_b2b_jodasignJoDa Sign
magic_b2b_logohorlogeLogohorloge
magic_b2b_desluisDe Sluis
magic_b2b_bovisalesBovi Sales
VariableDefaultDoel
SYNC_DB_HOSThost.docker.internalPostgreSQL host
SYNC_DB_PORT5432PostgreSQL poort
SYNC_DB_USERpostgresDatabase user
SYNC_DB_PASSWORD(hardcoded fallback)Database wachtwoord
backend/src/utils/password-sync.ts # Sync logica
backend/src/api/middlewares.ts # Middleware wiring

De passwordSyncMiddleware in middlewares.ts:

  1. Intercept POST /auth/user/emailpass/update
  2. Wacht op succesvol response (status 200)
  3. Decodeert JWT uit Authorization: Bearer {token} header
  4. Haalt entity_id (email) uit de token payload
  5. Roept syncPasswordToAllTenants(entityId) aan in de achtergrond
  6. Blokkeert het response niet — sync draait async
// Route binding in middlewares.ts
{
matcher: "/auth/user/emailpass/update",
method: "POST",
middlewares: [passwordSyncMiddleware]
}

De sync logt naar stdout (zichtbaar via docker logs):

[PasswordSync] Syncing password for user@example.com from magic_b2b_brinxx to all tenants...
[PasswordSync] Updated user@example.com in magic_pim
[PasswordSync] Updated user@example.com in magic_b2b_default
[PasswordSync] Done for user@example.com: 8 tenants updated, 0 failed

De Magic Portal gebruikt een apart auth systeem (niet Medusa).

ComponentTechnologie
BackendExpress 4.18 (server.cjs)
DatabaseMySQL (magic_doc)
Sessionsexpress-mysql-session (tabel: portal_sessions)
Hashingbcrypt
Session duur30 dagen
POST /api/auth/login
{ email, password }
Query: SELECT FROM users WHERE email = ? AND is_active = 1
bcrypt.compare(password, password_hash)
┌────┴────┐
│ Success │──► Session aangemaakt → cookie magic_portal_sid
└─────────┘
│ Failure │──► { success: false, message: "Invalid credentials" }
└─────────┘
RouteMethodPurpose
/api/auth/checkGETControleert of sessie actief is
/api/auth/loginPOSTEmail/password login
/api/auth/verify-2faPOST2FA code verificatie
/api/auth/logoutPOSTSessie vernietigen
/api/auth/change-passwordPOSTWachtwoord wijzigen
RolToegang
adminVolledig beheer
developerBeperkt beheer
clientKlantdashboard

B2B storefronts gebruiken een IP-based gatekeeper. Zie Access Control voor de volledige documentatie.


Sommige services zijn beschermd met een extra device-auth laag op nginx-niveau:

include /etc/nginx/snippets/device-auth.conf;
auth_request /___device_auth;

Dit is een aanvullende beveiligingslaag bovenop service-specifieke authenticatie.


Bij het aanmaken van een nieuwe tenant moeten de volgende auth-bestanden aanwezig zijn:

  1. Password reset route kopieren uit master_magic:

    backend/src/api/auth/[actor_type]/[auth_provider]/reset-password/route.ts
  2. Password sync utility kopieren:

    backend/src/utils/password-sync.ts
  3. Middleware updaten — zorg dat middlewares.ts de password sync middleware bevat:

    import { syncPasswordToAllTenants } from "../utils/password-sync"
    import * as jwt from "jsonwebtoken"
    // + passwordSyncMiddleware definitie
    // + route binding voor /auth/user/emailpass/update
  4. Tenant database toevoegen aan de TENANT_DATABASES array in password-sync.ts van alle bestaande tenants

  5. nodemailer dependency toevoegen aan package.json

  6. Backend rebuilden:

    Terminal window
    docker compose up -d --build backend