HomeDocumentation

Documentation

Everything you need to integrate with BM Guardian — from quick start guides to full API reference.

Table of Contents

Overview

BM Guardian is an identity and access management (IAM) platform for educational institutions, providing authentication (email/password + SSO), authorization (RBAC), OAuth2 application management, and a full OAuth2/OIDC Provider (Authorization Code Flow + PKCE, Client Credentials). It supports Multi-Factor Authentication (TOTP + Email OTP), two-level organization hierarchy (parent groups → child schools), user transfer workflows with approval, app-scoped user management API for subscriber apps, and org-scoped user attributes for managing institution-specific metadata. External apps can integrate as “Applications” registered per organization and authenticate users via standard OpenID Connect or manage users programmatically via the Client Credentials grant.

Backendhttp://localhost:3001
Via Proxyhttp://localhost:3000

Quick Start

1

Login to BM Guardian admin UI

Navigate to http://localhost:3000/login and sign in with the default super admin credentials:

Email:    admin@bm-guardian.local
Password: admin123
2

Create an Organization Group & Schools

Navigate to Institutions (/institutions) in the sidebar and click Add Group to create a parent organization. Then expand the group and click Add School to add child institutions underneath it.

3

Register an Application

Navigate to Applications (/apps), select your institution, and click Register Application. Provide:

  • Name — e.g., “My EdTech App”
  • Typeweb, mobile, or api
  • Redirect URIs — One per line (e.g., http://localhost:5174/callback)
4

Copy Credentials

After creating the app, copy the Client ID and Client Secret from the credentials dialog.

Important: The client secret is only shown once during creation. Store it securely — you cannot retrieve it later.

Password Authentication

For apps that use BM Guardian as the primary authentication provider, users log in directly with their email and password. Tokens are managed via httpOnly cookies.

React / Next.js
const API_BASE = process.env.NEXT_PUBLIC_BM_BASE_URL || 'http://localhost:3001';

export async function login(email: string, password: string) {
  const res = await fetch(`${API_BASE}/api/auth/login`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  if (!res.ok) throw new Error('Login failed');
  return res.json(); // { user, memberships }
}

export async function getMe() {
  const res = await fetch(`${API_BASE}/api/auth/me`, {
    credentials: 'include',
  });
  if (!res.ok) throw new Error('Not authenticated');
  return res.json(); // { id, email, firstName, lastName, role, memberships }
}

export async function refreshToken() {
  const res = await fetch(`${API_BASE}/api/auth/refresh`, {
    method: 'POST',
    credentials: 'include',
  });
  return res.ok;
}

export async function logout() {
  await fetch(`${API_BASE}/api/auth/logout`, {
    method: 'POST',
    credentials: 'include',
  });
}

Usage

await login('user@org.com', 'pass123');
const user = await getMe();
// { id, email, firstName, lastName, role, memberships }

Organization Hierarchy

BM Guardian supports a two-level organization hierarchy: parent groups contain child schools. This allows district-level administrators to manage multiple schools under a single umbrella.

Education Group
├── Sample Institution
└── Springfield Elementary

Key Concepts

  • Parent groups (parentId = null) serve as district or network containers.
  • Child schools reference their parent via the parentId field.
  • Two levels only — the API prevents deeper nesting (a parent must itself have no parent).
  • Permission inheritance: parent org admins automatically have authority over all child orgs via the isOrgAdmin() helper.
  • Standalone orgs (no parent, no children) are also supported.

API Usage

Create a parent group (no parentId):

Create group
POST /api/organizations
{ "name": "Education Group", "slug": "education-group" }

Create a child school under the group:

Create school
POST /api/organizations
{ "name": "Springfield High", "slug": "springfield-high",
  "parentId": "<group-id>" }

Filter top-level groups only:

GET /api/organizations?parentId=null

Get an org with its children included:

GET /api/organizations/:id
→ { ...org, children: [{ id, name, slug, isActive, createdAt }] }

User Transfer Workflow

BM Guardian supports transferring users between institutions with an approval-based workflow. The receiving school initiates the request, and the leaving school approves or rejects it.

How it works

1
Receiving school admin creates a transfer request (POST /api/transfers) specifying the user, source org, destination org, and role.
2
Leaving school admin reviews the request and approves (PATCH /api/transfers/:id/approve) or rejects it.
3
On approval, the system atomically: removes the old org membership, creates the new org membership, and clears org-scoped user attributes from the old school.
4
The new school admin sets fresh org-scoped attributes (school email, LMS account, etc.) for the user.

Same-Parent Constraint

By default, transfers are only allowed between schools that share the same parent organization (same group). Super admins can bypass this restriction.

Transfer Statuses

pendingapprovedrejectedcancelled

Endpoints

MethodEndpointDescription
POST/api/transfersCreate transfer request (receiving org admin)
GET/api/transfersList transfers (?orgId=, ?status=)
GET/api/transfers/:idGet transfer details
PATCH/api/transfers/:id/approveApprove and execute transfer
PATCH/api/transfers/:id/rejectReject transfer request
PATCH/api/transfers/:id/cancelCancel pending transfer

Example: Create a Transfer

POST /api/transfers
{
  "userId": "user-uuid",
  "fromOrgId": "sample-institution-id",
  "toOrgId": "springfield-elementary-id",
  "toRole": "member",
  "notes": "Relocating for new term"
}

User Attributes

User attributes are a flexible key-value store for attaching metadata to users. Attributes can be org-scoped (tied to a specific institution) or global (shared across all institutions).

Attribute Types

Org-Scoped

Tied to a specific org. Cleared when a user transfers. Examples: school email, LMS account ID, room number.

Global

Shared across all orgs. Persists through transfers. Examples: employee ID, HR profile URL, certification number.

Endpoints

MethodEndpointDescription
GET/api/user-attributes/users/:userIdGet all attributes for a user
GET/api/user-attributes/users/:userId/organizations/:orgIdGet org-scoped attributes
PUT/api/user-attributes/users/:userId/organizations/:orgIdBatch upsert org-scoped attributes
DELETE/api/user-attributes/users/:userId/organizations/:orgId/:keyDelete a single attribute
PUT/api/user-attributes/users/:userId/globalSet global attributes

Example: Set Org Attributes

PUT /api/user-attributes/users/:userId/organizations/:orgId
{
  "attributes": {
    "school_email": "john@springfield-elementary.edu",
    "lms_account_id": "LMS-2024-001",
    "classroom": "Room 204"
  }
}

OAuth2 Authorization Code Flow + PKCE

BM Guardian acts as a full OAuth2 Authorization Server and OIDC Provider. External apps can authenticate users using the standard Authorization Code Flow with PKCE — the recommended approach for SPAs and mobile apps.

How it works

1
Client generates a code_verifier (random string) and derives a code_challenge (SHA-256 hash, base64url-encoded).
2
Client redirects the user to GET /oauth/authorize with the challenge, client ID, redirect URI, scope, and state.
3
BM Guardian checks if the user is logged in — if not, redirects to /login first.
4
If the user hasn't previously consented, they see the consent page showing the app name, organization, and requested permissions.
5
User clicks “Allow Access” → BM Guardian generates a one-time authorization code (expires in 10 minutes) and redirects to redirect_uri?code=CODE&state=STATE.
6
Client exchanges the code at POST /oauth/token with the code_verifier.
7
BM Guardian validates the PKCE challenge and returns an access_token (RS256 JWT, 1hr), id_token (RS256 JWT with OIDC claims), and scope.
8
Client can call GET /oauth/userinfo with Authorization: Bearer <access_token> to get user claims.

Full React Example

React / Next.js — PKCE Flow
const BM_BASE = process.env.NEXT_PUBLIC_BM_BASE_URL || 'http://localhost:3000';
const CLIENT_ID = process.env.NEXT_PUBLIC_CLIENT_ID!;
const REDIRECT_URI = 'http://localhost:5174/callback';

// --- Step 1: Generate PKCE and redirect to authorize ---

function base64url(buffer: ArrayBuffer): string {
  return btoa(String.fromCharCode(...new Uint8Array(buffer)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}

async function startLogin() {
  const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
  const challenge = base64url(
    await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier))
  );
  const state = base64url(crypto.getRandomValues(new Uint8Array(16)));

  sessionStorage.setItem('pkce_verifier', verifier);
  sessionStorage.setItem('oauth_state', state);

  const params = new URLSearchParams({
    response_type: 'code',
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
    scope: 'openid profile email',
    state,
    code_challenge: challenge,
    code_challenge_method: 'S256',
  });

  window.location.href = `${BM_BASE}/oauth/authorize?${params}`;
}

// --- Step 2: Handle callback and exchange code for tokens ---

async function handleCallback() {
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code');
  const state = params.get('state');

  if (state !== sessionStorage.getItem('oauth_state')) {
    throw new Error('State mismatch — possible CSRF attack');
  }

  const verifier = sessionStorage.getItem('pkce_verifier')!;

  const res = await fetch(`${BM_BASE}/oauth/token`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'authorization_code',
      code: code!,
      redirect_uri: REDIRECT_URI,
      client_id: CLIENT_ID,
      code_verifier: verifier,
    }),
  });

  if (!res.ok) throw new Error('Token exchange failed');
  const { access_token, id_token } = await res.json();

  const payload = JSON.parse(atob(id_token.split('.')[1]));
  console.log('User:', payload.name, payload.email);

  return { access_token, id_token, user: payload };
}

