npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

better-auth-credentials-plugin

v0.4.0

Published

Generic credentials authentication plugin for Better Auth (To auth with ldap, external API, etc...)

Readme

Better Auth Credentials Plugin

npm version

Generic credentials authentication plugin for Better Auth

The plugin itself can be used to authenticate to anything, as are you that handle the logic that verify user input credentials in the callback, and just need to return user data that will be used to create/update the user in the database.

Features

  • Full control over the authentication process
  • Auto sign-up (optional) and management of Account linking and session creation
  • Similar in behaviour to the default email & password flow, but YOU handle the verification of the credentials and allow automatically sign-up
  • Route customization, you can choose the route path and the body parameters (using zod schema that doubles as validation and OpenAPI documentation)
  • Supports custom callbacks for sign-in and sign-up events

Examples (All are built using express + MongoDB):

  • examples/basic - Basic usage example with a fake user store, showcasing the credentials callback functionality and how to handle user data
  • examples/ldap-auth - Uses this plugin to perform LDAP authentication, showing how easy is to use it

Considerations:

  • You need to return a email field after the authentication, this is used to create/update the user in the database, and also to link the account with the session (email field should be unique).
  • It's not intended to use this to re-implement password login, but to be used when you need to integrate with an external system that uses credentials for authentication, like LDAP, or any other system that you can verify the credentials and get user data. If you try to mimic password login by hashing and storing the password, aditional database round-trips will be needed as this plugin will search the user again after you alread did (just use the email & password flow or username plugin don't do this).
  • If you want to use both this plugin and the email & password flow (or username), you must decide which behaviour to have:
  1. The default: Accounts created with this plugin will not be able to login with email & password (because no password is set), and accounts that have a password set will only be able to login with email & password.
  2. Specifying providerId: You can set a custom providerId for this plugin, mimicking a social login provider, so accounts created with this plugin will be separate from email & password accounts. Then if you want to allow users to login with both methods, you can set linkAccountIfExisting: true

Installation https://www.npmjs.com/package/better-auth-credentials-plugin

npm install better-auth-credentials-plugin

API Details

Configuration of the plugin

To use this plugin, you need to install it and configure it in your Better Auth application. The plugin provides a way to authenticate users using credentials (like username and password) that can be customized to fit your needs.

Hello world usage example (just to show how to use the plugin): auth.ts

import { betterAuth } from "better-auth";
import { credentials } from "better-auth-credentials-plugin";

// Server side:
export const auth = betterAuth({
    /** ... other configs ... */
    emailAndPassword: {
        // Disable email and password authentication
        enabled: false,
    },
    plugins: [
        credentials({
            autoSignUp: true,
            async callback(ctx, parsed) {
                return {};
            },
        })
    ],
});

// Client side:
import { User } from "better-auth";
import { createAuthClient } from "better-auth/client";
import { credentialsClient, defaultCredentialsSchema } from "better-auth-credentials-plugin/client";

export const authClient = createAuthClient({
    plugins: [
        credentialsClient<User, "/sign-in/credentials", typeof defaultCredentialsSchema>(),
    ],
});

Note that when importing in client side, in some bundlers (Webpack, Turbopack, Vite) you must use the dedicated client entry point via subpath export (better-auth-credentials-plugin/client).

Doing as above would allow any user sign in with any password, and create new users automatically if they don't exist.

The full set of options for the plugin is as follows:

| Attribute | Description | |-----------------------------|----------------------------------------------------------------------------------| | callback * | This callback is the only required option, here you handle the login logic and return the user data to create a new user or update existing ones | | inputSchema | Zod schema that defined the body contents of the sign-in route, you can put any schema you like, but if it doesn't have an email field, you then need to return the email to use in the callback. Defaults to the same as User & Password flow {email: string, password: string, rememberMe?: boolean} | | autoSignUp | If true will create new Users and Accounts if the don't exist | | linkAccountIfExisting | If true, will link the Account on existing users created with another login method (Only have effect with autoSignUp true) | | providerId | Id of the Account provider defaults to credential | | path | Path of the route endpoint, defaults to /sign-in/credentials | | UserType | If you have aditional fields in the User type and want correct typescript types in the callbacks, you can set here it's type, example: {} as User & {lastLogin: Date} |

If the callback throws an error or returns a falsy value, auth will fail with generic 401 Invalid Credentials error.

