Skip to main content

Authentication Plugin

What's an authentication plugin?​

A plugin that decides who can access or publish a given package. By default the htpasswd is built-in, but can easily be replaced by your own.

Getting Started​

The authentication plugins are defined in the auth: section, as follows:

auth:
htpasswd:
file: ./htpasswd

also multiple plugins can be chained:

auth:
htpasswd:
file: ./htpasswd
anotherAuth:
foo: bar
bar: foo
lastPlugin:
foo: bar
bar: foo

How the chain works​

Plugins are consulted in the order they appear in auth:, and Verdaccio appends its own built-in plugin at the end — that last one is what terminates the chain, so it always ends.

What "resolve the request" means is not the same for authentication and for permissions, and the difference is where chained setups usually go wrong.

The chain, at a glance​

The red path is the one that surprises people: an error is not "try the next one", it ends the chain. false is how you say "not mine".

For allow_access and friends the shape is the same but the roles swap — true is what stops the chain, and false hands the decision on:

authenticate — first success wins​

Your plugin callsWhat happens
cb(null, ['group-a'])authenticated, the remaining plugins are skipped
cb(null, false) — or an empty/absent group listnot a failure: the next plugin is tried
cb(err)the chain stops here and the request fails with that error
(no authenticate method)the plugin is skipped

If nobody authenticates, the built-in plugin answers 403 bad username/password.

caution

An error aborts the chain. If your LDAP is unreachable and you answer errorUtils.getInternalError(...), an htpasswd plugin listed after it is never consulted and every login fails. Answer false when you simply cannot vouch for this user; keep errors for "nobody else should decide either".

Returning a string instead of an array of groups throws a TypeError at runtime.

allow_access and friends — first grant wins​

allow_access, allow_publish, allow_unpublish and allow_stage all behave the same way:

Your plugin callsWhat happens
cb(null, true)granted, the remaining plugins are skipped
cb(null, false)not a veto: the next plugin is asked
cb(err)denied with that error, the chain stops
cb(null, undefined)allow_stage only: defers to allow_publish
caution

You cannot deny with false. A plugin answering false only steps aside, and a later plugin — including the built-in one, which applies the packages: block — can still grant the request. A hard denial has to be an error.

How do the authentication plugin works?​

Basically we have to return an object with a single method called authenticate that will receive 3 arguments (user, password, callback).

On each request, authenticate will be triggered and the plugin should return the credentials, if the authenticate fails, it will fallback to the $anonymous role by default.

API​

The interface lives in @verdaccio/core, under the pluginUtils namespace. Unlike storage, authentication is still callback-based:

import { pluginUtils } from '@verdaccio/core';

interface Auth<T> extends Plugin<T> {
authenticate(user: string, password: string, cb: AuthCallback): void;
adduser?(user: string, password: string, cb: AuthUserCallback): void;
changePassword?(
user: string,
password: string,
newPassword: string,
cb: AuthChangePasswordCallback
): void;
allow_publish?(user: RemoteUser, pkg: AllowAccess & PackageAccess, cb: AuthAccessCallback): void;
allow_access?(user: RemoteUser, pkg: AllowAccess & PackageAccess, cb: AccessCallback): void;
allow_unpublish?(
user: RemoteUser,
pkg: AllowAccess & PackageAccess,
cb: AuthAccessCallback
): void;
allow_stage?(user: RemoteUser, pkg: AllowAccess & PackageAccess, cb: AuthAccessCallback): void;
apiJWTmiddleware?(helpers: any): RequestHandler;
}

Only adduser, allow_access, apiJWTmiddleware, allow_publish, allow_unpublish and allow_stage are optional, verdaccio provide a fallback in all those cases.

allow_stage gates npm stage publish. Answering undefined defers to allow_publish, which is what the built-in plugin does when the packages configuration has no stage entry.

A complete plugin​

import { API_ERROR, errorUtils, pluginUtils } from '@verdaccio/core';
import type { AllowAccess, PackageAccess, RemoteUser } from '@verdaccio/types';

/**
* Options read from your plugin's key in `auth:` of `config.yaml`.
*
* ```yaml
* auth:
* my-service:
* endpoint: https://accounts.example.com
* ```
*
* Several auth plugins can be chained; the first one that resolves a request wins and the
* rest are skipped.
*/
type CustomConfig = { endpoint?: string };