// --- Step 3: Call UserInfo endpoint ---

async function getUserInfo(accessToken: string) {
  const res = await fetch(`${BM_BASE}/oauth/userinfo`, {
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  return res.json();
}

Client Credentials Grant (Machine-to-Machine)

Subscriber apps (such as BM-LMS) can authenticate as themselves — without a user present — to manage users programmatically. This uses the standard OAuth2 Client Credentials grant type.

Prerequisites

  • Register an application and note the Client ID and Client Secret.
  • Configure allowedScopes for the app (any of: users:read, users:write, users:suspend, auth:verify, auth:write, mfa:read, mfa:write).

Token Request

POST /oauth/token
grant_type=client_credentials
&client_id=<your_client_id>
&client_secret=<your_client_secret>
&scope=users:read users:write users:suspend

Response

JSON Response
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "scope": "users:read users:write users:suspend"
}

Note

The access token is RS256-signed, valid for 1 hour, and contains the app_id, org_id, and granted scopes in its payload. You can also authenticate via Authorization: Basic header instead of sending credentials in the body.

App-Scoped User Management API

Apps authenticated via the Client Credentials grant can manage users within their own organization using the /api/v1/users endpoints. All operations are scoped to the app's organization automatically.

Endpoints

MethodEndpointDescription
POST/api/v1/usersCreate user in app's org
GET/api/v1/usersList users in app's org
GET/api/v1/users/:idGet user by ID
PATCH/api/v1/users/:idUpdate user
POST/api/v1/users/:id/suspendSuspend user
POST/api/v1/users/:id/activateActivate user

Example: Create a User

Node.js
// Get access token via client_credentials grant
const tokenRes = await fetch('http://localhost:3001/oauth/token', {
  method: 'POST',
  headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  body: new URLSearchParams({
    grant_type: 'client_credentials',
    client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET,
    scope: 'users:read users:write',
  }),
});
const { access_token } = await tokenRes.json();

// Create a user in the app's organization
const userRes = await fetch('http://localhost:3001/api/v1/users', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${access_token}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    email: 'jane.doe@school.edu',
    firstName: 'Jane',
    lastName: 'Doe',
    password: 'secureP@ss1',
  }),
});
const newUser = await userRes.json();

