Azure DevOps
Proxy for the Azure DevOps Services REST API via the managed gateway.
What this connector does
Gives agents gated access to an Azure DevOps organization for:
- Listing projects.
- Listing Git repositories in a project.
- Querying and reading work items.
All operations are gated by PBAC policy. Every call flows: agent → gateway → /introspect (with this connector's resource_type) → OPA decision → Azure DevOps API → response.
The connector supports two upstream-auth modes:
-
entra_refresh(default) — PolicyArc uses the refresh token captured at the user's Microsoft sign-in to mint a fresh, correctly-audienced token per Microsoft service at call time. One sign-in works across any combination of Microsoft connectors — Graph (Mail, OneDrive, Teams) and Azure DevOps together. The minted token is cached per(user, resource)and re-minted on expiry. Azure DevOps enforces its own organization membership and permissions, and its audit trail shows the real person making each call. -
idp_passthrough— forwards the user's Microsoft sign-in token directly to Azure DevOps, with no exchange. That sign-in token carries a single audience, so passthrough works for only one Microsoft resource — either Azure DevOps or Graph (Mail/OneDrive/Teams), not both; don't mix passthrough across Azure DevOps and Graph. Choose this only for strict tenants that disable refresh-token issuance (nooffline_access), or that don't want a long-lived refresh token warehoused by PolicyArc. Otherwise preferentra_refresh.
Prerequisites
entra_refresh mode (default)
- Entra app registration — a confidential app used for login. It needs:
- a client secret (stored in PolicyArc's secret store),
- the federation redirect URI (
/oauth2/callback), - a delegated Azure DevOps API permission
(resource
499b84ac-1321-427f-aa17-267ca6975798,user_impersonation), with admin consent granted, offline_accessin the login scopes so a refresh token is captured.
- PolicyArc Entra IdP registered with provider key
microsoft, configured with the app'sclient_id/client_secretand the tenant authorize/token endpoints (https://login.microsoftonline.com/<tenant>/oauth2/v2.0/{authorize,token}). Login scopes must includeoffline_accessso the gateway captures a refresh token at sign-in. See the Entra ID IdP guide. - Azure DevOps organization connected to the same Entra tenant (Org settings → Microsoft Entra), and the user must be a member of the org with permission for the operations you want to gate.
PBAC_CREDENTIAL_KEY(or the legacyPBAC_GATEWAY_CREDENTIAL_KEY) set on the AS (encrypts the cached tokens).
idp_passthrough mode (no token exchange)
Passthrough forwards the user's sign-in token directly, so there is no token exchange:
- Entra app registration — a confidential app used only as a login IdP.
It needs a client secret, the
/oauth2/callbackredirect URI, and the delegated Azure DevOps permission (resource499b84ac-1321-427f-aa17-267ca6975798,user_impersonation) with admin consent granted. - PolicyArc Entra IdP (provider key
microsoft) — login scopes must beopenid profile email 499b84ac-1321-427f-aa17-267ca6975798/user_impersonation, so the captured token is audienced for Azure DevOps.offline_accessis optional in passthrough mode — add it only to have the forwarded token refreshed silently on expiry; omit it for tenants that disable refresh-token issuance (users re-authenticate when the token lapses). Installing the connector auto-mergesuser_impersonationinto the IdP scope list (viaIdpScopeMerger); do not also add…/.default— Entra rejects.defaultcombined with a resource-specific scope at sign-in (AADSTS70011). - Azure DevOps organization connected to the same Entra tenant and the
user a member of the org (same as
entra_refresh). PBAC_CREDENTIAL_KEY(or the legacyPBAC_GATEWAY_CREDENTIAL_KEY) set on the AS (encrypts the captured login token).
Install
The install wizard has an Authentication picker (entra_refresh / idp_passthrough).
The default is entra_refresh; choose idp_passthrough only if your deployment uses
a single Azure-DevOps-audienced token for every connector and you have no Graph
connectors configured.
1. Register the Entra IdP
Register the Entra IdP in PolicyArc (provider key microsoft) using the app's
client_id/client_secret and tenant endpoints as described in
Prerequisites. Confirm a user can log in to PolicyArc through
it before installing the connector.
2. Install the connector
From the Admin Portal:
- APIs & Connectors → + Connect a resource → Azure DevOps
- Fill in the wizard. It has no IdP field. The connector uses the IdP whose
provider key is
microsoft, the one registered in step 1.- Authentication — select
entra_refresh(default) oridp_passthrough. - Azure DevOps organization URL —
https://dev.azure.com/<your-org>.
- Authentication — select
- Select Connect.
The admin UI reads setup_fields directly from this connector's manifest — same
fields shown below in the Setup fields reference.
3. Verify
Log in to PolicyArc through the Entra IdP first (so the gateway has a captured refresh token), then request a token with an Azure DevOps scope and call through the gateway:
curl "https://<your-as>/gateway/azure-devops/_apis/projects?api-version=7.1" \
-H "Authorization: Bearer <token>"
A successful 200 with the project list confirms the full chain end-to-end:
entra_refresh— refresh token captured → per-resource token minted → introspect allowed → gateway proxied → Azure DevOps responded.idp_passthrough— no exchange: the captured Azure-DevOps-audienced login token is forwarded verbatim → introspect allowed → gateway proxied → Azure DevOps responded. (If the captured token is audienced for anything else, ADO returns401; re-check the IdP login scopes per Prerequisites.)
What can go wrong
| Symptom | Cause | Fix |
|---|---|---|
401 with "error": "idp_link_required" | The user has no stored microsoft token, or in idp_passthrough mode the stored token lacks a scope the connector now requires | Open the link_url from the response body and sign in through the Entra IdP, so the gateway can capture a refresh token. |
502 "Upstream auth failed: token resolution requires an authenticated subject" | The token is a client_credentials token, which carries no user | Use a user token from an authorization-code login. |
502 "admin consent / API permission missing" | The Entra app lacks admin-consented delegated Azure DevOps permission | Grant admin consent for the user_impersonation permission on resource 499b84ac-… in the app registration. |
401 with "error": "idp_link_required" after working before | The refresh token is expired or revoked. If no link can be built the gateway answers 502 with "refresh token expired or revoked; user must re-authenticate" instead | Open the link_url and sign in through the Entra IdP; ensure offline_access is in the login scopes. |
401/203 from Azure DevOps | The org is not connected to the same Entra tenant, or the user isn't a member | Connect the org to the tenant (Org settings → Microsoft Entra) and add the user. |
404 at the gateway | Wrong path | Gateway path is /gateway/<resource-name>/<ADO REST path> — ADO paths start with /_apis/... or /{project}/_apis/.... |
| ADO returns an HTML/redirect page instead of JSON | Missing api-version query parameter | Append ?api-version=7.1 (or the version your API requires) to ADO REST calls. |
403 from the gateway | Agent lacks the required scope | Request a token with azuredevops:read. |
Scopes
| Scope | Meaning | Routes |
|---|---|---|
azuredevops:read | Read-only operations | list projects, list repositories, query/get work items |
Policy authors can scope a rule with input.context.rs_context.resource_type == "urn:connector:identos:azure-devops". Per-project rules compare input.context.rs_context.resource_id with the bare project path value, such as Classified, not with azuredevops://.... The gateway passes the first path parameter the route's resource_id_template names (ConnectorV1Adapter.java).
Setup fields
| Field | Required | Default | Purpose |
|---|---|---|---|
base_url | yes | — | Azure DevOps organization URL, e.g. https://dev.azure.com/your-org. |
The Entra IdP (provider key microsoft) supplies the client credentials and token
endpoint. The connector finds it by that provider key; the install form has
no field for it.
MCP tools
When the managed gateway's MCP aggregator is enabled, this connector exposes the following tools to agents:
| Tool | Scope | Description |
|---|---|---|
list_projects | azuredevops:read | List projects in the organization. |
list_repositories | azuredevops:read | List Git repositories in a project. |
list_work_items | azuredevops:read | Query work items in a project via WIQL. |
get_work_item | azuredevops:read | Get a single work item by ID. |
Each tool's input schema is embedded in the manifest and forwarded verbatim to MCP clients.
Example policy
Restrict a sensitive project to members of one group, where the project and
group names are tenant-specific. The group comes from the groups claim of the
user's IdP login, which the AS captures onto input.subject.groups (the
default pbac.identity.claim-capture.allowed-claims includes groups) and
replays at every gateway call:
package pbac.connectors.identos.azure_devops
# The tenant-specific part is DATA, not a rule. Operators set it at
# data["pbac.operator.connectors.identos.azure-devops"], and the key has to be
# declared in this connector's `operator_data_schema` or ConnectorLinter will
# reject the manifest. Index it directly: object.get(data, ...) passes the whole
# `data` document, which makes this package depend on itself
# (rego_recursion_error). With no operator config `_op` is undefined and the
# rule below contributes nothing.
_op := data["pbac.operator.connectors.identos.azure-devops"]
deny contains msg if {
input.resource.type == "urn:connector:identos:azure-devops"
classified := _op.classified_project
classified != ""
input.context.rs_context.tool_arguments.project == classified
# object.get over the whole path: a client_credentials token has no
# subject at all, and must be refused rather than skipped.
not _op.classified_group in object.get(input, ["subject", "groups"], [])
msg := sprintf("azure-devops: %v is restricted to the classified group", [classified])
}
On a local instance, with classified_project set to Classified and
classified_group to classified-team, a list_work_items call for
Classified returned 403 for a user whose groups were ["sales"] and for a
client_credentials token, and reached the upstream for a user in
classified-team. An earlier version of this example read
input.context.actor.groups; actor is set only by token exchange, so on the
gateway path that rule never fired.
Five details that are easy to get wrong, each of which fails silently rather than erroring:
- The rule goes in this connector's package,
pbac.connectors.identos.azure_devops. The aggregator iteratesdata.pbac.connectors(vendor, then package) and nothing else. A rule authored underpbac.operator.*is never evaluated — no error, the deny just never fires. The operator namespace is for data. - Read tool arguments from
input.context.rs_context.tool_arguments.input.resource.<param>andinput.action.methodare not populated on the gateway path. Ten Google connector rules gated on them, never fired, and were removed in 0.43.0. - Read the user from
input.subject.input.context.actorexists only on a token-exchange token. - Entra sends group object ids, not names. The
groupsclaim holds each group's object id, and Entra sends it only when the app registration is configured to emit group claims (Microsoft Entra documentation, "Configure group claims for applications"). Setclassified_groupto the group's object id. Without the claim,input.subject.groupsis absent and the example denies every user, members included. - Declare any operator key in
operator_data_schema. This connector ships none today, so the example above needsclassified_projectandclassified_groupadded before it will lint.
Manifest reference
- ID:
identos.azure-devops - Version:
1.0.0 - Resource type:
urn:connector:identos:azure-devops - Capabilities:
mcp
Supported auth modes
| Type | Details |
|---|---|
entra_refresh | requires IdP microsoft |
idp_passthrough | requires IdP microsoft |
Setup fields
| ID | Label | Default | Secret? | Notes |
|---|---|---|---|---|
upstream_auth.type | Authentication | entra_refresh | no | Authentication mode. entra_refresh (default) mints a separate per-resource token for each Microsoft service from your one sign-in, so a single login works across any mix of Microsoft connectors — Graph (Mail/OneDrive/Teams) and Azure DevOps together. idp_passthrough instead forwards your Microsoft sign-in token as-is, and that token is valid for only one audience: passthrough therefore works for either Graph (Mail/OneDrive/Teams) or Azure DevOps, not both. Use entra_refresh if you need more than one. |
base_url | Azure DevOps organization URL | — | no | placeholder: https://dev.azure.com/your-org |
Scopes
| Scope |
|---|
azuredevops:read |
Routes
| Method | Pattern | Scope | Resource template |
|---|---|---|---|
GET | /_apis/projects | azuredevops:read | — |
GET | /{project}/_apis/git/repositories | azuredevops:read | azuredevops://{{project}}/repositories |
POST | /{project}/_apis/wit/wiql | azuredevops:read | azuredevops://{{project}}/workitems |
GET | /{project}/_apis/wit/workitems/{id} | azuredevops:read | azuredevops://{{project}}/workitems/{{id}} |
MCP tools
| Name | Scope | Description |
|---|---|---|
list_projects | azuredevops:read | List Azure DevOps projects in the organization. |
list_repositories | azuredevops:read | List Git repositories in a project. |
list_work_items | azuredevops:read | Query work items in a project via WIQL (Work Item Query Language). |
get_work_item | azuredevops:read | Get a single work item by ID. |
Send feedback about this page