# Repository Tokens: Create One and Authenticate

> Create a CloudRepo repository token (access token) and authenticate Maven, Gradle, npm, pip, twine and docker login with it. The username is your email address.

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:

## The Credential Every Client Sends

| 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.

## Create a Token in the Admin Portal

1. Sign in to the [CloudRepo Admin Portal](https://admin.cloudrepo.io) 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.](/docs/_astro/shots/repository-tokens/1-tokens-page.f7f0fbca67e2b7ff.png)

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](/docs/manage/repositories.html#creating-a-repository)).

   ![The Create repository token form: an empty Name field, Scope set to Read only, and five repositories to tick.](/docs/_astro/shots/repository-tokens/2-create-form.cb493fcab4faae58.png)

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.](/docs/_astro/shots/repository-tokens/3-name.ce10ed6ee9585d52.png)

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.](/docs/_astro/shots/repository-tokens/4-scope.499c595566ab6e7d.png)

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.](/docs/_astro/shots/repository-tokens/5-repositories.a120961c74f584fd.png)

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](/docs/migration-advanced/security-hardening.html)).

   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.](/docs/_astro/shots/repository-tokens/6-expiry.7a6121359e529d76.png)

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.](/docs/_astro/shots/repository-tokens/7a-confirm-identity.29fa18dee4b5114f.png)

   ![Token created, with the token value shown once and a Copy button.](/docs/_astro/shots/repository-tokens/7b-token-created.c73b91ac55b551e0.png)

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!](/docs/_astro/shots/repository-tokens/8-copied.c80a5d9d575173a3.png)

## Use the Token in Your Client

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

**Maven**

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`

```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`

```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**

```bash
mvn --batch-mode deploy
```

More: [Maven Repositories](/docs/formats/maven.html).

**Gradle**

Keep the credential in `~/.gradle/gradle.properties`, outside your project:

`~/.gradle/gradle.properties`

```properties
cloudrepoUsername=you@example.com
cloudrepoToken=YOUR_REPOSITORY_TOKEN
```

Then read it in the build script.

`build.gradle.kts`

```kotlin
repositories {
    maven {
        url = uri("https://your-org.mycloudrepo.io/repositories/your-repo")
        credentials {
            username = providers.gradleProperty("cloudrepoUsername").get()
            password = providers.gradleProperty("cloudrepoToken").get()
        }
    }
}
```

`build.gradle`

```groovy
repositories {
    maven {
        url = "https://your-org.mycloudrepo.io/repositories/your-repo"
        credentials {
            username = findProperty("cloudrepoUsername")
            password = findProperty("cloudrepoToken")
        }
    }
}
```

To publish, add a `publishing` block that sends your Java library to the same repository, and run `gradle publish`:

`build.gradle.kts`

```kotlin
publishing {
    publications {
        create<MavenPublication>("library") {
            from(components["java"])
        }
    }
    repositories {
        maven {
            url = uri("https://your-org.mycloudrepo.io/repositories/your-repo")
            credentials {
                username = providers.gradleProperty("cloudrepoUsername").get()
                password = providers.gradleProperty("cloudrepoToken").get()
            }
        }
    }
}
```

**Terminal**

```bash
gradle publish
```

A Groovy build script takes the same `maven { ... }` block under `publishing { repositories { ... } }`. More: [Gradle Repositories](/docs/repository-types/jvm/gradle-repositories.html).

**npm**

npm sends the token on its own, with no username. Put it in the project’s `.npmrc`, or in `~/.npmrc`:

`.npmrc`

```ini
registry=https://your-org.mycloudrepo.io/repositories/your-repo/
//your-org.mycloudrepo.io/repositories/your-repo/:_authToken=${CLOUDREPO_TOKEN}
```

npm replaces `${CLOUDREPO_TOKEN}` with the environment variable of that name, so the file can be committed without the token in it. Keep the trailing `/` on both lines, and keep the `//your-org...` line identical to the registry URL without `https:`.

Publish a package to check that npm sends the token:

**Terminal**

```bash
npm publish
```

- `npm login` also works, for a local npm repository: it opens the admin portal in your browser and writes a token to `.npmrc` for you. That token starts with `crn_v1_` and works only on the local npm repository you ran `npm login` against. A proxy npm repository refuses it with `403` (`npm_token_not_supported_on_proxy`), even though `npm login` against the proxy succeeds; use a token from **Repository Tokens** there.
- An `npm login` token is read-only unless you choose **Read-write** when you authorize it and your user has write access, so `npm publish` answers `403` with a read-only one. A token created under **Repository Tokens** works on every npm repository it reaches, local or proxy.
- `npm token create` is not supported: create tokens in the admin portal.

More: [npm Repositories](/docs/formats/npm.html).

**pip**

To upload with twine, use the repository URL without `/simple/`. twine’s username is a plain field, so the `@` in your email address needs no encoding:

`~/.pypirc`

```ini
[distutils]
index-servers = cloudrepo


[cloudrepo]
repository = https://your-org.mycloudrepo.io/repositories/your-repo
username = you@example.com
password = YOUR_REPOSITORY_TOKEN
```

Build your package, then upload what you built:

**Terminal**

```bash
pip wheel . --no-deps -w dist
twine upload --repository cloudrepo dist/*
```

In CI, skip the file and set the environment instead:

**Terminal**

```bash
export TWINE_USERNAME="you@example.com"
export TWINE_PASSWORD="$CLOUDREPO_TOKEN"
twine upload --repository-url https://your-org.mycloudrepo.io/repositories/your-repo dist/*
```

pip takes the credential inside the index URL. Write the `@` in your email address as `%40`. On Windows the file is `pip.ini`:

`~/.config/pip/pip.conf`

```ini
[global]
index-url = https://you%40example.com:YOUR_REPOSITORY_TOKEN@your-org.mycloudrepo.io/repositories/your-repo/simple/
```

The same URL works for one command or one shell session, through the `PIP_INDEX_URL` environment variable. In CI, set `PIP_INDEX_URL` from a secret instead of writing a file:

**Terminal**

```bash
export PIP_INDEX_URL="https://you%40example.com:${CLOUDREPO_TOKEN}@your-org.mycloudrepo.io/repositories/your-repo/simple/"
pip install my-package
```

More, including keeping the token in your system keyring: [Basic Configuration Examples](/docs/quick-start/basic-configuration.html).

The username is your email address, not `__token__`. More: [Python Repositories](/docs/formats/python.html), and for Poetry and uv the pages under [Python](/docs/formats/python.html).

**Docker**

Log in to the host alone, with your email address and the token:

**Terminal**

```bash
echo "$CLOUDREPO_TOKEN" | docker login your-org.mycloudrepo.io \
  --username you@example.com \
  --password-stdin
```

Expected: `Login Succeeded`. Then build and push an image, with the repository in the image reference:

**Terminal**

```bash
docker build -t your-org.mycloudrepo.io/repositories/your-repo/my-app:1.0.0 .
docker push your-org.mycloudrepo.io/repositories/your-repo/my-app:1.0.0
```

The password is the token, not the password you sign in to the admin portal with. Use a token: it reaches only the repositories you tick, can be read only, can carry an expiry, and can be revoked without changing anyone’s password. More: [Docker Repositories](/docs/formats/docker.html).

## When a Client Answers 401, 403 or 404

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**.

## Service Keys for Your Pipelines

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`

```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>`.

---

The page: https://www.cloudrepo.io/docs/authenticate/repository-tokens.html