/**
* Authentication plugins decide who a user is and what they may do.
*
* Unlike storage, this contract is still **callback based** — there is no promise variant.
* Only `authenticate` is required; Verdaccio falls back to its own behaviour for every
* other method you leave out.
*
* @see https://verdaccio.org/dev/plugin-auth
*/
export default class ServiceAuth
extends pluginUtils.Plugin<CustomConfig>
implements pluginUtils.Auth<CustomConfig>
{
/**
* Verify credentials. Called on every request that carries them.
*
* The second argument decides the outcome, and the three cases are easy to confuse:
*
* - `cb(null, ['group-a'])` — authenticated, with the groups the user belongs to.
* - `cb(null, false)` — **wrong credentials**. Not an error; the request falls back to
* `$anonymous`, or to the next plugin in the chain.
* - `cb(errorUtils.getInternalError(...))` — the auth *service* failed. Use this only
* when you could not decide, never for a bad password.
*
* Messages live in `API_ERROR` (`@verdaccio/core`); most `errorUtils` helpers already
* default to the right one, so passing a hand-written string usually means duplicating
* a constant.
*/
public authenticate(user: string, password: string, cb: pluginUtils.AuthCallback): void {
if (!password) {
// no argument: the default is already API_ERROR.NO_CREDENTIALS_PROVIDED
return cb(errorUtils.getUnauthorized());
}

if (user === 'known-user' && password === 'secret') {
return cb(null, ['developers', 'publishers']);
}

return cb(null, false);
}

/**
* Handle `npm adduser`. Optional: without it, registration falls back to Verdaccio.
*
* Answer `cb(null, true)` on success, or an error to refuse — `getConflict` is the usual
* one when the account already exists or a quota is reached. Plugins backed by a directory
* you do not own (LDAP, SSO) normally delegate to `authenticate` and refuse creation.
*/
public adduser(user: string, password: string, cb: pluginUtils.AuthUserCallback): void {
this.authenticate(user, password, (err, groups) => {
if (err || !groups) {
return cb(errorUtils.getConflict(API_ERROR.REGISTRATION_DISABLED));
}
return cb(null, true);
});
}

/**
* Handle `npm profile set password`. Optional.
*
* There is no `API_ERROR` for an unknown user — `getNotFound()` defaults to
* `NO_PACKAGE`, which would be wrong here — so this is one of the few places a custom
* message is the right call.
*/
public changePassword(
user: string,
_password: string,
_newPassword: string,
cb: pluginUtils.AuthChangePasswordCallback
): void {
if (user !== 'known-user') {
return cb(errorUtils.getNotFound('user not found'));
}
return cb(null, true);
}

/**
* Can this user read the package?
*
* `pkg.access` holds whatever the `packages:` block configured, including the role tokens
* `$all`, `$anonymous` and `$authenticated`. If you answer `cb(null, false)` the client
* gets a 404 rather than a 403, on purpose: an unauthorised read must not reveal that the
* package exists.
*/
public allow_access(
user: RemoteUser,
pkg: AllowAccess & PackageAccess,
cb: pluginUtils.AccessCallback
): void {
const allowed = (pkg.access ?? []).some((group) => user.groups.includes(group));
return cb(null, allowed);
}

/** Can this user publish? Same shape as {@link allow_access}. */
public allow_publish(
user: RemoteUser,
pkg: AllowAccess & PackageAccess,
cb: pluginUtils.AuthAccessCallback
): void {
const allowed = (pkg.publish ?? []).some((group) => user.groups.includes(group));
return cb(null, allowed);
}

/**
* Can this user unpublish?
*
* When a package has no `unpublish` entry the convention is to fall back to `publish`,
* which is what the built-in plugin does.
*/
public allow_unpublish(
user: RemoteUser,
pkg: AllowAccess & PackageAccess,
cb: pluginUtils.AuthAccessCallback
): void {
const rules = Array.isArray(pkg.unpublish) ? pkg.unpublish : pkg.publish;
const allowed = (rules ?? []).some((group) => user.groups.includes(group));
return cb(null, allowed);
}
}

apiJWTmiddleware method​

apiJWTmiddleware was introduced on PR#1227 in order to have full control of the token handler, overriding this method will disable login/adduser support. We recommend don't implement this method unless is totally necessary. See a full example here.

Error messages come from API_ERROR​

@verdaccio/core exports API_ERROR, a list of the messages the registry already uses — BAD_USERNAME_PASSWORD, NO_CREDENTIALS_PROVIDED, MAX_USERS_REACHED, REGISTRATION_DISABLED, UNAUTHORIZED_ACCESS and around forty more.

Most errorUtils helpers already default to the right one, so the common case takes no argument at all:

errorUtils.getUnauthorized(); // → API_ERROR.NO_CREDENTIALS_PROVIDED
errorUtils.getNotFound(); // → API_ERROR.NO_PACKAGE
errorUtils.getConflict(); // → API_ERROR.PACKAGE_EXIST

Passing a hand-written string usually means retyping a constant, and your plugin's errors then read differently from the registry's for the same situation. Write your own message only when nothing in the list fits.

What should I return in each of the methods?​

Verdaccio relies on callback functions at time of this writing. Each method should call the method and what you return is important, let's review how to do it.

authentication callback​

Once the authentication has been executed there is 2 options to give a response to verdaccio.

If the authentication fails​

If the auth was unsuccessful, return false as the second argument.

callback(null, false);
If the authentication success​

The auth was successful.

groups is an array of strings where the user is part of.