Important

  • Org scoping — Apps can only manage users within their own organization. The org_id is derived from the access token.
  • Auto-provisioning — User creation/suspension/activation automatically triggers provisioning to connected directories (Google Workspace, Microsoft Entra).
  • Existing users — If a user already exists globally but not in the app's org, they are added to the org (200 with isNew: false). If already in the org, returns 409 Conflict.

App-Scoped Auth API

Apps authenticated via the Client Credentials grant can verify user credentials, handle MFA challenges, and manage passwords on behalf of their users. The app manages its own sessions — no cookies are set by these endpoints.

Credential Verification Flow

1
App calls POST /api/v1/auth/verify with user email and password. If no MFA, returns { verified: true, user }.
2
If MFA is required, returns mfaPendingToken and availableMethods. App initiates challenge via POST /api/v1/auth/mfa/challenge.
3
App verifies MFA code via POST /api/v1/auth/mfa/verify. On success, returns the verified user object.

Endpoints

MethodEndpointDescription
POST/api/v1/auth/verifyVerify user email + password
POST/api/v1/auth/mfa/challengeInitiate MFA challenge for user
POST/api/v1/auth/mfa/verifyVerify MFA code to complete auth
POST/api/v1/auth/forgot-passwordSend password reset email (with app's resetBaseUrl)
POST/api/v1/auth/reset-passwordReset password using token from email
POST/api/v1/auth/change-passwordChange password (requires current password)

Example: Verify Credentials

POST /api/v1/auth/verify
// Request
{ "email": "student@school.edu", "password": "password123" }

// Response (no MFA)
{ "verified": true, "user": { "id": "uuid", "email": "student@school.edu", "orgRole": "member", ... } }

// Response (MFA required)
{ "verified": false, "mfaRequired": true, "mfaPendingToken": "<jwt>", "availableMethods": ["totp"] }

Example: App-Scoped Password Reset

POST /api/v1/auth/forgot-password
// Request — resetBaseUrl is YOUR app's reset page
{
  "email": "student@school.edu",
  "resetBaseUrl": "https://myapp.com/reset-password"
}
// Email sent with link: https://myapp.com/reset-password?token=<token>

// Then reset:
// POST /api/v1/auth/reset-password
{ "token": "<token-from-email>", "password": "new-password" }

Key Difference

Unlike the admin password reset (/api/auth/forgot-password), the app-scoped version accepts a resetBaseUrl parameter so the reset link points to your app's UI instead of the BM Guardian dashboard.

App-Scoped MFA Management API

Apps can manage MFA enrollment for users within their organization. This allows subscriber apps to build custom MFA enrollment flows without requiring users to visit the BM Guardian admin dashboard.

Endpoints

MethodEndpointDescription
GET/api/v1/users/:id/mfa/statusMFA enrollment status + org policy
POST/api/v1/users/:id/mfa/totp/setupGenerate TOTP secret + QR code
POST/api/v1/users/:id/mfa/totp/verifyVerify TOTP to complete enrollment
POST/api/v1/users/:id/mfa/email/setupSend email OTP for enrollment
POST/api/v1/users/:id/mfa/email/verifyVerify email OTP to complete enrollment
POST/api/v1/users/:id/mfa/recovery-codesGenerate 10 recovery codes
DELETE/api/v1/users/:id/mfa/methods/:methodIdRemove an MFA method
POST/api/v1/users/:id/mfa/resetFull MFA reset (removes all methods)

Example: TOTP Enrollment via App

Node.js
// Step 1: Generate TOTP QR code
const setupRes = await fetch(`http://localhost:3001/api/v1/users/${userId}/mfa/totp/setup`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${appToken}` },
});
const { qrCode, secret } = await setupRes.json();
// Show qrCode (data URL) to the user in your app

// Step 2: User scans QR, enters code from authenticator app
const verifyRes = await fetch(`http://localhost:3001/api/v1/users/${userId}/mfa/totp/verify`, {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${appToken}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ code: '123456' }),
});
// { success: true, message: "TOTP enrolled successfully" }

