Partner Co Azure & M365 Architecture
identity model · bot infrastructure · client onboarding
Partner Co uses two separate Azure app registrations for two completely different jobs. One gives you Graph API access to act on behalf of client employees. The other is just an identity for Teams bots to receive messages. This page explains how they work, what's shared across clients, and what you create per client.
Client employee logs into app.partner.example.com → OAuth consent →
you get a refresh token → your platform can read/send their email, manage calendar,
access OneDrive on their behalf.
- Multi-tenant (any company's users)
- Permissions:
Mail.ReadWrite,Calendars.ReadWrite, etc. - Admin consent required (powerful permissions)
- One app for ALL clients
User @mentions the bot in Teams → Microsoft signs a JWT → POSTs to your webhook → your code validates the JWT and processes the message. No Graph API involved.
- Multi-tenant app registration (consented per tenant)
- Permissions: NONE (zero Graph permissions)
- Admin consent: maybe not needed
- One app per bot identity
| Partner Co App | Bot App | |
|---|---|---|
| Calls Graph API? | Yes — email, calendar, files | No |
| API permissions? | Many (Mail.ReadWrite, etc.) | None |
| If secret leaks? | Attacker can read client email | Attacker can impersonate bot messages only |
| How many? | One for all clients | One per bot identity |
| Rotation cycle | Shared across all clients | Independent per bot |
Three Azure/Teams resources are linked by the same App ID. All three are required for a Teams bot to work.
App Registration
A client ID + secret. Just an identity — it has no idea it's a bot. Like a passport.
Bot Service Resource
The glue. Tells Microsoft: "When someone messages this bot, POST to this webhook URL."
Teams App Manifest
Name, icon, and botId. Uploaded to client's Teams Admin Center. What users see.
App Registration
Lives in Partner Co's tenant.
Master definition of the app.
"I am BotOne, here's my ID and secret."
= Your passport
Service Principal
Created in the client's tenant.
Local copy that says: "I know about this app and I've approved it."
= A visa stamp
Without a service principal, the client's tenant has no idea your app exists. With it, the tenant recognizes the app and can enforce policies on it (conditional access, audit logs, etc.).
For the Partner Co App: the service principal carries all granted permissions (Mail.ReadWrite, etc.).
For a bot app: the service principal is mostly just "I acknowledge this app exists" — since there are zero permissions.
How a Teams message reaches an AgentPlatform bot and comes back.
sequenceDiagram
participant U as Teams User
participant T as Teams Cloud
participant B as Azure Bot Service
participant C as Caddy
participant O as AgentPlatform
participant AI as Claude API
U->>T: @BotOne summarize the sales report
T->>B: Route to bot via App ID
B->>B: Sign JWT with Microsoft private keys
B->>C: POST webhook URL with signed JWT
C->>O: Reverse proxy to container port
O->>O: Validate JWT signature, aud, tid claims
O->>AI: Process with agent engine
AI-->>O: AI response
O-->>B: Reply via Bot Framework SDK
B-->>T: Deliver to conversation
T-->>U: BotOne replies in thread
tenantId — only messages from the configured client tenant are accepted.
- Partner Co App (aaaaaaaa)
- M365 tenant (partner.example.com)
- Azure subscription
- Resource group (dogood-bots)
- VPS (203.0.113.10)
- Caddy reverse proxy
- Partner Center enrollment
- Partner Co Admins group
- App Registration
- Bot Service resource
- Teams channel enable
- Teams App Manifest ZIP
~5 min Azure work
- Admin consent (Partner Co App)
- Admin consent (Bot App?)
- Teams app sideload
- AgentPlatform container
- DNS A record
- Caddyfile entry
- tenantId in config
- GDAP relationship
Each client's admin clicks up to two consent URLs. These are independent — Graph access and Teams bot access are separate concerns.
These terms mean different things in different contexts. Here's what actually matters.
| Setting | Where | What it controls |
|---|---|---|
| Multi-tenant app registration | Entra ID | The app can be consented into any tenant (consent URL creates a service principal there) |
| SingleTenant bot resource | Azure Bot Service | How the bot authenticates outbound to Bot Framework (uses Partner Co's creds). Does NOT restrict who can message it. |
tenantId in AgentPlatform config |
Your VPS | Inbound access control — JWT tid claim must match this tenant, or the message is rejected. |
Is bot admin consent actually required?
The bot app has zero API permissions. The Teams app sideload might create the service principal automatically, making the consent URL redundant.
Test plan: Skip the bot consent URL during Client B onboarding (Wednesday 3/4). Just do the sideload. If the bot responds, drop this step from the playbook. If not, have client admin click the consent link and retry.
One bot app per client vs. shared platform app?
Current approach: one App Registration + Bot Service per bot identity (BotOne, BotTwo, etc.). This gives per-client naming, isolation, and independent secret rotation.
A shared approach (one app + routing layer) would reduce Azure resources but sacrifice isolation and add complexity. Verdict: Per-bot is better at our scale (2-10 clients). Revisit at 50+.