An Azure Pipelines job fails with 401 Unauthorized while restoring from an Azure Artifacts feed in the same organization. How does a pipeline authenticate to a feed, and what would you check?
answer
- build service identity, not the queueing user
- auth task before the restore
- role granted on the feed itself
- Reader restores, Contributor publishes
- project scope blocks cross-project feeds
basics
~20 sA pipeline authenticates as its build service identity, and an authentication task injects that identity's token into the package client's config. A 401 usually means the build service identity holds no role on the feed, or the job's authorization scope cannot reach a feed in another project.
solid answer
~50 sPipelines do not use a personal token for same-organization feeds. The run has a **build service identity** — `{Project} Build Service ({org})` when the job authorization scope is the project, or `Project Collection Build Service ({org})` when it is the collection — and Azure DevOps issues it a short-lived token for the run. What turns that into working package-manager credentials is an **authentication task** you add before the restore: `NuGetAuthenticate@1`, `npmAuthenticate@0` (pointed at an `.npmrc`), `PipAuthenticate@1`, `TwineAuthenticate@1`, or `MavenAuthenticate@0`, which writes into `~/.m2/settings.xml`. So for a 401 I check three things in order: is an authentication task present and does it run before the restore; does the build service identity have a role on the feed (Reader to restore, Collaborator to pull new versions through upstream, Contributor to publish); and if the feed is project-scoped in a different project, is the job's authorization scope and pipeline permission wide enough to reach it.
code
yaml · 13 linessteps:
- task: NuGetAuthenticate@1
- script: dotnet restore MySolution.sln
displayName: Restore from Azure Artifacts
- task: npmAuthenticate@0
inputs:
workingFile: web/.npmrc
- script: npm ci
workingDirectory: web
displayName: Install from Azure Artifactsgo deeper
Know that a pipeline needs an authentication task such as NuGetAuthenticate before the restore step, and that the build has its own identity rather than using yours.
Name the build service identity, explain which feed role each operation needs, and know that the auth task must run in the same job and before the client command that reads the config.
Diagnose from the symptom: distinguish 401 from 403 from a missing package, spot the Reader-versus-Collaborator upstream failure, and reason about job authorization scope for cross-project feeds.
Own the access model — which pipelines may publish under which package names, whether feeds are project- or organization-scoped, and how you avoid long-lived tokens across every repository in the organization.
## Two halves of the problem Every Azure Artifacts 401 in CI is one of two things: the client never received credentials, or it received credentials belonging to an identity that has no role on the feed. Diagnosing quickly means knowing which half you are in. ## The identity A pipeline run does not act as the person who queued it. It acts as a **build service identity**, and which one depends on the job authorization scope: - Scope **project**: `{Project Name} Build Service ({Organization})` - Scope **collection** (organization): `Project Collection Build Service ({Organization})` Azure DevOps mints a short-lived OAuth token for this identity for the duration of the run — the value exposed as `$(System.AccessToken)` when you enable it. That token is what feed access rides on for feeds inside the same organization. There is no need for a personal access token in this path, and hardcoding one in YAML is a straightforward secret-management defect. ## The credential injection The token existing is not enough: `dotnet restore`, `npm ci`, `pip install` and `mvn` each read credentials from their own config file, and none of them know anything about Azure DevOps. That is the job of an authentication task, which writes the run's credentials into the right place before your restore step runs: | Ecosystem | Task | Writes into | |---|---|---| | NuGet / .NET | `NuGetAuthenticate@1` | NuGet credential provider environment | | npm | `npmAuthenticate@0` | the `.npmrc` you name in `workingFile` | | pip | `PipAuthenticate@1` | pip index credentials for the run | | twine (publish) | `TwineAuthenticate@1` | a twine config for the named feed | | Maven | `MavenAuthenticate@0` | `~/.m2/settings.xml` server entries | The classic mistake is adding the authentication task *after* the restore, or in a different job. Credentials are established per job on the agent, so a task in job A does nothing for job B. Another is authenticating npm but pointing `workingFile` at a path where no `.npmrc` exists — the task succeeds, the install still 401s, because the registry line and the credential line have to end up in the same file the client reads. ## The permission If credentials are arriving and the 401 persists, the identity has no useful role on the feed. Feed roles are granted in **Feed settings → Permissions**, and the build service identity is a principal you add there like any user: - **Reader** — enough to restore versions already in the feed. - **Collaborator** — additionally allows saving a new version from an upstream source. A pipeline with only Reader restores fine until someone adds a brand-new dependency, and then fails on that one package, which is a confusing symptom worth recognizing. - **Contributor** — required to publish, e.g. `dotnet nuget push` or `NuGetCommand@2` with `command: push` and `publishVstsFeed`. ## Scope and cross-project access A **project-scoped** feed in project B is not automatically reachable from a pipeline in project A. Two settings interact: the pipeline's **job authorization scope** (a project-scoped token cannot represent the collection-level identity), and whether the feed grants a role to the identity that actually shows up. Organization-scoped feeds sidestep the first half but still need the role. If the same restore works from a developer laptop and fails in CI, this asymmetry — a human with broad access versus a narrowly scoped machine identity — is usually the reason. ## Reading the failure properly A few distinctions save time: - **401** means the request had no acceptable credentials — an identity or injection problem. - **403** means authenticated but not allowed — the role is missing or too low. - A **404 on a package** from a feed you can otherwise list is often not auth at all: the version genuinely is not there, and upstream either is not configured or could not save it. ## For self-hosted agents and other organizations Self-hosted agents use the same run token; nothing extra is needed for same-organization feeds. Reaching a feed in a *different* organization is the one case that does need explicit credentials — typically a service connection of the appropriate package type, referenced by the authentication task, rather than an inline token.
- The pipeline restores existing packages fine but fails only when a developer adds a brand-new dependency. What is the likely cause?The build service identity holds Reader on the feed. Reader can download what the feed already has, but saving a new version through an upstream source needs at least Collaborator. Grant Collaborator so CI can hydrate the feed, and keep Contributor for the pipelines that actually publish.
- Why is hardcoding a personal access token in the YAML the wrong fix for a feed 401?It ties builds to one person's account and lifetime, puts a long-lived credential in source control or a variable, and grants that human's full breadth of access to every run. The run already has a scoped, short-lived identity; the correct fix is granting that identity a feed role.
- What changes when the feed lives in a different Azure DevOps organization?The run's token is not valid there, so you need explicit credentials — a service connection of the right package type, referenced by the authentication task. Within one organization the build identity suffices; crossing the organization boundary is the case that genuinely requires configured credentials.
- How do you tell a permissions problem from a package that simply is not in the feed?Read the status code. A 401 means no acceptable credentials reached the feed and a 403 means the identity lacks the role, both auth problems. A 404 for one package on a feed you can otherwise list usually means that version is absent and no upstream supplied it.
saying these in an interview costs you the question
- Assuming the pipeline runs as the user who queued it
- Adding the authentication task after the restore step
- Hardcoding a personal access token in the pipeline YAML
- Granting the build identity Contributor when Reader would do
- Ignoring project scope when the feed lives in another project