Maven Credential Helper

Published 2026-03-03
javamavengitlabops

As a longtime Maven user, I have quite often been annoyed by one particular feature: Repository authentication. Since the beginning of Maven 2, this has been done the same way, by having a <server> node in the $HOME/.m2/settings.xml. This post will explore a new Maven extension I have created that attempts to avoid hard-coding these credentials in a static xml file. Before we get into that, however, we will take a look at how this works today.

The current state of affairs

There are a few things to know here:

  • The order of definition matters, since that is an implicit order of priority.
  • The values in <id> elements must be unique within the set of repository types.

There are 3 main types of repositories:

  • dependency
  • plugin
  • distributionManagement

The most basic thing we need is being able to resolve libraries (dependencies), from either public or private sources. For this example we'll use gitlab packages.

pom.xml snippet:

<repositories>
  <repository>
    <id>gitlab-group-1</id>
    <url>https://gitlab.example.com/api/v4/groups/1/-/packages/maven</url>
  </repository>
  <repository>
    <id>gitlab-group-2</id>
    <url>https://gitlab.example.com/api/v4/groups/2/-/packages/maven</url>
  </repository>
</repositories>

For a more complex build, like having private plugins, we also need plugin repositories:

<pluginRepositories>
  <pluginRepository>
    <id>gitlab-group-1</id>
    <url>https://gitlab.example.com/api/v4/groups/1/-/packages/maven</url>
    </releases>
  </pluginRepository>
  <pluginRepository>
    <id>gitlab-group-2</id>
    <url>https://gitlab.example.com/api/v4/groups/2/-/packages/maven</url>
  </pluginRepository>
</pluginRepository>

If we want to publish the project to the maven repository allowing other projects depend on that. We need:

<distributionManagement>
  <repository>
    <id>gitlab-project</id>
    <url>https://gitlab.example.com/api/v4/projects/1000/packages/maven</url>
  </repository>
  <snapshotRepository>
    <id>gitlab-project</id>
    <url>https://gitlab.example.com/api/v4/projects/1000/packages/maven</url>
  </snapshotRepository>
</distributionManagement>

This is all well and good. But we still need authentication of the remote repositories. With Maven we need to add each private repository id to a corresponding <server> node like this in $HOME/.m2/settings.xml

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <servers>
    <server>
      <id>gitlab-project</id>
      <username>gitlabusername</username>
      <password>personal-access-token</password>
    </server>
    <server>
      <id>gitlab-group-1</id>
      <username>gitlabusername</username>
      <password>personal-access-token</password>
    </server>
    <server>
      <id>gitlab-group-2</id>
      <username>gitlabusername</username>
      <password>personal-access-token</password>
    </server>
  </servers>
</settings>

So what is the problem?

All the authentication information in a cleartext file. There is also a lot of duplication, since we have to repeat this information for each server element.

I am sure there are some solution in Maven 3 or Maven 4 to that.

The main issue, in my view, is that we need relatively static passwords for all these <server> nodes. Users will create tokens that are long-lived and this may cause a security nightmare. The issue can be slightly improved by using interpolated environment variables, but this just moves the problem somewhere else. The duplication issue remains.

One possible solution to this could be to have developer tooling that rewrites this file every so often, but this assumes that the file is managed by a security team. Unfortunately this file is too useful for local tooling to make that really a viable solution. Developer workstations are usually not the most uniform in their setup, so there must be a better way.

Aside: Docker authentication

If we look at how docker cli handles authentication, we see a very similar setup. The configuration can be found found in $HOME/.docker/config.json

If you for instance login to a docker respository using the cli, there will a corresponding auths section. For now we will ignore the credHelpers and credsStore properties we will get back to them in a bit.

{
  "auths": {
    "registry.github.example.com": {
      "auth": "<base64-encoded>"
    },
    "ghcr.io": {
      "auth": "<base64-encoded>"
    },
    "https://index.docker.io/v1/": {
      "auth": "<base64-encoded>"
    }
  }
}

This has exactly the same problem as maven, users will create long-lived tokens for these.

Docker has one feature which improves cleartext storage slightly and that is the credsStore which uses the system keychain or pass. But for now, we can consider docker authentication the same as maven authentication.

Docker's solution

Docker introduces a protocol for handling credentials.

The way it works is by outsourcing the problem to a command line interface (CLI) hook on the PATH, thereby making the Authentication a coding problem instead of a configuration problem.

This hook is not meant to be used by humans, but by the docker cli process.

There are multiple implementations of this interface by vendors like google, gitlab, github (multiple) and amazon to name a few.

Having a separate command for authentication introduces complexity. We now need another program to be made available to users and put that in the PATH. Luckily there is a solution for most of these, and that is by using mise. I will not go into details of how that works as the documentation is quite good and my friend Karl Yngve Lervåg has written about it.

