External-write provenance¶
The fifth deep page in the trust arc. When your app writes to an external system on the end user's behalf, whose identity does that system see? Two answers with very different tradeoffs — pick per-write, not per-app.
The two identities the external system can see¶
| # | Pattern | Who the external system sees | Who survives in its audit log | When to use |
|---|---|---|---|---|
| 1 | App-attributed | Your app / workload identity | Only the app | The write is inherent to the service (backup, sync, background job). No individual user is "responsible." |
| 2 | User-delegated (OBO / impersonation) | The original end user (still) | The user, exactly as if they'd clicked the button themselves | The write reflects a specific human's intent + must survive scrutiny in the other system's audit log |
Both are legitimate. The mistake is defaulting to one. Every external write should be a conscious choice: whose action does this actually represent?
The load-bearing test¶
Answer these three questions for the write:
- If audit comes calling six months later, whose name should appear next to this row in the external system's log?
- Who does the external system's authorization model actually check — an app service principal, or an individual user's ACL entries?
-
If the user leaves the company, should this write's authority to exist leave with them?
-
All three answers point at the user → user-delegated.
- All three point at the app → app-attributed.
- Mixed → split the operation. App-attributed for the mechanical parts, user-delegated for the parts where "who authorized this" is meaningful.
Concrete rich example — Microsoft Graph → SharePoint¶
The Azure / Microsoft Graph deep page walks both paths end to end because SharePoint's permission model exposes the distinction unusually clearly: Sites.Selected (app-attributed, per-site explicit grant, least privilege) vs the delegated OBO chain (user-attributed, honors the user's ACLs on the site). It also covers the operational gotcha that grants for a managed-identity service principal cannot be assigned via the Azure portal — only via az rest / Microsoft Graph PowerShell.
Generalized per-cloud equivalents¶
Not every cloud has a "SharePoint moment." Where the equivalents map, use them; where they don't, say so — don't force a fake analog.
Azure — Microsoft Graph / SharePoint¶
Both patterns are first-class. See the deep page. Summary:
- App-attributed = client credentials +
Sites.Selectedapplication permission + explicit per-site grant. - User-delegated = OBO exchange from TA.3 → Graph delegated token → SharePoint sees the user.
GCP — service-account impersonation¶
- App-attributed with role separation —
iam.serviceAccounts.getAccessToken+roles/iam.serviceAccountTokenCreatorlets one identity (typically your workload SA) mint a short-lived token as another SA (typically one with narrower rights). Still an app-attributed write; the audit log records both the caller and the impersonated SA per Cloud Docs on impersonation — a stronger audit trail than the equivalent Azure client-credentials flow. - User-delegated (Workspace only) — domain-wide delegation lets a service account act as a Workspace end user against Google Workspace APIs (Gmail, Drive, Calendar, etc.). Requires a Workspace admin approval; the resulting call in Gmail/Drive is attributed to the user. Does not apply to GCP APIs (BigQuery, Cloud Storage, Vertex, …) — those don't have a general "act as user" model. If you need user attribution against a GCP API, use the attributed-write pattern inside your database and record the human
oidin your own audit column.
AWS — STS token exchange¶
- App-attributed with role assumption —
sts:AssumeRoleWithWebIdentity(RoleArn, RoleSessionName, WebIdentityToken)takes an OIDC token from your IdP and returns short-lived AWS credentials for the assumed role. The credentials are role-attributed — CloudTrail seesarn:aws:sts::…:assumed-role/FederatedRole/<RoleSessionName>, not the OIDC user directly. This is the AWS closest analog to Azure client-credentials for external writes. - Carrying user context through — three levers on top of the assumed role:
RoleSessionName= a per-user identifier that appears in CloudTrail (assumed-role/…/RoleSessionName).- Session tags (
sts:TagSessionon the IdP's principal) — key/value pairs the IdP asserts, which land as request context for ABAC + attribute-based policies. sts:SourceIdentitycondition key — force the caller to provide a source identity that CloudTrail records and that persists across role chains.- True user-delegated writes to AWS-managed services (S3, DynamoDB, …) exist only through Cognito Identity Pools with fine-grained IAM policies keyed on
${cognito-identity.amazonaws.com:sub}— a per-user resource-policy grant on the resource itself. Powerful, but AWS-specific and worth knowing you're building a custom identity plane.
Concretely: AWS does not have a general "app impersonates user against arbitrary service" story like Azure OBO. For non-Cognito setups, treat AWS writes as app-attributed with per-user CloudTrail via RoleSessionName/SourceIdentity, and keep the user attribution in your own DB via the audit-column pattern.
Decision matrix¶
| Situation | Choose |
|---|---|
| Nightly backup to blob storage; user identity irrelevant. | App-attributed. |
| User clicks "Publish to SharePoint" and their permissions on that site determine whether it works. | User-delegated (OBO). |
| Agent generates a document to file in SharePoint on behalf of an approved user; site ACLs govern who can read after. | User-delegated (OBO) so the file is owned by the user, or app-attributed with Sites.Selected + an audit-column row in your DB carrying actor_oid = agent, approver_oid = user. Both are defensible; pick per-workflow. |
| Cloud Run job writes a per-user file to Drive (Google Workspace). | Domain-wide delegation as the user, if a Workspace admin will approve; otherwise write to Cloud Storage under an app-attributed SA and record the user in your own DB. |
| Lambda writes an object to S3 in response to an API call from a signed-in end user. | App-attributed with the Lambda's IRSA role; carry the user's IdP sub into RoleSessionName for CloudTrail + into your DB actor_oid column. |
| Middle-tier worker calls a downstream company API on the user's behalf. | Whichever the downstream API accepts. If it takes a user token, OBO. If it only takes app credentials, app-attributed + audit-column in your DB. |
What every path shares — the DB audit column stays¶
Regardless of which external-write pattern you pick, the row in your own database that records the outbound write MUST carry the human's oid/sub via the attributed-write pattern. The external system's audit log is one witness; your DB's audit column is the other, and it's the one you own. Ideal setup:
- App-attributed external write → your DB row has
actor_oid = agent_workload_sa+approver_oid = <human oid>. - User-delegated external write → your DB row has
actor_oid = <human oid>directly (the user was the caller; no separate approver).
Either way, the human's oid never disappears from your side, even if the external system only recorded "the app did it."
Docs verified 2026-08-09¶
- Microsoft Graph
Sites.Selectedapplication permission: learn.microsoft.com/…/graph/permissions-reference (identifier19432fd1-cce4-40ce-9a32-37f7b301e90f; "Read and write selected SharePoint sites") - Assign app role to a managed-identity SP (CLI-only path): learn.microsoft.com/…/entra/identity/managed-identities-azure-resources/assign-app-role-managed-identity-azure-cli
- Microsoft Entra OAuth 2.0 On-Behalf-Of flow: learn.microsoft.com/…/entra/identity-platform/v2-oauth2-on-behalf-of-flow
- GCP service-account impersonation (
iam.serviceAccounts.getAccessToken): cloud.google.com/iam/docs/service-account-impersonation - GCP Workspace domain-wide delegation: cloud.google.com/identity/docs/how-to/setup-workforce-identity-federation + developers.google.com/workspace/guides/create-credentials
- AWS
sts:AssumeRoleWithWebIdentity: docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity - AWS session tags for identity federation: docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html
Where the arc goes next¶
- Azure/Graph SharePoint — the concrete example — the two paths end to end, including the MI-service-principal CLI gotcha.
- Later — cross-tenant federation for external writes (guest access, B2B guest tenants, Workspace Federated Login).