Note

When an org's MFA policy is required, the last active MFA method cannot be removed (returns 400) unless the user is marked as MFA-exempt by a super admin.

OIDC Endpoints

OIDC Discovery

Returns the full OpenID Connect discovery document with all endpoint URLs, supported scopes, algorithms, and claims.

GET /.well-known/openid-configuration

JWKS (JSON Web Key Set)

Returns the RSA public key used to verify ID tokens and access tokens (RS256).

GET /.well-known/jwks.json
GET /oauth/jwks

Authorization Endpoint

GET /oauth/authorize?
  response_type=code
  &client_id=<your_client_id>
  &redirect_uri=<registered_redirect_uri>
  &scope=openid+profile+email
  &state=<random_csrf_state>
  &code_challenge=<base64url(SHA256(code_verifier))>
  &code_challenge_method=S256
  &nonce=<optional_nonce>
ParameterRequiredDescription
response_typeYesMust be "code"
client_idYesFrom application registration
redirect_uriYesMust exactly match a registered redirect URI
scopeYesSpace-separated: openid, profile, email
stateYesRandom string for CSRF protection (echoed back)
code_challengeNoPKCE challenge (SHA-256 of code_verifier, base64url-encoded). Required for public clients.
code_challenge_methodNoS256 (recommended) or plain
nonceNoIncluded in the ID token if provided

Token Exchange

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=<authorization_code>
&redirect_uri=<same_as_authorize>
&client_id=<your_client_id>
&code_verifier=<original_code_verifier>

Authentication Methods

  • Public clients (SPAs, mobile): Use PKCE (code_verifier parameter)
  • Confidential clients (server-side): Use client_secret in body or Authorization: Basic header
  • Both PKCE and client_secret can be used together for maximum security

Response

JSON Response
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "id_token": "eyJhbGciOiJSUzI1NiIs...",
  "scope": "openid profile email"
}

UserInfo Endpoint

GET /oauth/userinfo
Authorization: Bearer <access_token>

Returns user claims based on the token's scope.

ID Token Claims

The id_token is a JWT signed with RS256 containing:

ClaimScopeDescription
subalwaysUser ID
issalwaysIssuer URL (BM Guardian)
audalwaysClient ID
expalwaysExpiry timestamp
iatalwaysIssued-at timestamp
nonceif providedEchoed nonce from authorize request
emailemailUser's email address
email_verifiedemailWhether email is verified
nameprofileFull name ("First Last")
given_nameprofileFirst name
family_nameprofileLast name
org_rolesalwaysArray of { org_id, org_slug, org_name, role } for each organization membership
app_rolesalwaysArray of { app_id, app_name, client_id, roles[] } for per-app RBAC assignments

Example decoded id_token payload

