Skip to content

Repository Tokens: Create One and Authenticate

View as Markdown

A repository token is the credential your build tools send to CloudRepo. Other tools call the same thing an access token or an API token. You create it in the admin portal, choose which repositories it reaches and whether it can publish, and paste it into Maven, Gradle, npm, pip, twine or Docker in place of a password.

Every repository token:

  • reaches only the repositories you tick when you create it;
  • is Read only (download) or Read + write (download, publish and delete);
  • expires when you choose: 90, 180 or 365 days after you create it, on any date up to 31 December 9999, or never (the default is 90 days);
  • is shown once, when you create it, and starts with crp_v1_;
  • can be revoked on its own, without changing your password or your other tokens.

This page uses four placeholders. your-org is your organization’s name, the first part of your repository host: for acme.mycloudrepo.io it is acme. your-repo is the repository’s name as it appears in the admin portal. you@example.com stands for your email address, and YOUR_REPOSITORY_TOKEN for the token. Never commit a real token to version control. Type your organization and repository here, and every example on this page uses them:

Fill the examples with your names

Used on this page only, never saved. Never enter a token or password here.

Client Username Password or token
Maven, Gradle, pip, twine, Poetry, uv, curl Your email address: the email of the account that created the token The repository token
npm None The repository token, as _authToken in .npmrc
Docker (docker login) Your email address (CloudRepo’s registry does not check it) The repository token

The username is your email address, not the token’s name and not a placeholder. Capital letters do not matter. For the clients that send a username (Maven, Gradle, pip, twine, Poetry, uv, curl and npm _auth), a username of __token__, token, or anything other than the email address of the account that created the token is refused with 401 Unauthorized. (__token__ is the convention on pypi.org; CloudRepo does not use it.) CloudRepo’s registry does not check the Docker username; use your email address there anyway.

  1. Sign in to the CloudRepo Admin Portal as the user whose builds will use the token, and in the sidebar, click Repository Tokens.

    Expected: the Repository Tokens page. If you have no tokens yet, it shows No repository tokens yet.

    The Repository Tokens page with no tokens yet and a Create your first token button.
  2. Click Create token, or Create your first token if you have none.

    Expected: the Create repository token form, with Name, Scope, Repositories and Expires. If it shows No repositories available instead, your organization has no repositories yet: create one first (see Creating a Repository).

    The Create repository token form: an empty Name field, Scope set to Read only, and five repositories to tick.
  3. In Name, type a name that says where the token will be used, such as ci-maven or laptop-npm.

    Expected: the field holds the name. The name is a label for the token list; it is never the username.

    The Name field holds ci-maven.
  4. Under Scope, choose Read only to download, or Read + write to download, publish and delete.

    Expected: one option is selected.

    Scope is set to Read + write.
  5. Under Repositories, tick each repository the token may reach.

    Expected: the repositories you need are ticked. This choice is permanent: to reach different repositories later, create a new token.

    maven-releases and maven-snapshots are ticked under Repositories.
  6. Under Expires, keep the default of 90 days, click 90 days, 180 days or 365 days, pick any date up to 31 December 9999, or click Never expires. A preset counts from when you click it, and the default from when the page opens. A date you pick ends at its start (00:00 UTC), so pick a date after the last day you need the token. For a token a CI system holds, choose a date and rotate the token before it (see Security Hardening).

    Expected: the line under the choices says when the token expires, or reads This token will never expire. The expiry is fixed when you create the token, and rotating the token keeps it; for a different expiry, create a new token. When a token expires, builds that use it stop authenticating.

    Expires holds 12/30/2026, above the quick presets and the line saying the token expires on December 30, 2026.
  7. Click Create token.

    Expected: a green Token created banner and the token value. If you have not confirmed your identity in the last 15 minutes, a Confirm Your Identity dialog comes first: enter your password and click Confirm. If the form says Your account is read-only and cannot create a write-enabled token., your user has read only access: choose Read only, or ask your organization owner for write access.

    The Confirm Your Identity dialog asks for the password before the token is created.
    Token created, with the token value shown once and a Copy button.
  8. Click Copy next to the token, and store it where your build tool will read it, such as a CI secret.

    Expected: the button reads Copied!. CloudRepo shows the token only this once. If you lose it, rotate the token from the Repository Tokens page to get a new value.

    The Copy button now reads Copied!

Pick your client. The choice is remembered on every page of these docs.

Put the credential in ~/.m2/settings.xml. The <id> is any name you choose; it must match the <id> of the repository in your pom.xml.