You then must return an object with the following shape: | Attribute | Description | |-----------------------------|----------------------------------------------------------------------------------| | ...userData | User data that will be used to create or update the user in the database, this must contain an email field if the inputSchema doesn't have it | | onSignIn | Callback that will be called after the user is sucesfully signed in. It receives the user data returned above, User and Account from database as parameters, and you should return the mutated user data to update (The account linking happens after this callback, so it can be null) | | onSignUp | Callback that will be called after the user is sucesfully signed up (only if autoSignUp is true). It receives the user data returned above, and you should return the mutated user data with the fields a new user should have | | onLinkAccount | Callback that will be called when a Account is linked to the user. Can happen in a fresh new user sign up or the first time a existing user signs in with this credentials provider. It receives the User from database as parameter, and you should return additional fields to put in the Account being created. You shouldn't throw errors on this callback, because when it runs the user was already created in the database |

All those callbacks can be async if you want.

  • If the onSignIn returns a falsy value or throws an error, auth will fail with generic 401 Invalid Credentials error, you can return an empty object to skip updating the user data in the database.
  • If the onSignUp returns a object without email field, falsy value or throws an error, auth will fail with generic 401 Invalid Credentials error.
  • OnLinkAccount shouldn't throw errors nor return a falsy value, in the moment this callback is called the user was already created, so you'll leave a user without an account linked to it, which could cause issues.

If the error you throw is a instance of APIError from better-call package, the error returned will be the one you threw instead of the generic 401 Invalid Credentials error, so this way you can return a more specific error code and message to the user if needed.

Usage examples

Basic: Accept only equal email and password

Example using the plugin to authenticate users with a simple username and password, where the credentials must be the same as the password. This is just for demonstration purposes,

examples/basic

credentials({
    autoSignUp: true,
    // Credentials login callback, this is called when the user submits the form
    async callback(ctx, parsed) {
        // Just for demonstration purposes, half of the time we will fail the authentication
        if (parsed.email !== parsed.password) {
            throw new Error("Authentication failed, please try again.");
        }
        
        return {
            // Called if this is a existing user sign-in
            onSignIn(userData, user, account) {
                console.log("Existing User signed in:", user);

                return userData;
            },

            // Called if this is a new user sign-up (only used if autoSignUp is true)
            onSignUp(userData) {
                console.log("New User signed up:", userData.email);

                return {
                    ...userData,
                    name: parsed.email.split("@")[0]
                };
            }
        };
    },
})

Login on external API

Example using the plugin to authenticate users against an external API, when you want to use the plugin to authenticate users against an external system that uses credentials for authentication, like a custom API or service. For this demonstration, the API has predefined users and returns user data after successful authentication.

Server side: examples/external-api/auth.ts


export const myCustomSchema = z.object({
    username: z.string().min(1),
    password: z.string().min(1),
});

export const auth = betterAuth({
    plugins: [
        credentials({
            autoSignUp: true,
            path: "/sign-in/external",
            inputSchema: myCustomSchema,
            // Credentials login callback, this is called when the user submits the form
            async callback(ctx, parsed) {
                // Simulate an external API call to authenticate the user
                const { username, password } = parsed;
                const response = await fetch(`http://localhost:${process.env.PORT || 3000}/example/login`, {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json",
                    },
                    body: JSON.stringify({ username, password }),
                });

                if (!response.ok) {
                    throw new Error("Error authenticating:"+ ` ${response.status} ${response.statusText}`);
                }

                const apiUser = await response.json();

                return {
                    // Must return email, because inputSchema doesn't have it
                    email: apiUser.email,

                    // Other user data to update
                    name: apiUser.name,
                    username: apiUser.username,
                };
            },
        }),
    ],
});

When you provide custom path and inputSchema, you must pass the type parameters to the credentialsClient on the client side, so it can infer the correct types for the user data and input schema.

Client side: examples/external-api/client.ts

// Dedicated client entry point via subpath export, prevents server-side dependencies from being included in the client bundle
import { credentialsClient, defaultCredentialsSchema } from "better-auth-credentials-plugin/client";

export const authClient = createAuthClient({
    // The base URL of your Better Auth API
    baseURL: `http://localhost:${port}`,
    plugins: [
        // Initialize the client plugin with the correct generic types parameters:
        // 0: User -> The type of the user returned by the API
        // 1: "/sign-in/external" -> The path for the credentials sign-in endpoint
        // 2: typeof myCustomSchema -> The input schema for the credentials sign-in
        credentialsClient<User, "/sign-in/external", typeof myCustomSchema>(),

        // https://www.better-auth.com/docs/concepts/typescript#inferring-additional-fields-on-client
        // This will infer the additional fields defined in the auth schema
        // and make them available on the client (e.g., `username`).
        inferAdditionalFields<typeof auth>(),
    ],
});