{
  "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "iss": "http://localhost:3001",
  "aud": "app_xxxxxxxxxxxxxxxx",
  "exp": 1719000000,
  "iat": 1718996400,
  "nonce": "n-0S6_WzA2Mj",
  "email": "jane.doe@school.edu",
  "email_verified": true,
  "name": "Jane Doe",
  "given_name": "Jane",
  "family_name": "Doe",
  "org_roles": [
    {
      "org_id": "org_abc123",
      "org_slug": "springfield-elementary",
      "org_name": "Springfield Elementary",
      "role": "admin"
    },
    {
      "org_id": "org_def456",
      "org_slug": "shelbyville-high",
      "org_name": "Shelbyville High",
      "role": "member"
    }
  ],
  "app_roles": [
    {
      "app_id": "app_xyz789",
      "app_name": "Learning Portal",
      "client_id": "client_abc...",
      "roles": ["staff", "app_admin"]
    }
  ]
}

Using org_roles for authorization in your app

React / Next.js — check org roles
// After obtaining the id_token or calling /oauth/userinfo:

interface OrgRole {
  org_id: string;
  org_slug: string;
  org_name: string;
  role: 'owner' | 'admin' | 'member';
}

// Decode the id_token (or use the userinfo response directly)
const payload = JSON.parse(atob(idToken.split('.')[1]));
const orgRoles: OrgRole[] = payload.org_roles ?? [];

// Check if the user belongs to a specific org
function isMemberOf(orgSlug: string): boolean {
  return orgRoles.some((r) => r.org_slug === orgSlug);
}

// Check if the user is an admin (or owner) of a specific org
function isOrgAdmin(orgSlug: string): boolean {
  return orgRoles.some(
    (r) => r.org_slug === orgSlug && (r.role === 'admin' || r.role === 'owner')
  );
}

// Guard a route or component
if (!isMemberOf('springfield-elementary')) {
  return <p>You do not have access to this institution.</p>;
}

// Show admin-only UI
{isOrgAdmin('springfield-elementary') && (
  <button>Manage Settings</button>
)}

Multi-Factor Authentication (MFA)

BM Guardian supports TOTP (authenticator apps like Google Authenticator, Authy) and Email OTP (6-digit code sent via SMTP) as MFA methods. MFA can be configured at both the organization level and per-user.

Organization MFA Policy

off

MFA not available for org members

optional

Members can self-enroll in MFA

required

All members must enroll; enforced on next login

Login Flow with MFA

1
User logs in with email/password via POST /api/auth/login.
2
If MFA is enabled, the response contains a mfaPendingToken (JWT, 10min TTL) and availableMethods array instead of setting cookies.
3
Client initiates a challenge via POST /api/mfa/challenge with the pending token and chosen method. For email_otp, a code is sent.
4
Client verifies the code via POST /api/mfa/challenge/verify. On success, real httpOnly cookies are set and login is complete.

MFA Enrollment Endpoints

MethodEndpointDescription
GET/api/mfa/statusEnrollment status + org policy
POST/api/mfa/totp/setupGenerate TOTP secret + QR code
POST/api/mfa/totp/verifyVerify TOTP to activate enrollment
POST/api/mfa/email/setupSend OTP to user's email
POST/api/mfa/email/verifyVerify email OTP to activate
POST/api/mfa/recovery-codesGenerate 10 recovery codes
DELETE/api/mfa/methods/:idRemove enrolled method

MFA Challenge Endpoints

MethodEndpointDescription
POST/api/mfa/challengeInitiate MFA challenge
POST/api/mfa/challenge/verifyVerify code to complete login

Admin MFA Management

MethodEndpointDescription
POST/api/mfa/admin/reset/:userIdReset user's MFA enrollment
PATCH/api/mfa/admin/exempt/:userIdToggle MFA exemption
Recovery codes are shown only once when generated. Users should store them securely. After 5 failed MFA attempts, the challenge is invalidated and the user must re-login. Super admins can reset a user's MFA enrollment if they are locked out.

SSO Provider Setup

Institution admins can configure SSO providers per organization. Navigate to SSO Providers (/providers) in the admin sidebar.

1
Select an institution from the dropdown
2
Click Add Provider and choose a provider type
3
Fill in the required credentials for your chosen provider
4
Optionally link the provider to a specific application

Supported Provider Types

Google

Client ID + Client Secret

Microsoft

Client ID + Client Secret (Entra ID)

LinkedIn

Client ID + Client Secret

Meta

Client ID + Client Secret

OIDC

Client ID + Secret + Endpoint URLs

SAML

Entry Point + X.509 Certificate

Security

Client secrets and SAML certificates are encrypted with AES-256 before storage. They are never exposed in API responses.

API Reference

Authentication

