composer/composer · error · Composer\Downloader\TransportException
Invalid credentials for
Error message
Invalid credentials for '{url}', aborting. What it means
Thrown by AuthHelper::promptAuthIfNeeded() as a TransportException for a GitLab origin when the stored authentication's password is one of the known-invalid token types ('gitlab-ci-token', 'private-token', 'oauth2') that GitLab no longer accepts, OR when the auth did not change after a re-prompt (credentials unchanged). It aborts rather than retrying with known-bad credentials.
Solutions
- Generate a GitLab personal access token (read_api/read_repository scope) and configure it: composer config gitlab-token.<domain> <token>.
- If using CI job tokens, ensure the job token is only used against its own project or configure CI cross-project tokens per GitLab docs.
- Remove the stale auth (composer config --unset gitlab-token.<domain>) and re-enter credentials.
- Verify the token is not expired and has sufficient scope/role for the project.
Defensive patterns
Strategy: retry
Validate before calling
// Detect known-bad GitLab token types before using them
$auth = $io->getAuthentication($origin);
if (in_array($auth['password'] ?? null, ['gitlab-ci-token','private-token','oauth2'], true)) {
throw new \RuntimeException('GitLab password type known to be rejected; use a personal access token.');
} Type guard
function gitLabTokenIsAcceptable(string $password): bool {
return !in_array($password, ['gitlab-ci-token','private-token','oauth2'], true);
} Try / catch
try {
$repo->whatProvides($pool, $name);
} catch (\Composer\Downloader\TransportException $e) {
if (str_contains($e->getMessage(), "Invalid credentials")) {
// unset stale auth and configure a fresh personal access token
}
} Prevention
- Use a GitLab personal access token (read_api/read_repository) instead of gitlab-ci-token/private-token/oauth2.
- Use CI job tokens only within their own project.
- Remove and re-enter auth when tokens expire.
When it happens
Trigger: GitLab origin; io has authentication with password in ['gitlab-ci-token','private-token','oauth2'] → throws at line 162 (code = $statusCode); also thrown at line 174 if after re-prompt the auth object is identical to the previous one.
Common situations: Using a gitlab-ci-token outside its own project; an expired 'private-token'; an 'oauth2'-style token that the GitLab instance rejects; CI job token cross-project where not allowed.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Could not authenticate against
- can not ask for authentication in non interactive mode
- No GitLab refresh token present for
- Error logging in
- Failed to clone , git was not found, check that it is…
AI-assisted analysis of composer/composer@c435d285c9 (2026-08-07).
Data as JSON: /api/errors/59d18a4aad0d6c98.
Report an issue: GitHub.
Appendix: 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 c435d285c9)