callback(null, groups);
If the authentication produce an error​

The authentication service might fails, and you might want to reflect that in the user response, eg: service is unavailable.

import { API_ERROR, errorUtils } from '@verdaccio/core';

callback(errorUtils.getInternalError(API_ERROR.RESOURCE_UNAVAILABLE));

A failure on login is not the same as service error, if you want to notify user the credentials are wrong, just return false instead string of groups. The behaviour mostly depends of you.

adduser callback​

If adduser success​

If the service is able to create an user, return true as the second argument.

callback(null, true);
If adduser fails​

Any other action different than success must return an error.

import { API_ERROR, errorUtils } from '@verdaccio/core';

const err = errorUtils.getConflict(API_ERROR.MAX_USERS_REACHED);

callback(err);

changePassword callback​

If the request is successful​

If the service is able to create an user, return true as the second argument.

const user = serviceUpdatePassword(user, password, newPassword);

callback(null, user);
If the request fails​

Any other action different than success must return an error.

import { errorUtils } from '@verdaccio/core';

// `API_ERROR` has no entry for an unknown user, and the default for getNotFound is
// NO_PACKAGE — 'no such package available' — which would be misleading here.
const err = errorUtils.getNotFound('user not found');

callback(err);

allow_access, allow_publish, or allow_unpublish callback​

These methods aims to allow or deny trigger some actions.

If the request success​

If the service is able to create an user, return a true as the second argument.


allow_access(user: RemoteUser, pkg: PackageAccess, cb: Callback): void {
const isAllowed: boolean = checkAction(user, pkg);

callback(null, isAllowed)
}
If the request fails​

Any other action different than success must return an error.

import { API_ERROR, errorUtils } from '@verdaccio/core';

const err = getForbidden('not allowed to access package');

callback(err);

Generate an authentication plugin​

For detailed info check our plugin generator page. Run the yo command in your terminal and follow the steps.

➜ yo verdaccio-plugin

Just found a `.yo-rc.json` in a parent directory.
Setting the project root at: /Users/user/verdaccio_yo_generator

_-----_ ╭──────────────────────────╮
| | │ Welcome to │
|--(o)--| │ generator-verdaccio-plug │
`---------´ │ in plugin generator! │
( _´U`_ ) ╰──────────────────────────╯
/___A___\ /
| ~ |
__'.___.'__
´ ` |° ´ Y `

? What is the name of your plugin? service-name
? Select Language typescript
? What kind of plugin you want to create? auth
? Please, describe your plugin awesome auth plugin
? GitHub username or organization myusername
? Author's Name Juan Picado
? Author's Email jotadeveloper@gmail.com
? Key your keywords (comma to split) verdaccio,plugin,auth,awesome,verdaccio-plugin
create verdaccio-plugin-authservice-name/package.json
create verdaccio-plugin-authservice-name/.gitignore
create verdaccio-plugin-authservice-name/.npmignore
create verdaccio-plugin-authservice-name/jest.config.js
create verdaccio-plugin-authservice-name/.babelrc
create verdaccio-plugin-authservice-name/.travis.yml
create verdaccio-plugin-authservice-name/README.md
create verdaccio-plugin-authservice-name/.eslintrc
create verdaccio-plugin-authservice-name/.eslintignore
create verdaccio-plugin-authservice-name/src/index.ts
create verdaccio-plugin-authservice-name/index.ts
create verdaccio-plugin-authservice-name/tsconfig.json
create verdaccio-plugin-authservice-name/types/index.ts
create verdaccio-plugin-authservice-name/.editorconfig

I'm all done. Running npm install for you to install the required dependencies. If this fails, try running the command yourself.


⸨ ░░░░░░░░░░░░░░░░░⸩ ⠋ fetchMetadata: sill pacote range manifest for @babel/plugin-syntax-jsx@^7.7.4 fetc

After the install finish, access to your project scalfold.

➜ cd verdaccio-plugin-service-name
➜ cat package.json

{
"name": "verdaccio-plugin-service-name",
"version": "0.0.1",
"description": "awesome auth plugin",
...

Full implementation ES5 example​

function Auth(config, stuff) {
var self = Object.create(Auth.prototype);
self._users = {};

// config for this module
self._config = config;

// verdaccio logger
self._logger = stuff.logger;

// pass verdaccio logger to ldapauth
self._config.client_options.log = stuff.logger;

return self;
}

Auth.prototype.authenticate = function (user, password, callback) {
var LdapClient = new LdapAuth(self._config.client_options);
....
LdapClient.authenticate(user, password, function (err, ldapUser) {
...
var groups;
...
callback(null, groups);
});
};

module.exports = Auth;

And the configuration will looks like:

auth:
htpasswd:
file: ./htpasswd

Where htpasswd is the sufix of the plugin name. eg: verdaccio-htpasswd and the rest of the body would be the plugin configuration params.

List Community Authentication Plugins​

Have you developed a new plugin? Add it here !