MethodEndpointDescription
POST/api/auth/registerRegister new user
POST/api/auth/loginLogin (sets httpOnly cookies)
POST/api/auth/refreshRefresh access token
POST/api/auth/logoutClear auth cookies
GET/api/auth/meGet current user + memberships
POST/api/auth/forgot-passwordRequest password reset email
POST/api/auth/reset-passwordReset password with token from email

Organizations

MethodEndpointDescription
GET/api/organizationsList organizations (?parentId= filter)
POST/api/organizationsCreate org (accepts parentId for hierarchy)
GET/api/organizations/:idGet org details + members + children
PATCH/api/organizations/:idUpdate organization (incl. parentId)
DELETE/api/organizations/:idDelete organization (cascades children)

Userssuper_admin

MethodEndpointDescription
GET/api/usersList all users with org memberships
GET/api/users/:idGet user details
PATCH/api/users/:idUpdate user (name, active status)
PATCH/api/users/:id/suspendSuspend user (terminates sessions)
PATCH/api/users/:id/activateReactivate suspended/deleted user
DELETE/api/users/:idSoft-delete user (preserves data)
GET/api/users/:id/exportGDPR data export (JSON)
DELETE/api/users/:id/eraseRight to Erasure — anonymize PII (GDPR Art. 17)

Applicationsorg-scoped

MethodEndpointDescription
GET/api/applications/organizations/:orgIdList org applications
POST/api/applications/organizations/:orgIdCreate app (returns credentials)
PATCH/api/applications/organizations/:orgId/:idUpdate application
DELETE/api/applications/organizations/:orgId/:idDelete application

SSO Providersorg-scoped

MethodEndpointDescription
GET/api/providers/organizations/:orgIdList org SSO providers
POST/api/providers/organizations/:orgIdCreate SSO provider
GET/api/providers/organizations/:orgId/:idGet provider details
PATCH/api/providers/organizations/:orgId/:idUpdate provider
DELETE/api/providers/organizations/:orgId/:idDelete provider

Transfers

MethodEndpointDescription
POST/api/transfersCreate transfer request
GET/api/transfersList transfers (?orgId, ?status)
GET/api/transfers/:idGet transfer details
PATCH/api/transfers/:id/approveApprove and execute transfer
PATCH/api/transfers/:id/rejectReject transfer request
PATCH/api/transfers/:id/cancelCancel pending transfer

Application Rolesper-app RBAC

MethodEndpointDescription
GET/api/app-roles/:appId/rolesList role definitions for an app
POST/api/app-roles/:appId/rolesCreate a role definition
PATCH/api/app-roles/:appId/roles/:roleIdUpdate a role definition
DELETE/api/app-roles/:appId/roles/:roleIdDelete role (cascades assignments)
GET/api/app-roles/:appId/usersList user-role assignments
POST/api/app-roles/:appId/usersAssign a role to a user
DELETE/api/app-roles/:appId/users/:assignmentIdRemove a user-role assignment

User Attributes

MethodEndpointDescription
GET/api/user-attributes/users/:userIdGet all attributes for a user
GET/api/user-attributes/users/:userId/organizations/:orgIdGet org-scoped attributes
PUT/api/user-attributes/users/:userId/organizations/:orgIdBatch upsert org-scoped attributes
DELETE/api/user-attributes/users/:userId/organizations/:orgId/:keyDelete a single attribute
PUT/api/user-attributes/users/:userId/globalSet global attributes

Platform Managementsuper_admin

MethodEndpointDescription
GET/api/statsDashboard statistics
GET/api/settingsGet platform settings
PATCH/api/settingsUpdate platform settings

OAuth2 / OIDC Provider

MethodEndpointDescription
GET/oauth/authorizeAuthorization endpoint
POST/oauth/authorizeProcess consent (approve/deny)
POST/oauth/tokenToken exchange (auth_code + client_credentials)
GET/oauth/userinfoGet user claims from access token
GET/oauth/jwksRSA public key (JWK format)
GET/oauth/client-infoPublic app metadata for consent UI
GET/.well-known/openid-configurationOIDC discovery document
GET/.well-known/jwks.jsonJWKS (standard path)

Multi-Factor Authentication

MethodEndpointDescription
GET/api/mfa/statusMFA enrollment status + org policy
POST/api/mfa/totp/setupGenerate TOTP secret + QR code
POST/api/mfa/totp/verifyVerify TOTP to activate enrollment
POST/api/mfa/email/setupSend OTP to user's email
POST/api/mfa/email/verifyVerify email OTP to activate
POST/api/mfa/recovery-codesGenerate 10 recovery codes
DELETE/api/mfa/methods/:idRemove enrolled method
POST/api/mfa/challengeInitiate MFA challenge
POST/api/mfa/challenge/verifyVerify code to complete login
POST/api/mfa/admin/reset/:userIdReset user's MFA
PATCH/api/mfa/admin/exempt/:userIdToggle MFA exemption