~/.m2/settings.xml
<settings>
<servers>
<server>
<id>cloudrepo</id>
<username>you@example.com</username>
<password>${env.CLOUDREPO_TOKEN}</password>
</server>
</servers>
</settings>

${env.CLOUDREPO_TOKEN} reads the token from an environment variable, which keeps it out of the file. You can paste the token there instead.

pom.xml
<repositories>
<repository>
<id>cloudrepo</id>
<url>https://your-org.mycloudrepo.io/repositories/your-repo</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>cloudrepo</id>
<url>https://your-org.mycloudrepo.io/repositories/your-repo</url>
</repository>
</distributionManagement>

Deploy once to check that Maven sends the credential:

Terminal
mvn --batch-mode deploy

More: Maven Repositories.

A refused credential answers 401 Unauthorized; a credential that may not do what was asked answers 403 Forbidden. A proxy repository answers 404 Not Found instead of 403, with one exception: it refuses an npm login token with 403 (see the npm item below).

npm is different for a bad token. npm sends _authToken as a Bearer token, and a token that is mistyped, expired, revoked or from another organization answers 404 Not Found, not 401. npm then prints 404 Not Found for the package. If npm reports 404 for a package you know exists, check the token first.

Check these in order:

  • The username. It must be the email address of the account that created the token (capital letters do not matter). Not __token__, not the token’s name, not your organization name.
  • The token. Paste it whole, starting at crp_v1_. Check on the Repository Tokens page that it has not expired or been revoked. With npm, a bad token shows as 404.
  • The repository. The token reaches only the repositories ticked when it was created. A different repository needs a token that includes it.
  • Publishing. A Read only token downloads but cannot publish; create a Read + write token for the build that publishes.
  • An npm token elsewhere. A token from npm login (crn_v1_) works only on the local npm repository it was created for, and publishes only if you chose Read-write and your user has write access. A proxy npm repository refuses it with 403. Use a token from Repository Tokens for a proxy npm repository, every other npm repository, Maven, Python and Docker.
  • The expiry. The Expires column on the Repository Tokens page shows when each token stops working, or Never expires.

A service key belongs to your organization, not to a person. It keeps working when the person who created it leaves, which makes it the credential for CI. Only the organization owner can create one. There is no screen for service keys: the owner creates and revokes them through the CloudRepo API, with the script below.

A service key expires at most 90 days after it is created, so note the date and create the next key before it.

Save the script as service-key.sh and set the five values at the top. Then run bash service-key.sh create to create a key, or bash service-key.sh revoke <key-id> to revoke one before its date. It asks for the owner’s password, and it needs Python 3 and curl 7.55 or later.