LDAP Authentication Example

Example using the plugin to authenticate users against an LDAP server, showcasing how to use the plugin with an external authentication system.

Uses https://github.com/shaozi/ldap-authentication for LDAP authentication

examples/ldap-auth

credentials({
    // User type to use, this will be used to type the user in the callback
    // This way the zod schema will infer correctly, otherwise you would have to pass both generic types explicitly
    UserType: {} as User & {
        ldap_dn: string,
        description: string,
        groups: string[]
    },
    // Sucessful authenticated users will have a 'ldap' Account linked to them, no matter if they previously exists or not
    autoSignUp: true,
    linkAccountIfExisting: true,
    providerId: "ldap",
    inputSchema: z.object({
        credential: z.string().min(1),
        password: z.string().min(1)
    }),
    // Credentials login callback, this is called when the user submits the form
    async callback(ctx, parsed) {
        // Login via LDAP and return user data
        const secure = process.env.LDAP_URL!.startsWith("ldaps://");
        const ldapResult = await authenticate({
            // LDAP client connection options
            ldapOpts: {
                url: process.env.LDAP_URL!,
                connectTimeout: 5000,
                strictDN: true,
                ...(secure ? {tlsOptions: { minVersion: "TLSv1.2" }} : {})
            },
            adminDn: process.env.LDAP_BIND_DN,
            adminPassword: process.env.LDAP_PASSW,
            userSearchBase: process.env.LDAP_BASE_DN,
            usernameAttribute: process.env.LDAP_SEARCH_ATTR,
            // https://github.com/shaozi/ldap-authentication/issues/82
            //attributes: ['jpegPhoto;binary', 'displayName', 'uid', 'mail', 'cn'],
            explicitBufferAttributes: ["jpegPhoto"],

            username: parsed.credential,
            userPassword: parsed.password,
        });
        const uid = ldapResult[process.env.LDAP_SEARCH_ATTR!];
        
        return {
            // Required to return email to identify the user, as the inputSchema does not have it
            email: (Array.isArray(ldapResult.mail) ? ldapResult.mail[0] : ldapResult.mail) || `${uid}@local`,

            // Atributes that will be saved in the user, regardless if is sign-in or sign-up
            ldap_dn: ldapResult.dn,
            name: ldapResult.displayName || uid,
            description: ldapResult.description || "",
            groups: ldapResult.objectClass && Array.isArray(ldapResult.objectClass) ? ldapResult.objectClass : [],
            
            // Callback that is called after sucessful sign-up (New user)
            async onSignUp(userData) {
                // Only on sign-up we save the image to disk and save the url in the user data
                if(ldapResult.jpegPhoto) {
                    userData.image = await saveImageToDisk(ldapResult.uid, ldapResult.jpegPhoto);
                }

                return userData;
            },
        };
    },
})

Building and running the example

Requirements:

  • Node.js (v20 or later)
  • Docker
  1. Clone the repository:
git clone https://github.com/erickweil/better-auth-credentials-plugin.git
cd better-auth-credentials-plugin
  1. Install dependencies and build the project:
npm install
npm run build
  1. Start the MongoDB server and the test LDAP server using Docker:
docker compose up -d
  1. Run the example:
cp .env.example .env
npm run example:ldap
  1. Open your browser and go to http://localhost:3000. You should see the better-auth OpenAPI plugin docs
  • Now you can login with the LDAP credentials, go to Credentials -> /sign-in/credentials and use the following credentials (username & password must be those values):
{
  "credential": "fry",
  "password": "fry"
}

You can use any value from the default values: https://github.com/rroemhild/docker-test-openldap

Using ldap sign-up should be done automatically after the first sucessful sign-in via LDAP, just like social sign-in, (unless you don't have it enabled it in the configuration)

Running the tests

docker compose up -d
npm run test

License

This project is licensed under the MIT License - see the LICENSE file for details.

Contributing

Contributions are welcome! If you have any ideas or improvements, feel free to open an issue or submit a pull request. When you do that, you are free to include relevant tests and update the documentation accordingly.

Acknowledgements

This project is inspired by the need for a simple and effective way to integrate LDAP authentication into Better Auth. Special thanks to the Better Auth team for their work on the core library.

Also this project would not be possible if not for shaozi/ldap-authentication package which was used for the LDAP authentication

  • https://github.com/shaozi/ldap-authentication