App-Scoped User APIclient_credentials

MethodEndpointDescription
POST/api/v1/usersCreate user in app's org
GET/api/v1/usersList users in app's org
GET/api/v1/users/:idGet user by ID
PATCH/api/v1/users/:idUpdate user
POST/api/v1/users/:id/suspendSuspend user
POST/api/v1/users/:id/activateActivate user

App-Scoped Auth APIclient_credentials

MethodEndpointDescription
POST/api/v1/auth/verifyVerify user credentials
POST/api/v1/auth/mfa/challengeInitiate MFA challenge
POST/api/v1/auth/mfa/verifyVerify MFA code
POST/api/v1/auth/forgot-passwordSend password reset email
POST/api/v1/auth/reset-passwordReset password with token
POST/api/v1/auth/change-passwordChange password

App-Scoped MFA APIclient_credentials

MethodEndpointDescription
GET/api/v1/users/:id/mfa/statusMFA enrollment status + org policy
POST/api/v1/users/:id/mfa/totp/setupGenerate TOTP QR code
POST/api/v1/users/:id/mfa/totp/verifyVerify TOTP enrollment
POST/api/v1/users/:id/mfa/email/setupSend email OTP for enrollment
POST/api/v1/users/:id/mfa/email/verifyVerify email OTP enrollment
POST/api/v1/users/:id/mfa/recovery-codesGenerate recovery codes
DELETE/api/v1/users/:id/mfa/methods/:methodIdRemove MFA method
POST/api/v1/users/:id/mfa/resetFull MFA reset

Audit Logssuper_admin

MethodEndpointDescription
GET/api/audit-logsList audit logs (?userId, ?actorId, ?action, ?limit, ?offset)

Provisioningsuper_admin

MethodEndpointDescription
GET/api/provisioning/connectorsList provisioning connectors
POST/api/provisioning/connectorsCreate connector (Google Workspace, Entra, SCIM)
PATCH/api/provisioning/connectors/:idUpdate connector
DELETE/api/provisioning/connectors/:idDelete connector
POST/api/provisioning/connectors/:id/testTest connector connection
POST/api/provisioning/connectors/:id/provision/:userIdProvision single user
GET/api/provisioning/eventsList provisioning events
POST/api/provisioning/events/:id/retryRetry failed event
GET/api/provisioning/sync-statusPer-user sync state dashboard

Swagger UI

BM Guardian includes a built-in Swagger UI for interactive API exploration. All endpoints are documented with request/response schemas, parameter descriptions, and authentication requirements.

Swagger UIhttp://localhost:3001/swagger
Swagger JSONhttp://localhost:3001/swagger/json

Note

When running the full app, Swagger UI is also accessible via the Next.js proxy at http://localhost:3000/swagger.

Audit Logging & Provisioning

BM Guardian maintains a comprehensive audit trail for accountability (GDPR/DPDP compliance) and supports outbound user provisioning to external identity systems.

Audit Logs

Every significant admin action is recorded: user login/logout, suspension, deletion, erasure, MFA enrollment, provisioning events, and more. Each log entry captures the actor, action, target, metadata, IP address, and timestamp.

user.loginuser.login_faileduser.suspenduser.activateuser.deleteuser.eraseuser.exportmfa.totp_enrolledmfa.resetprovisioning.connector.createprovisioning.provision

Outbound Provisioning

Provisioning connectors sync user lifecycle events (creation, suspension, activation, deletion) to external identity systems. Events are dispatched automatically when users are managed via the admin dashboard or the App-Scoped API.

Google Workspace

Sync users to Google Admin Directory

Microsoft Entra

Sync users to Azure AD / Entra ID

Generic SCIM

Sync via standard SCIM 2.0 protocol

Note

Provisioning connector configs are AES-256 encrypted at rest. Failed events can be retried manually. The sync status dashboard shows per-user, per-connector state with aggregate statistics.

GDPR & Data Privacy Compliance

BM Guardian includes built-in support for GDPR (EU) and DPDP (India) data subject rights, including data portability and the right to erasure.

Data Portability (GDPR Art. 20)

Super admins can export all user data as JSON via GET /api/users/:id/export. The export includes: user profile, organization memberships, user attributes, transfers, app role assignments, sessions, OAuth consents, linked SSO providers, and audit logs.

Right to Erasure (GDPR Art. 17 / DPDP Sec. 12)