service-key.sh
#!/usr/bin/env bash
# Create or revoke an organization service key with the CloudRepo API.
# Run it with create, or with revoke and the key ID that create printed.
set -euo pipefail
API=https://api.cloudrepo.io
ORG=your-org # your organization ID
OWNER=you@example.com # the organization owner's email address
NAME=ci-build # create: where the key will be used
SCOPE=read,write # create: read to download only; read,write to publish as well
case "$API" in https://*) ;; *) echo "API must start with https://. Nothing was sent." >&2; exit 2;; esac
MODE=${1:-}; KEY_ID=${2:-}
case "$MODE" in
create) ;;
revoke) # A key ID is a lowercase UUID. Refuse anything else before any request is sent.
[[ $KEY_ID =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]] \
|| { echo "Usage: bash $0 revoke <key-id>, where <key-id> is the key ID create printed"; exit 1; } ;;
*) echo "Usage: bash $0 create | revoke <key-id>"; exit 1 ;;
esac
umask 077
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
# field NAME: one field of the last answer, or nothing when the answer is not JSON.
field() { python3 -I -c 'import json,sys
try: print(json.load(open(sys.argv[1])).get(sys.argv[2], ""))
except Exception: print("")' "$WORK/out" "$1"; }
# 1. Sign in as the owner.
IFS= read -rsp 'Owner password: ' PW; echo
S=$(printf '%s' "$PW" | ORG="$ORG" OWNER="$OWNER" python3 -I -c 'import json,os,sys; print(json.dumps({"organizationId": os.environ["ORG"], "emailAddress": os.environ["OWNER"], "passphrase": sys.stdin.read()}))' \
| curl -sS --proto =https -X POST "$API/api/authorize" -H 'Content-Type: application/json' --data-binary @- \
-o "$WORK/out" -w '%{http_code}')
[ "$S" = 200 ] || { echo "Sign-in refused (HTTP $S). $(field error)"; exit 1; }
STEP_UP=password
case "$(field mfa_action)" in
"") ;;
challenge) # This sign-in also asks for a code from the owner's authenticator app.
unset PW; STEP_UP=totp
CHALLENGE=$(field challenge_token)
read -rp 'Code from your authenticator app: ' CODE
S=$(printf '{"challenge_token": "%s", "code": "%s"}' "$CHALLENGE" "$CODE" \
| curl -sS --proto =https -X POST "$API/api/mfa/verify" -H 'Content-Type: application/json' --data-binary @- \
-o "$WORK/out" -w '%{http_code}')
[ "$S" = 200 ] || { echo "The code was refused (HTTP $S). $(field error)"; exit 1; } ;;
*) echo "Sign in to the admin portal once and finish what it asks for, then run this again."; exit 1 ;;
esac
[ -n "$(field token)" ] || { echo "The sign-in did not finish (HTTP $S). $(field error)"; exit 1; }
printf 'Authorization: Bearer %s\n' "$(field token)" > "$WORK/auth"
# post PATH: send the JSON on standard input to the API as the owner; print the HTTP status.
post() { curl -sS --proto =https -X POST "$API$1" -H @"$WORK/auth" -H 'Content-Type: application/json' \
--data-binary @- -o "$WORK/out" -w '%{http_code}'; }
if [ "$MODE" = revoke ]; then
unset PW
S=$(curl -sS --proto =https -X DELETE "$API/api/repository-tokens/$KEY_ID" -H @"$WORK/auth" \
-o "$WORK/out" -w '%{http_code}')
[ "$S" = 204 ] || { echo "The key was not revoked (HTTP $S). $(field error)"; exit 1; }
echo "Key $KEY_ID is revoked."
exit 0
fi
# 2. Confirm your identity: with the password, or, when the sign-in asked for a code, with the
# NEXT code from the app (the one you signed in with is refused a second time).
if [ "$STEP_UP" = totp ]; then
read -rp 'Next code from your authenticator app (wait for a new one): ' CODE
S=$(printf '{"method": "totp", "credential": "%s", "action": "repository-token-mint"}' "$CODE" \
| post /api/auth/confirm-identity)
else
S=$(printf '%s' "$PW" | python3 -I -c 'import json,sys; print(json.dumps({"method": "password", "credential": sys.stdin.read(), "action": "repository-token-mint"}))' \
| post /api/auth/confirm-identity)
unset PW
fi
CONFIRMATION=$(field confirmation-token)
[ "$S" = 200 ] && [ -n "$CONFIRMATION" ] || { echo "Your identity was not confirmed (HTTP $S). $(field error)"; exit 1; }
# 3. Choose the repositories the key may reach.
S=$(curl -sS --proto =https "$API/api/repositories" -H @"$WORK/auth" -o "$WORK/out" -w '%{http_code}')
[ "$S" = 200 ] || { echo "Could not list repositories (HTTP $S). $(field error)"; exit 1; }
python3 -I -c 'import json,sys; [print(r["repositoryEntityId"], r["repositoryId"]) for r in json.load(open(sys.argv[1]))]' "$WORK/out"
read -rp 'IDs from the first column, separated by spaces: ' IDS
# 4. Create the key. A service key expires at most 90 days out; this one expires in 89.
EXPIRES=$(( ( $(date +%s) + 89 * 86400 ) * 1000 ))
S=$(printf '%s' "$CONFIRMATION" | NAME="$NAME" SCOPE="$SCOPE" IDS="$IDS" EXPIRES="$EXPIRES" python3 -I -c 'import json,os,sys; print(json.dumps({"name": os.environ["NAME"], "format": "generic", "repo-ids": os.environ["IDS"].split(), "scope": os.environ["SCOPE"].split(","), "expires-at": int(os.environ["EXPIRES"]), "backing": {"kind": "org"}, "confirmation-token": sys.stdin.read()}))' \
| post /api/repository-tokens)
[ "$S" = 201 ] && [ -n "$(field token-value)" ] || { echo "The key was not created (HTTP $S). $(field error)"; exit 1; }
echo "Key ID, to revoke it: $(field token-id)"
echo "Key, shown once: $(field token-value)"

The script prints a key that starts with crk_. Store it as a secret in your CI, and use it wherever this page puts a repository token. A service key is not checked against a username, so the email address rule above does not apply to it: where a build tool also asks for a username, use token.

Keep the key ID the script prints. It is how you revoke the key with bash service-key.sh revoke <key-id>.