API Authentication
Cluster uses Azure AD OAuth 2.0 tokens for API authentication. The frontend handles token acquisition; the API validates tokens on each request.
Token Flow
sequenceDiagram
participant Client
participant API
participant AzureAD
Client->>AzureAD: Acquire token (MSAL)
AzureAD->>Client: Access token + ID token
Client->>API: Request + Bearer token
API->>AzureAD: Validate token
AzureAD->>API: Token valid + claims
API->>Client: Response
Request Format
Include the access token in the Authorization header:
GET /api/annotations HTTP/1.1
Host: localhost:4000
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJS...
Token Validation
The API validates:
- Signature — Token signed by Azure AD
- Issuer — Matches configured tenant
- Audience — Matches application ID
- Expiration — Token not expired
- Tenant — Matches allowed tenant(s)
Current User
Get Current User
GET /api/auth/me
Response:
{
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "jsmith@company.com",
"displayName": "Jane Smith",
"avatarUrl": "https://graph.microsoft.com/v1.0/me/photo/$value",
"organization": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Company Inc",
"azureTenantId": "12345678-1234-1234-1234-123456789012"
}
}
}
Get Current Organization
GET /api/auth/organization
Response:
{
"data": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"name": "Company Inc",
"azureTenantId": "12345678-1234-1234-1234-123456789012",
"settings": {
"defaultTaxonomyId": "770e8400-e29b-41d4-a716-446655440000"
},
"createdAt": "2025-01-01T00:00:00Z"
}
}
User Provisioning
When a user logs in for the first time:
- Organization lookup — Find or create org by Azure tenant ID
- User lookup — Find or create user by Azure user ID
- Association — Link user to organization
// Automatic provisioning in auth middleware
const org = await findOrCreateOrganization(tenantId, tenantName);
const user = await findOrCreateUser(org.id, azureUserId, email, displayName);
Multi-Tenant Support
Cluster supports multiple Azure AD tenants:
| Configuration | Description |
|---|---|
| Single-tenant | One Azure AD tenant, one Cluster organization |
| Multi-tenant | Multiple tenants, each creates separate organization |
Single-Tenant Setup
# Server environment
AZURE_TENANT_ID=12345678-1234-1234-1234-123456789012
Multi-Tenant Setup
# Server environment - use 'common' for any tenant
AZURE_TENANT_ID=common
Each tenant's users are isolated to their own organization. Data is filtered by org_id on all queries.
Organization Isolation
All API requests are scoped to the user's organization:
// Every query includes org_id filter
const annotations = await db
.select()
.from(annotations)
.where(eq(annotations.orgId, req.user.orgId));
Users cannot access data from other organizations.
Token Refresh
Access tokens expire after 1 hour. The frontend uses MSAL to silently refresh tokens:
// Frontend token refresh
const account = msalInstance.getActiveAccount();
const response = await msalInstance.acquireTokenSilent({
scopes: ['api://your-client-id/.default'],
account,
});
SharePoint Access
For SharePoint operations, the API uses the On-Behalf-Of flow:
- Frontend sends user's access token
- API exchanges token for SharePoint-scoped token
- API calls Microsoft Graph with new token
// On-behalf-of token exchange
const oboToken = await cca.acquireTokenOnBehalfOf({
oboAssertion: userToken,
scopes: ['https://graph.microsoft.com/Files.Read.All'],
});
Error Responses
401 Unauthorized
Missing or invalid token:
{
"error": {
"code": "UNAUTHORIZED",
"message": "Invalid or expired access token"
}
}
403 Forbidden
Token valid but insufficient permissions:
{
"error": {
"code": "FORBIDDEN",
"message": "You do not have access to this resource"
}
}
Security Headers
The API sets security headers on all responses:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: default-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
Testing Without Azure AD
For local development without Azure AD:
# Enable development mode (bypasses token validation)
NODE_ENV=development
DEV_USER_EMAIL=developer@localhost
DEV_USER_NAME=Developer
DEV_ORG_ID=dev-org-id
Never use development mode in production.
Next Steps
- Annotations API — Create and manage annotations
- Files API — Access SharePoint files