Architecture

Multi-Tenant Architecture

How CONFIG365 uses a single multi-tenant app registration to manage multiple Microsoft 365 tenants with centralized credentials and per-tenant admin consent.

6 min read Updated March 22, 2026

Overview

CONFIG365 uses a single multi-tenant app registration hosted in your MSP's Entra ID tenant. This one app can authenticate to any number of customer tenants — each customer simply grants admin consent once, and no per-tenant app registration is ever needed.

Single app to manage
Centralized credential management
One secret rotation updates all tenants
Reduced administrative overhead

Architecture Components

1. Single Multi-Tenant App Registration

Located in your MSP's Entra ID tenant. One Client ID and secret shared across all managed tenants.

App configuration
Name:               M365-Automation
Sign-in Audience:   AzureADMultipleOrgs (multi-tenant)
Permissions:        Microsoft Graph API (Application)

2. Per-Tenant Admin Consent

Each tenant's Global Administrator visits a consent URL once. After consent, a Service Principal is created in their tenant — your app can then authenticate to it.

Consent URL format
https://login.microsoftonline.com/organizations/adminconsent?client_id={your-app-id}

3. Credentials in CONFIG365

Credentials are managed at two levels inside CONFIG365:

MSP-level Gitea org secrets (set automatically by the portal)

Secret Value
AZURE_CLIENT_ID App Client ID (shared across all tenants)
MSP_ORG MSP Gitea org name
PLATFORM_ORG Platform (config365) Gitea org name
CONFIG365_TOKEN Gitea admin token for pipeline operations

Per-tenant credentials (stored encrypted in SQLite, resolved at runtime)

Field Value
tenantId Tenant's Entra ID tenant ID
clientId App Client ID (or per-tenant override)
slug Short name used as Gitea repo prefix (e.g. contoso)
exchangeOrgName contoso.onmicrosoft.com (optional)

The token-api service running on localhost:4322 inside the container resolves per-tenant tokens at pipeline runtime — credentials never appear in workflow YAML.

Authentication Flow

MSP Entra ID Tenant
└── Multi-Tenant App Registration (delegated)
    App ID: 12345678-...
    Allow public client flows: Yes
              │
              │  Admin consent + device-code sign-in (once per tenant)
              ▼
Tenant A (ID: 111...)        Tenant B (ID: 222...)
Delegated refresh token      Delegated refresh token
Same App ID                  Same App ID
              │
              ▼
Pipelines call token-api (localhost:4322)
  Graph      → GRAPH delegated scopes
  Exchange   → outlook.office365.com/.default  (Exchange.Manage)
  SharePoint → {tenant}.sharepoint.com/.default (AllSites.FullControl)
  MDE        → api.securitycenter.microsoft.com/.default (Machine.Read)

Security Model

Credential Protection

Do

  • Store Client Secret in the CONFIG365 portal — it encrypts it in SQLite automatically
  • Rotate secrets every 12–24 months and update via the portal
  • Use a separate app registration for dev vs. production

Don't

  • Never commit secrets to Git
  • Don't share secrets via email or chat
  • Don't grant more permissions than needed

Tenant Isolation

  • – Each tenant has its own Git repository and pipeline
  • – Each pipeline specifies the tenant's unique Tenant ID — accidental cross-tenant deployment is impossible
  • – Backups are stored per-tenant in their own repository
  • – Tenant admins can revoke consent at any time from their own Entra portal

Credential Rotation

1
Create a new secret

Entra admin center → App registrations → Your app → Certificates & secrets → New client secret (24 months)

2
Update credentials in the CONFIG365 portal

Open Settings → MSP App Registration and paste the new Client Secret — the portal re-encrypts and updates Gitea org secrets automatically

3
Test with one tenant

Trigger a deploy for a test tenant from the portal dashboard and verify successful authentication

4
All tenants update immediately

Because credentials are resolved at runtime by the token-api, every subsequent pipeline run uses the new secret without any per-tenant changes

5
Delete the old secret

After confirming the new secret works, remove the old one from app registration

Multi-Tenant vs. Per-Tenant Apps

Aspect Multi-Tenant (recommended) Per-Tenant Apps
Number of Apps 1 One per tenant
Credential Management Centralized Distributed
Secret Rotation Update once Update N times
Setup Complexity Low High
Ongoing Maintenance Low High
Tenant Onboarding Send consent URL Create new app
Scalability Excellent Poor

Tenant-Specific App Override

For special cases — GCC High, dedicated security requirements, or tenants that need their own app registration — you can override the shared credentials on a per-tenant basis. Set the following fields in the tenant's settings in the CONFIG365 portal:

Variable Value Secret
Tenant-clientId Tenant's app Client ID 🔒 Yes
Tenant-clientSecret Tenant's app Client Secret 🔒 Yes

When a per-tenant clientId and clientSecret are saved in the tenant's portal settings, the token-api automatically uses them instead of the shared MSP credentials. No workflow YAML changes are needed.

Troubleshooting

AADSTS650053: The application is asking for permissions that weren't granted

Cause: Tenant has not granted admin consent

  • – Generate consent URL and send to tenant's Global Admin
  • – Wait for consent confirmation before running the pipeline

AADSTS7000218: The request body must contain client_assertion or client_secret

Cause: Client secret is missing or incorrect

  • – Verify the Client Secret is saved in the CONFIG365 portal (Settings → MSP App Registration)
  • – Confirm the portal has successfully synced the secret to Gitea org secrets
  • – Check if the secret has expired in the Entra admin center

AADSTS90002: Tenant not found

Cause: Incorrect Tenant ID saved in the portal

  • – Verify the tenant ID in the CONFIG365 portal tenant settings
  • – Confirm the tenant has not moved tenants
  • – Check for typos

Failed to acquire token

Cause: Consent may have been revoked

  • – Check Entra ID → Enterprise Applications for your app
  • – If missing, generate a new consent URL and re-consent