
Security News
Package Maintainers Call for Improvements to GitHub’s New npm Security Plan
Maintainers back GitHub’s npm security overhaul but raise concerns about CI/CD workflows, enterprise support, and token management.
better-auth-credentials-plugin
Advanced tools
Generic credentials authentication plugin for Better Auth (To auth with ldap, external API, etc...)
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.
(Early version, experimental, the behaviour WILL CHANGE)
Examples (All are built using express + MongoDB):
Considerations:
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).Installation https://www.npmjs.com/package/better-auth-credentials-plugin
npm install better-auth-credentials-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";
export const authClient = createAuthClient({
plugins: [
credentialsClient<User, "/sign-in/credentials", typeof defaultCredentialsSchema>(),
],
});
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 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.
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,
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]
};
}
};
},
})
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
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>(),
],
});
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
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;
},
};
},
})
Requirements:
git clone https://github.com/erickweil/better-auth-credentials-plugin.git
cd better-auth-credentials-plugin
npm install
npm run build
docker compose up -d
cp .env.example .env
npm run example:ldap
http://localhost:3000
. You should see the better-auth OpenAPI plugin docs/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)
docker compose up -d
npm run test
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! But please note that this is an early version and not yet ready for anything. If you have any ideas or improvements, feel free to open an issue or submit a pull request.
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
FAQs
Generic credentials authentication plugin for Better Auth (To auth with ldap, external API, etc...)
We found that better-auth-credentials-plugin demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
Maintainers back GitHub’s npm security overhaul but raise concerns about CI/CD workflows, enterprise support, and token management.
Product
Socket Firewall is a free tool that blocks malicious packages at install time, giving developers proactive protection against rising supply chain attacks.
Research
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.