The DELETE /api/users/:id/erase endpoint anonymizes a user's PII (email becomes erased-uuid@anonymized.local, name becomes “Erased User”) and removes all associated data: sessions, attributes, OAuth consents, app role assignments, and org memberships. Audit log entries are preserved but the actor reference is set to null.

Erasure is irreversible. The user can only be erased if they are already suspended or soft-deleted. This is a two-step process: first soft-delete, then erase.

Environment Variables

Create a .env file in the server/ directory:

.env
# JWT Secrets (generate your own for production)
JWT_ACCESS_SECRET=your-access-secret-key-here
JWT_REFRESH_SECRET=your-refresh-secret-here

# Database (defaults to ./data/bm-guardian.db)
DATABASE_PATH=./data/bm-guardian.db

# Server
SERVER_PORT=3001
LOG_LEVEL=info

# OIDC (defaults to http://localhost:3000)
ISSUER_URL=http://localhost:3000

# SMTP (for Email OTP — falls back to console.log if not set)
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your-smtp-user
SMTP_PASS=your-smtp-password
SMTP_FROM=noreply@bm-guardian.com

# Background cleanup interval (default: 900000 = 15 minutes)
CLEANUP_INTERVAL_MS=900000

For external client apps connecting to BM Guardian:

.env (client app)
NEXT_PUBLIC_BM_BASE_URL=http://localhost:3001
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret

Security Best Practices

  • Always use HTTPS in production
  • Validate redirect URIs — must match exactly what's registered
  • Use httpOnly cookies — tokens are never exposed to JavaScript
  • Token rotation — refresh tokens proactively, revoke on logout
  • CORS — pre-configured for localhost in dev; add production origins in server/src/index.ts
  • Credential encryption — client secrets and SAML certs are AES-256 encrypted at rest
  • OAuth2 tokens signed with RS256 — verify ID tokens and access tokens using the JWKS endpoint
  • PKCE enforcement — public clients must use PKCE; confidential clients must provide client_secret
  • One-time authorization codes — codes are deleted immediately after use and expire in 10 minutes
  • Transfer approval workflow — org-scoped data is atomically cleaned up during user transfers
  • Permission inheritance — parent org admins inherit access to all child organizations
  • MFA — TOTP with ±1 step window (30s tolerance), Email OTP with 5-minute expiry, max 5 attempts per challenge
  • TOTP secrets encrypted with AES-256 at rest; recovery codes bcrypt-hashed and single-use
  • Background cleanup — expired MFA challenges, sessions, and OAuth codes automatically purged every 15 minutes
  • App-scoped API — client credentials tokens are org-scoped; apps can only manage users within their own organization

Troubleshooting

CORS errors when calling the API
Check server/src/index.ts → CORS configuration. Ensure your frontend origin is listed in the allowed origins. In development, http://localhost:3000 is pre-configured.
Invalid client error during OAuth flow
Verify that your redirect_uri exactly matches one of the registered URIs for the application. No wildcards are allowed — the match must be exact, including trailing slashes.
Token expired / 401 Unauthorized
Call POST /api/auth/refresh to get a new access token. If that also fails, the user needs to log in again. Ensure credentials: 'include' is set on all fetch calls.
Database table missing or migration errors
Run the migration: cd server && npx tsx src/db/migrate.ts — this creates all tables with CREATE TABLE IF NOT EXISTS.
Backend server not starting
Check terminal output or /tmp/bm-server.log if running in background. Common issues: port 3001 already in use, missing .env file, or SQLite database locked.
RSA keys not found / JWKS empty
RSA keys are auto-generated on first server start and saved to server/data/rsa-private.pem and server/data/rsa-public.pem. Ensure the data directory is writable.
Transfer fails with 'orgs must share the same parent'
Transfers are restricted to schools within the same parent group. Both the from and to organizations must have the same parentId. Super admins can bypass this constraint.
Cannot create a school under another school
BM Guardian enforces a two-level hierarchy only. The parent organization must be a top-level group (parentId = null). You cannot nest a school under another school.
MFA challenge expired or too many attempts
MFA pending tokens expire after 10 minutes. Email OTP codes expire after 5 minutes. After 5 failed attempts, the challenge is invalidated. The user must re-login to get a new pending token.
User locked out of MFA
Users can use a recovery code instead of TOTP/email OTP during the challenge step. If they've lost all recovery codes, a super_admin can reset their MFA via POST /api/mfa/admin/reset/:userId.
Email OTP not received
Check SMTP configuration in server/.env (SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, SMTP_FROM). If SMTP vars are not set, codes are logged to the server console (dev mode only).
Client credentials token returns 'invalid_scope'
The application must have allowedScopes configured. Check the application's settings and ensure the requested scopes (users:read, users:write, users:suspend) are included in allowedScopes.