Everything you need to integrate with BM Guardian — from quick start guides to full API reference.
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.
http://localhost:3001http://localhost:3000Login 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: admin123Create 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.
Register an Application
Navigate to Applications (/apps), select your institution, and click Register Application. Provide:
web, mobile, or apihttp://localhost:5174/callback)Copy Credentials
After creating the app, copy the Client ID and Client Secret from the credentials dialog.
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.
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 }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.
Create a parent group (no parentId):
POST /api/organizations
{ "name": "Education Group", "slug": "education-group" }Create a child school under the group:
POST /api/organizations
{ "name": "Springfield High", "slug": "springfield-high",
"parentId": "<group-id>" }Filter top-level groups only:
GET /api/organizations?parentId=nullGet an org with its children included:
GET /api/organizations/:id
→ { ...org, children: [{ id, name, slug, isActive, createdAt }] }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.
POST /api/transfers) specifying the user, source org, destination org, and role.PATCH /api/transfers/:id/approve) or rejects it.Same-Parent Constraint
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/transfers | Org Admin | Create transfer request (receiving org admin) |
| GET | /api/transfers | Authenticated | List transfers (?orgId=, ?status=) |
| GET | /api/transfers/:id | Authenticated | Get transfer details |
| PATCH | /api/transfers/:id/approve | Org Admin | Approve and execute transfer |
| PATCH | /api/transfers/:id/reject | Org Admin | Reject transfer request |
| PATCH | /api/transfers/:id/cancel | Requester | Cancel pending transfer |
{
"userId": "user-uuid",
"fromOrgId": "sample-institution-id",
"toOrgId": "springfield-elementary-id",
"toRole": "member",
"notes": "Relocating for new term"
}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).
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.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/user-attributes/users/:userId | Org Admin | Get all attributes for a user |
| GET | /api/user-attributes/users/:userId/organizations/:orgId | Org Admin | Get org-scoped attributes |
| PUT | /api/user-attributes/users/:userId/organizations/:orgId | Org Admin | Batch upsert org-scoped attributes |
| DELETE | /api/user-attributes/users/:userId/organizations/:orgId/:key | Org Admin | Delete a single attribute |
| PUT | /api/user-attributes/users/:userId/global | Super Admin | Set global attributes |
{
"attributes": {
"school_email": "john@springfield-elementary.edu",
"lms_account_id": "LMS-2024-001",
"classroom": "Room 204"
}
}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.
code_verifier (random string) and derives a code_challenge (SHA-256 hash, base64url-encoded).GET /oauth/authorize with the challenge, client ID, redirect URI, scope, and state./login first.redirect_uri?code=CODE&state=STATE.POST /oauth/token with the code_verifier.access_token (RS256 JWT, 1hr), id_token (RS256 JWT with OIDC claims), and scope.GET /oauth/userinfo with Authorization: Bearer <access_token> to get user claims.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();
}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.
grant_type=client_credentials
&client_id=<your_client_id>
&client_secret=<your_client_secret>
&scope=users:read users:write users:suspend{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "users:read users:write users:suspend"
}Note
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.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.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/v1/users | Bearer (app) | Create user in app's org |
| GET | /api/v1/users | Bearer (app) | List users in app's org |
| GET | /api/v1/users/:id | Bearer (app) | Get user by ID |
| PATCH | /api/v1/users/:id | Bearer (app) | Update user |
| POST | /api/v1/users/:id/suspend | Bearer (app) | Suspend user |
| POST | /api/v1/users/:id/activate | Bearer (app) | Activate user |
// 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
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.
POST /api/v1/auth/verify with user email and password. If no MFA, returns { verified: true, user }.mfaPendingToken and availableMethods. App initiates challenge via POST /api/v1/auth/mfa/challenge.POST /api/v1/auth/mfa/verify. On success, returns the verified user object.| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/verify | Bearer (auth:verify) | Verify user email + password |
| POST | /api/v1/auth/mfa/challenge | Bearer (auth:verify) | Initiate MFA challenge for user |
| POST | /api/v1/auth/mfa/verify | Bearer (auth:verify) | Verify MFA code to complete auth |
| POST | /api/v1/auth/forgot-password | Bearer (auth:write) | Send password reset email (with app's resetBaseUrl) |
| POST | /api/v1/auth/reset-password | Bearer (auth:write) | Reset password using token from email |
| POST | /api/v1/auth/change-password | Bearer (auth:write) | Change password (requires current password) |
// 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"] }// 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
/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.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.
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/v1/users/:id/mfa/status | Bearer (mfa:read) | MFA enrollment status + org policy |
| POST | /api/v1/users/:id/mfa/totp/setup | Bearer (mfa:write) | Generate TOTP secret + QR code |
| POST | /api/v1/users/:id/mfa/totp/verify | Bearer (mfa:write) | Verify TOTP to complete enrollment |
| POST | /api/v1/users/:id/mfa/email/setup | Bearer (mfa:write) | Send email OTP for enrollment |
| POST | /api/v1/users/:id/mfa/email/verify | Bearer (mfa:write) | Verify email OTP to complete enrollment |
| POST | /api/v1/users/:id/mfa/recovery-codes | Bearer (mfa:write) | Generate 10 recovery codes |
| DELETE | /api/v1/users/:id/mfa/methods/:methodId | Bearer (mfa:write) | Remove an MFA method |
| POST | /api/v1/users/:id/mfa/reset | Bearer (mfa:write) | Full MFA reset (removes all methods) |
// 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
required, the last active MFA method cannot be removed (returns 400) unless the user is marked as MFA-exempt by a super admin.Returns the full OpenID Connect discovery document with all endpoint URLs, supported scopes, algorithms, and claims.
GET /.well-known/openid-configurationReturns the RSA public key used to verify ID tokens and access tokens (RS256).
GET /.well-known/jwks.json
GET /oauth/jwksGET /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>| Parameter | Required | Description |
|---|---|---|
| response_type | Yes | Must be "code" |
| client_id | Yes | From application registration |
| redirect_uri | Yes | Must exactly match a registered redirect URI |
| scope | Yes | Space-separated: openid, profile, email |
| state | Yes | Random string for CSRF protection (echoed back) |
| code_challenge | No | PKCE challenge (SHA-256 of code_verifier, base64url-encoded). Required for public clients. |
| code_challenge_method | No | S256 (recommended) or plain |
| nonce | No | Included in the ID token if provided |
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
code_verifier parameter)client_secret in body or Authorization: Basic header{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"scope": "openid profile email"
}GET /oauth/userinfo
Authorization: Bearer <access_token>Returns user claims based on the token's scope.
The id_token is a JWT signed with RS256 containing:
| Claim | Scope | Description |
|---|---|---|
| sub | always | User ID |
| iss | always | Issuer URL (BM Guardian) |
| aud | always | Client ID |
| exp | always | Expiry timestamp |
| iat | always | Issued-at timestamp |
| nonce | if provided | Echoed nonce from authorize request |
| User's email address | ||
| email_verified | Whether email is verified | |
| name | profile | Full name ("First Last") |
| given_name | profile | First name |
| family_name | profile | Last name |
| org_roles | always | Array of { org_id, org_slug, org_name, role } for each organization membership |
| app_roles | always | Array of { app_id, app_name, client_id, roles[] } for per-app RBAC assignments |
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"]
}
]
}org_roles for authorization in your app// 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>
)}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.
off
MFA not available for org members
optional
Members can self-enroll in MFA
required
All members must enroll; enforced on next login
POST /api/auth/login.mfaPendingToken (JWT, 10min TTL) and availableMethods array instead of setting cookies.POST /api/mfa/challenge with the pending token and chosen method. For email_otp, a code is sent.POST /api/mfa/challenge/verify. On success, real httpOnly cookies are set and login is complete.| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/mfa/status | Cookie | Enrollment status + org policy |
| POST | /api/mfa/totp/setup | Cookie | Generate TOTP secret + QR code |
| POST | /api/mfa/totp/verify | Cookie | Verify TOTP to activate enrollment |
| POST | /api/mfa/email/setup | Cookie | Send OTP to user's email |
| POST | /api/mfa/email/verify | Cookie | Verify email OTP to activate |
| POST | /api/mfa/recovery-codes | Cookie | Generate 10 recovery codes |
| DELETE | /api/mfa/methods/:id | Cookie | Remove enrolled method |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/mfa/challenge | Pending Token | Initiate MFA challenge |
| POST | /api/mfa/challenge/verify | Pending Token | Verify code to complete login |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/mfa/admin/reset/:userId | Super Admin | Reset user's MFA enrollment |
| PATCH | /api/mfa/admin/exempt/:userId | Super Admin | Toggle MFA exemption |
Institution admins can configure SSO providers per organization. Navigate to SSO Providers (/providers) in the admin sidebar.
Client ID + Client Secret
Microsoft
Client ID + Client Secret (Entra ID)
Client ID + Client Secret
Meta
Client ID + Client Secret
OIDC
Client ID + Secret + Endpoint URLs
SAML
Entry Point + X.509 Certificate
Security
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/auth/register | None | Register new user |
| POST | /api/auth/login | None | Login (sets httpOnly cookies) |
| POST | /api/auth/refresh | Cookie | Refresh access token |
| POST | /api/auth/logout | Cookie | Clear auth cookies |
| GET | /api/auth/me | Cookie | Get current user + memberships |
| POST | /api/auth/forgot-password | None | Request password reset email |
| POST | /api/auth/reset-password | None | Reset password with token from email |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/organizations | Authenticated | List organizations (?parentId= filter) |
| POST | /api/organizations | Authenticated | Create org (accepts parentId for hierarchy) |
| GET | /api/organizations/:id | Authenticated | Get org details + members + children |
| PATCH | /api/organizations/:id | Authenticated | Update organization (incl. parentId) |
| DELETE | /api/organizations/:id | Authenticated | Delete organization (cascades children) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/users | Super Admin | List all users with org memberships |
| GET | /api/users/:id | Super Admin | Get user details |
| PATCH | /api/users/:id | Super Admin | Update user (name, active status) |
| PATCH | /api/users/:id/suspend | Super Admin | Suspend user (terminates sessions) |
| PATCH | /api/users/:id/activate | Super Admin | Reactivate suspended/deleted user |
| DELETE | /api/users/:id | Super Admin | Soft-delete user (preserves data) |
| GET | /api/users/:id/export | Super Admin | GDPR data export (JSON) |
| DELETE | /api/users/:id/erase | Super Admin | Right to Erasure — anonymize PII (GDPR Art. 17) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/applications/organizations/:orgId | Authenticated | List org applications |
| POST | /api/applications/organizations/:orgId | Authenticated | Create app (returns credentials) |
| PATCH | /api/applications/organizations/:orgId/:id | Authenticated | Update application |
| DELETE | /api/applications/organizations/:orgId/:id | Authenticated | Delete application |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/providers/organizations/:orgId | Authenticated | List org SSO providers |
| POST | /api/providers/organizations/:orgId | Authenticated | Create SSO provider |
| GET | /api/providers/organizations/:orgId/:id | Authenticated | Get provider details |
| PATCH | /api/providers/organizations/:orgId/:id | Authenticated | Update provider |
| DELETE | /api/providers/organizations/:orgId/:id | Authenticated | Delete provider |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/transfers | Org Admin | Create transfer request |
| GET | /api/transfers | Authenticated | List transfers (?orgId, ?status) |
| GET | /api/transfers/:id | Authenticated | Get transfer details |
| PATCH | /api/transfers/:id/approve | Org Admin | Approve and execute transfer |
| PATCH | /api/transfers/:id/reject | Org Admin | Reject transfer request |
| PATCH | /api/transfers/:id/cancel | Requester | Cancel pending transfer |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/app-roles/:appId/roles | Authenticated | List role definitions for an app |
| POST | /api/app-roles/:appId/roles | Org Admin | Create a role definition |
| PATCH | /api/app-roles/:appId/roles/:roleId | Org Admin | Update a role definition |
| DELETE | /api/app-roles/:appId/roles/:roleId | Org Admin | Delete role (cascades assignments) |
| GET | /api/app-roles/:appId/users | Authenticated | List user-role assignments |
| POST | /api/app-roles/:appId/users | Org Admin | Assign a role to a user |
| DELETE | /api/app-roles/:appId/users/:assignmentId | Org Admin | Remove a user-role assignment |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/user-attributes/users/:userId | Org Admin | Get all attributes for a user |
| GET | /api/user-attributes/users/:userId/organizations/:orgId | Org Admin | Get org-scoped attributes |
| PUT | /api/user-attributes/users/:userId/organizations/:orgId | Org Admin | Batch upsert org-scoped attributes |
| DELETE | /api/user-attributes/users/:userId/organizations/:orgId/:key | Org Admin | Delete a single attribute |
| PUT | /api/user-attributes/users/:userId/global | Super Admin | Set global attributes |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/stats | Super Admin | Dashboard statistics |
| GET | /api/settings | Super Admin | Get platform settings |
| PATCH | /api/settings | Super Admin | Update platform settings |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /oauth/authorize | Cookie | Authorization endpoint |
| POST | /oauth/authorize | Cookie | Process consent (approve/deny) |
| POST | /oauth/token | None / Basic | Token exchange (auth_code + client_credentials) |
| GET | /oauth/userinfo | Bearer | Get user claims from access token |
| GET | /oauth/jwks | None | RSA public key (JWK format) |
| GET | /oauth/client-info | None | Public app metadata for consent UI |
| GET | /.well-known/openid-configuration | None | OIDC discovery document |
| GET | /.well-known/jwks.json | None | JWKS (standard path) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/mfa/status | Cookie | MFA enrollment status + org policy |
| POST | /api/mfa/totp/setup | Cookie | Generate TOTP secret + QR code |
| POST | /api/mfa/totp/verify | Cookie | Verify TOTP to activate enrollment |
| POST | /api/mfa/email/setup | Cookie | Send OTP to user's email |
| POST | /api/mfa/email/verify | Cookie | Verify email OTP to activate |
| POST | /api/mfa/recovery-codes | Cookie | Generate 10 recovery codes |
| DELETE | /api/mfa/methods/:id | Cookie | Remove enrolled method |
| POST | /api/mfa/challenge | Pending Token | Initiate MFA challenge |
| POST | /api/mfa/challenge/verify | Pending Token | Verify code to complete login |
| POST | /api/mfa/admin/reset/:userId | Super Admin | Reset user's MFA |
| PATCH | /api/mfa/admin/exempt/:userId | Super Admin | Toggle MFA exemption |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/v1/users | Bearer (app) | Create user in app's org |
| GET | /api/v1/users | Bearer (app) | List users in app's org |
| GET | /api/v1/users/:id | Bearer (app) | Get user by ID |
| PATCH | /api/v1/users/:id | Bearer (app) | Update user |
| POST | /api/v1/users/:id/suspend | Bearer (app) | Suspend user |
| POST | /api/v1/users/:id/activate | Bearer (app) | Activate user |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /api/v1/auth/verify | Bearer (auth:verify) | Verify user credentials |
| POST | /api/v1/auth/mfa/challenge | Bearer (auth:verify) | Initiate MFA challenge |
| POST | /api/v1/auth/mfa/verify | Bearer (auth:verify) | Verify MFA code |
| POST | /api/v1/auth/forgot-password | Bearer (auth:write) | Send password reset email |
| POST | /api/v1/auth/reset-password | Bearer (auth:write) | Reset password with token |
| POST | /api/v1/auth/change-password | Bearer (auth:write) | Change password |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/v1/users/:id/mfa/status | Bearer (mfa:read) | MFA enrollment status + org policy |
| POST | /api/v1/users/:id/mfa/totp/setup | Bearer (mfa:write) | Generate TOTP QR code |
| POST | /api/v1/users/:id/mfa/totp/verify | Bearer (mfa:write) | Verify TOTP enrollment |
| POST | /api/v1/users/:id/mfa/email/setup | Bearer (mfa:write) | Send email OTP for enrollment |
| POST | /api/v1/users/:id/mfa/email/verify | Bearer (mfa:write) | Verify email OTP enrollment |
| POST | /api/v1/users/:id/mfa/recovery-codes | Bearer (mfa:write) | Generate recovery codes |
| DELETE | /api/v1/users/:id/mfa/methods/:methodId | Bearer (mfa:write) | Remove MFA method |
| POST | /api/v1/users/:id/mfa/reset | Bearer (mfa:write) | Full MFA reset |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/audit-logs | Super Admin | List audit logs (?userId, ?actorId, ?action, ?limit, ?offset) |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/provisioning/connectors | Super Admin | List provisioning connectors |
| POST | /api/provisioning/connectors | Super Admin | Create connector (Google Workspace, Entra, SCIM) |
| PATCH | /api/provisioning/connectors/:id | Super Admin | Update connector |
| DELETE | /api/provisioning/connectors/:id | Super Admin | Delete connector |
| POST | /api/provisioning/connectors/:id/test | Super Admin | Test connector connection |
| POST | /api/provisioning/connectors/:id/provision/:userId | Super Admin | Provision single user |
| GET | /api/provisioning/events | Super Admin | List provisioning events |
| POST | /api/provisioning/events/:id/retry | Super Admin | Retry failed event |
| GET | /api/provisioning/sync-status | Super Admin | Per-user sync state dashboard |
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.
http://localhost:3001/swaggerhttp://localhost:3001/swagger/jsonNote
http://localhost:3000/swagger.BM Guardian maintains a comprehensive audit trail for accountability (GDPR/DPDP compliance) and supports outbound user provisioning to external identity systems.
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.
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
BM Guardian includes built-in support for GDPR (EU) and DPDP (India) data subject rights, including data portability and the right to erasure.
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.
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.
Create a .env file in the server/ directory:
# 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=900000For external client apps connecting to BM Guardian:
NEXT_PUBLIC_BM_BASE_URL=http://localhost:3001
CLIENT_ID=your_client_id
CLIENT_SECRET=your_client_secret