composer/composer · error · TransportException
Invalid credentials for '{url}', aborting.
Error message
Invalid credentials for '{url}', aborting. What it means
Thrown as a TransportException during GitLab authentication when the origin already has stored credentials whose password field is one of the legacy token types ('gitlab-ci-token', 'private-token', or 'oauth2'). Composer treats these as pre-validated tokens, so if the server still rejects the request (401/403) the credentials are stale/revoked and retrying the same value is pointless, so it aborts rather than loop. The {url} is URL-sanitized to avoid leaking any embedded credentials.
Source
Thrown at src/Composer/Util/AuthHelper.php:162
$message .= 'create a GitHub OAuth token to access private repos';
}
}
}
if (!$gitHubUtil->authorizeOAuth($origin)
&& (!$this->io->isInteractive() || !$gitHubUtil->authorizeOAuthInteractively($origin, $message))
) {
throw new TransportException('Could not authenticate against '.$origin, 401);
}
} elseif (in_array($origin, $this->config->get('gitlab-domains'), true)) {
$message = "\n".'Could not fetch '.Url::sanitize($url).', enter your ' . $origin . ' credentials ' .($statusCode === 401 ? 'to access private repos' : 'to go over the API rate limit');
$gitLabUtil = new GitLab($this->io, $this->config, null);
$auth = null;
if ($this->io->hasAuthentication($origin)) {
$auth = $this->io->getAuthentication($origin);
if (in_array($auth['password'], ['gitlab-ci-token', 'private-token', 'oauth2'], true)) {
throw new TransportException("Invalid credentials for '" . Url::sanitize($url) . "', aborting.", $statusCode);
}
}
if (!$gitLabUtil->authorizeOAuth($origin)
&& (!$this->io->isInteractive() || !$gitLabUtil->authorizeOAuthInteractively(parse_url($url, PHP_URL_SCHEME), $origin, $message))
) {
throw new TransportException('Could not authenticate against '.$origin, 401);
}
if ($auth !== null && $this->io->hasAuthentication($origin)) {
if ($auth === $this->io->getAuthentication($origin)) {
throw new TransportException("Invalid credentials for '" . Url::sanitize($url) . "', aborting.", $statusCode);
}
}
} elseif ($origin === 'bitbucket.org' || $origin === 'api.bitbucket.org') {
$askForOAuthToken = true;
$origin = 'bitbucket.org';
if ($this->io->hasAuthentication($origin)) {View on GitHub (pinned to 6ffc117740)
Solutions
- Regenerate a GitLab personal access token with at least read_api + read_repository scopes and re-run `composer config --global --auth http-basic.gitlab.example.com <user> <token>` (or set the oauth2/private-token entry correctly).
- Clear the stale entry: `composer config --global --unset http-basic.<gitlab-host>` (and check auth.json) then let Composer prompt interactively.
- If using a CI job token, ensure the pipeline actually provides a valid CI_JOB_TOKEN and that 'gitlab-ci-token' is intended for this origin; do not reuse job tokens across jobs/runs.
- Verify the host is correctly listed in `gitlab-domains` and that the token was issued by that exact GitLab instance.
Example fix
// before (stale token in auth.json)
// "http-basic": { "gitlab.example.com": { "username": "oauth2", "password": "private-token" } }
// after - remove and re-auth
// composer config --global --unset http-basic.gitlab.example.com
// composer config --global gitlab-domains.gitlab.example.com token <NEW_PAT> Defensive patterns
Strategy: try-catch
Validate before calling
// Before downloading, confirm GitLab token validity and type
$auth = $io->getAuthentication('gitlab.example.com');
if ($auth !== null && in_array($auth['password'], ['gitlab-ci-token','private-token','oauth2'], true)) {
// Optionally ping /api/v4/user to confirm the token still works
// (do this in your own code; Composer itself trusts stored tokens)
} Try / catch
try {
$downloader->get('https://gitlab.example.com/...');
} catch (\Composer\Downloader\TransportException $e) {
if (str_contains($e->getMessage(), 'Invalid credentials for')) {
// stale GitLab token: clear and surface a re-auth prompt
$config->getAuthConfigSource()->removeConfigSetting('http-basic.gitlab.example.com');
}
throw $e;
} Prevention
- Store GitLab tokens via composer config rather than hard-coding so they can be rotated centrally.
- Document token scopes (read_api, read_repository) required by your private repos in project README.
- In CI, fail fast by validating GITLAB_TOKEN with a curl /api/v4/user call before composer install.
When it happens
Trigger: Called from AuthHelper::promptAuthIfNeeded() for a host in config 'gitlab-domains', after the IO already has authentication for that origin, and $auth['password'] is 'gitlab-ci-token' | 'private-token' | 'oauth2'. Reached when GitLab returns 401/403 and the interactive/automatic OAuth flow was not triggered because a token-type credential is already configured.
Common situations: A GitLab personal access token or CI job token stored in auth.json or COMPOSER_AUTH that has since been revoked, expired, or lacks the read_api scope; rotating to a new GitLab instance hostname while keeping old credentials; using a CI token outside its job; domain added to gitlab-domains but token belongs to a different account.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Invalid credentials (HTTP {statusCode}) for '{url}', abortin
- Invalid GitLab credentials 5 times in a row, aborting.
- GitLab API seems to not be authenticated as it did not retur
- Repository ${url} could not be processed, ${message}
- No GitLab refresh token present for ${originUrl}.
AI-assisted analysis of composer/composer@6ffc117740 (2026-08-07).
Data as JSON: /api/errors/59d18a4aad0d6c98.
Report an issue: GitHub.