Registration

We register a new credential helper for docker by using the $HOME/.docker/config.json as before:

{
  "credHelpers": {
    "registry.gitlab.example.com": "glab",
    "ghcr.io": "ghcr-login"
  }
}

The short names are are actually references to the command docker-credential-<shortname> which MUST be found in the system PATH.

Example: docker-credential-glab found in $HOME/.local/bin

My solution to the Maven Authentication Problem

Based on the information above I realized that I could build a maven extension that uses the exact same protocol. The way the extension works is by hooking into the maven lifecycle and enhancing the Authentication lookup mechanism.

This would allow us to have short lived tokens, and get them controlled by the team that manages the repository. It would allow platform- and security teams to greatly increase the security, but not impeding the developers by having them do manual labour.

We get the same increased complexity by having to install a separate command-hook for authentication, but that is that manageable.

Setting it up

We assume that we have a maven project located at /home/myuser/Projects/my-maven-project. In the rest of this post we will reference this path as ${project.basedir}.

We will need to register the extension in ${project.basedir}/.mvn/extensions.xml. This must be done since we need to hook into Maven before it has loaded any projects.

<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
            xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.1.0 https://maven.apache.org/xsd/core-extensions-1.1.0.xsd">
    <extension>
        <groupId>net.hamnaberg</groupId>
        <artifactId>maven-credential-helper</artifactId>
        <version>version-from-badge-below</version>
    </extension>
</extensions>

Sonatype Central

This sets up Maven to be able to use a maven-credential-helper, but none are currently registered. So let us do just that.

We need to define a new file ${project.basedir}/.mvn/credhelpers.json, which we expect users to be able to share. That means that we need to use the shorthand syntax referenced in the documentation.

{ "https://gitlab.example.com": "glab" }

Right now there is no defined maven-credential-glab anywhere, so we'll have to write it ourselves. For simplicity this is written in bash.

We start by adding some dependencies to the global mise toolset.

mise use -g glab

Then we need to login to our gitlab instance.

glab auth login --hostname gitlab.example.com

Lastly we will write our script.

#!/bin/bash
# $HOME/bin/maven-credential-glab
HOSTNAME="gitlab.example.com"

# this command ensures we have an up-to-date token in the config file if we use oauth2 tokens.
status=$(glab auth status --hostname "$HOSTNAME" 2>&1)
if [[ $? != 0 ]]; then
  echo $status >&2
  exit 2
fi

MAVEN_TARGET_URL="https://$HOSTNAME"
# Hack to fix issue in glab, which looks up the USER environment variable. This is not what we want here.
OLD_USER=$USER
unset $USER
MAVEN_USERNAME="$(glab config get --host "$HOSTNAME" user)"
export USER=$OLD_USER
# End hack

## Get the token from glab
MAVEN_SECRET="$(glab config get --host "$HOSTNAME" token)"

# The maven credential helper extension passes the command (get, list) as the first argument
COMMAND=$1

case "$COMMAND" in
    get)
        # Docker sends the registry URL via stdin
        read -r INPUT_URL

        # Exercise for the reader is to improve the regex to make sure we have a maven Repository url here.
        if [[ "$MAVEN_TARGET_URL" == *"$INPUT_URL"* ]] || [[ "$INPUT_URL" == *"$MAVEN_TARGET_URL"* ]]; then
            # Return JSON with the credentials
            printf '{"ServerURL":"%s","Username":"%s","Secret":"%s"}\n' \
                "$INPUT_URL" "$MAVEN_USERNAME" "$MAVEN_SECRET"
        else
            # If the URL doesn't match, return an error message to stderr
            # and exit with a non-zero code so Docker knows it's not here.
            echo "Credentials not found for $INPUT_URL" >&2
            exit 1
        fi
        ;;

    list)
        printf '{"%s":"%s"}\n' "$MAVEN_TARGET_URL" "$MAVEN_USERNAME"
        ;;

    *)
        exit 0
        ;;
esac

Save this as $HOME/bin/maven-credential-glab and make it executable chmod 755 $HOME/bin/maven-credential-glab.

Lets test it:

echo "https://gitlab.example.com/api/v4/groups/2/-/packages/maven" | maven-credential-glab get

Now we can resolve the dependencies for all the configured repositories that are part of gitlab.example.com.

Conclusion

Using the docker-credential-helper protocol has enabled us to avoid hard-coding tokens in the settings.xml file. We avoid repeating ourselves, keeping things DRY. We also eliminate error prone updates, since we avoid repeating ourselves.

I find this very useful, and will start using it for my current project. I am sure other uses may appear down the road. I can imagine this can be useful for npm authentication or similar things.

Tilbake til bloggen