Prerequisites
Before using this integration, you need:
- An Autheona account: Sign up
- An Autheona project. Use a Sandbox project for testing and a Production project for live applications. See the Quick Start Guide.
- An Autheona access token for the project. See the Access Tokens documentation.
- A Better Auth project with Better Auth installed and configured.
- A database configured for Better Auth if your application uses persistent user storage.
Better Auth is a TypeScript authentication framework that supports email and password authentication, social providers, sessions, plugins, and other authentication features.
Autheona access tokens provide access to the Autheona API. Store the token securely using a server-side environment variable or your deployment platform's secret management system.
Never hardcode an Autheona access token in your source code.
Important
Decide where you want the Autheona check to occur:
- Use validateUserInfo when you want to evaluate identities before users are created, linked, or authenticated through supported provider flows.
- Use a lower level databaseHooks.user.create.before hook when you specifically need to validate or modify the database user record before it is written.
- Use a request before hook when you need to target a specific Better Auth endpoint such as
/sign-up/email.
For a general user trust policy, validateUserInfo is the recommended integration point.
Validate User Information
Use the Better Auth validateUserInfo callback when you want Autheona to evaluate an identity before Better Auth accepts it. This is generally the appropriate integration point when your primary goal is preventing disposable, suspicious, or otherwise disallowed identities from entering your authentication system.
Better Auth documents validateUserInfo as a high level policy gate. It can run before a user is created, before an account is linked, and during supported provider sign in flows.
Better Auth Configuration
Open your Better Auth configuration file. Depending on your project, this may be:
src/lib/auth.ts
or:
lib/auth.ts
or:
auth.ts
A basic Better Auth configuration looks like:
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: yourDatabase,
emailAndPassword: {
enabled: true,
},
});
Better Auth supports email and password authentication through the emailAndPassword.enabled option. Add the Autheona validation logic to the configuration.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: yourDatabase,
emailAndPassword: {
enabled: true,
},
user: {
validateUserInfo: async ({ user, source }) => {
const email = user.email;
if (!email) {
return {
error: "missing_email",
errorDescription: "An email address is required.",
};
}
const apiKey = process.env.AUTHEONA_API_KEY;
if (!apiKey) {
console.log("Autheona access token is not configured");
return;
}
const normalizedEmail = email.toLowerCase().trim();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(
"https://api.autheona.com/v1/intelligence",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
email_address: normalizedEmail,
}),
signal: controller.signal,
},
);
if (!response.ok) {
console.log(`Autheona API error: ${response.status}`);
return;
}
const result = await response.json();
if (result.action === "block") {
return {
error: "email_not_allowed",
errorDescription: "Email address is not allowed.",
};
}
console.log(
`Autheona returned action "${result.action}" for ${normalizedEmail}`,
);
console.log(
`Better Auth validation source: ${source.action} / ${source.method}`,
);
} catch (error) {
console.log(
`Autheona validation error: ${
error instanceof Error ? error.message : String(error)
}`,
);
} finally {
clearTimeout(timeout);
}
},
},
});
The example above evaluates the user's email before Better Auth accepts the identity. If Autheona returns block, the callback returns an error object, which prevents the operation from continuing.
Failure Behavior
The example above uses a fail-open strategy for Autheona infrastructure failures.
The behavior is:
- If Autheona returns
block, the operation is denied. - If Autheona returns
allow, the operation continues. - If Autheona returns
ignore, the operation continues. - If Autheona is temporarily unavailable, the operation continues.
- If the Autheona request times out, the operation continues.
- If the Autheona API returns an HTTP error, the operation continues.
This behavior is intentional. It prevents an Autheona outage from automatically becoming an outage of your authentication system. A policy decision returned by Autheona is different from an infrastructure failure.
For example:
Autheona returns block -> Deny the identity
While:
Autheona cannot be reached -> Continue authentication
If your security requirements require fail-closed behavior, change the error handling so that Autheona failures also result in a rejected authentication request.
For example:
return {
error: "validation_failed",
errorDescription: "Unable to validate the email address.",
};
Choose the failure behavior deliberately based on the role Autheona plays in your authentication flow.
Understanding validateUserInfo
Better Auth's validateUserInfo callback is designed as a high level identity policy gate. The callback receives information about the user and the operation that caused the validation. The source information can identify operations such as:
create-user
link-account
sign-in
The authentication method is also available through the source information. Additional properties like source.oauth (for OAuth provider data) and source.sso (for SSO claims) are available depending on the authentication method.
For example:
user: {
validateUserInfo: async ({ user, source }) => {
console.log(user.email);
console.log(source.action);
console.log(source.method);
// For OAuth providers, check provider-specific data
if (source.oauth?.providerId === "google") {
console.log(source.oauth.profile?.hd); // Google Workspace domain
}
},
},
To reject an identity, return an error object with error and errorDescription properties. Return nothing (or undefined) to allow the operation to continue.
user: {
validateUserInfo: async ({ user, source }) => {
if (!user.email?.endsWith("@example.com")) {
return {
error: "invalid_domain",
errorDescription: "Only example.com emails are allowed.",
};
}
},
},
This makes it possible to apply the same Autheona policy across multiple authentication paths instead of implementing separate checks for every provider. For example:
flowchart TD
A[Authentication Request] --> B{Better Auth}
B --> C[validateUserInfo]
C --> D[Autheona User Trust API]
D --> E{Policy Decision}
E -->|allow| F[Continue]
E -->|ignore| F
E -->|block| G[Deny]This approach is useful when your application supports more than email and password authentication.
Email and Password Signup
Better Auth provides email and password authentication through the emailAndPassword configuration. For example:
export const auth = betterAuth({
database: yourDatabase,
emailAndPassword: {
enabled: true,
},
});
A client can then create a user with:
const { data, error } = await authClient.signUp.email({
name: "John Doe",
email: "john@example.com",
password: "password1234",
});
Better Auth exposes the email signup endpoint as /sign-up/email. With the Autheona integration enabled, the request becomes:
flowchart TD User["User"] --> Signup["Better Auth signup"] Signup --> Validate["validateUserInfo"] Validate --> Autheona["Autheona"] Autheona --> Policy["Policy decision"] Policy -->|allow| Create["Create user"] Policy -->|ignore| Create Policy -->|block| Reject["Reject signup"]
The client does not need to call Autheona directly. The Autheona access token remains on the server.
Social Authentication
The same validateUserInfo policy can also be used when your Better Auth application supports social authentication.
For example:
flowchart TD User["User"] --> Provider["Google / GitHub / Other Provider"] Provider --> BetterAuth["Better Auth"] BetterAuth --> Validate["validateUserInfo"] Validate --> Autheona["Autheona"] Autheona --> Policy["Policy decision"] Policy -->|allow| Continue["Continue"] Policy -->|ignore| Continue Policy -->|block| Reject["Deny"]
This is useful when you want to apply a consistent identity policy across authentication methods. For example, if a provider returns a disposable email address and Autheona returns:
{
"action": "block"
}
your validation callback can reject the identity before it is accepted. Better Auth documents validateUserInfo as running for user creation and account linking across authentication methods, including OAuth and other provider based flows.
Account Linking
Better Auth can also evaluate identities when a new provider account is linked to an existing user.
Because validateUserInfo receives the current operation through source.action, the same Autheona policy can be applied to account linking.
For example:
user: {
validateUserInfo: async ({ user, source }) => {
if (source.action !== "link-account") {
return;
}
// Run the same Autheona validation used by the main integration.
},
},
In most applications, you do not need to branch on the action. If your policy should apply consistently to all supported identity creation and linking operations, simply validate the email for every invocation.
Request Specific Hooks
Better Auth also provides request lifecycle hooks. A request before hook can run custom logic before an endpoint is executed. Better Auth's documentation shows that hooks can inspect the request path and perform validation before the endpoint continues.
For example:
import { betterAuth } from "better-auth";
import { createAuthMiddleware, APIError } from "better-auth/api";
export const auth = betterAuth({
database: yourDatabase,
hooks: {
before: createAuthMiddleware(async (ctx) => {
if (ctx.path !== "/sign-up/email") {
return;
}
const email = ctx.body?.email;
if (!email) {
throw new APIError("BAD_REQUEST", {
message: "An email address is required.",
});
}
// Autheona validation
}),
},
});
This approach is useful when you only want to protect a specific endpoint. For example:
/sign-up/email
could be protected without applying the check to other authentication operations.
Note
Important difference between integration points:
validateUserInforeturns error objects ({ error, errorDescription }) to reject identities- Request
hooks.beforewithcreateAuthMiddlewarethrowsAPIErrorto stop request processing databaseHooks.user.create.beforecan throwAPIErroror return modified data
The patterns are not interchangeable. Use the correct pattern for each integration point.
However, if your goal is to create a general identity policy across authentication methods, validateUserInfo is usually the better integration point.
Database Hooks
Better Auth also provides database lifecycle hooks. A databaseHooks.user.create.before hook runs before a user record is created.
For example:
import { betterAuth } from "better-auth";
import { APIError } from "better-auth/api";
export const auth = betterAuth({
database: yourDatabase,
databaseHooks: {
user: {
create: {
before: async (user) => {
const email = user.email;
if (!email) {
throw new APIError("BAD_REQUEST", {
message: "An email address is required.",
});
}
const apiKey = process.env.AUTHEONA_API_KEY;
if (!apiKey) {
console.log("Autheona access token is not configured");
return { data: user }; // Fail-open
}
const normalizedEmail = email.toLowerCase().trim();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 3000);
try {
const response = await fetch(
"https://api.autheona.com/v1/intelligence",
{
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
email_address: normalizedEmail,
}),
signal: controller.signal,
},
);
if (!response.ok) {
console.log(`Autheona API error: ${response.status}`);
return { data: user }; // Fail-open
}
const result = await response.json();
if (result.action === "block") {
throw new APIError("BAD_REQUEST", {
message: "Email address is not allowed.",
});
}
return { data: user };
} catch (error) {
if (error instanceof APIError) {
throw error;
}
console.log(
`Autheona validation error: ${
error instanceof Error ? error.message : String(error)
}`,
);
return { data: user }; // Fail-open
} finally {
clearTimeout(timeout);
}
},
},
},
},
});
Better Auth documents database hooks for user, session, and account lifecycle operations. A before hook can abort the operation or replace the data being written.
Database hooks are useful when your validation is specifically tied to database creation. However, they are lower level than validateUserInfo. If the goal is to decide whether Better Auth should accept an identity, use:
validateUserInfo
If the goal is to modify or validate the database record itself, use:
databaseHooks.user.create.before
Testing With a Custom Policy
For testing and development, you can provide a custom_policy in the Autheona request. For example:
const customPolicy = {
parent_rules: [
{
action: "block",
field: "domain_disposable",
operator: "==",
target: {
bool: true,
},
},
],
stop_on_block: true,
version: "1.0",
};
const response = await fetch("https://api.autheona.com/v1/intelligence", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
email_address: normalizedEmail,
custom_policy: customPolicy,
}),
});
Warning
custom_policy is intended for testing and development. For production use, configure your policy in the Autheona Policy Editor instead of sending a custom policy with every request.
See the Autheona Policy Editor documentation.
Understanding Autheona Actions
The User Trust API returns an action representing the policy result. The commonly used actions are:
allow: Allow the request to continue.block: Block the request.ignore: Do not block the request based on the returned policy decision.
Your application should decide what each action means for its own authentication workflow.
For example:
if (result.action === "block") {
return {
error: "email_not_allowed",
errorDescription: "Email address is not allowed.",
};
}
The integration does not need to treat ignore as a manual review state. If your application does not have a review workflow, ignore can simply allow the authentication request to continue.
API Errors and Rate Limits
The Autheona API uses the following HTTP response categories:
| HTTP status | Meaning |
|---|---|
200 | Successful request with intelligence results |
400 | Invalid request |
401 | Invalid or missing authentication |
500 | Internal server error |
Autheona currently documents a rate limit of 10 requests per second per access token. Production integrations should avoid unnecessary retries and should handle transient rate limit or server errors appropriately. See the Autheona API Reference.
Timeout and Retry Considerations
The example uses a 3 second request timeout to prevent an external API request from waiting indefinitely. The timeout is an example value. Adjust it based on your application's authentication and registration requirements.
If you implement retries:
- Retry only transient failures.
- Do not blindly retry authentication failures such as HTTP
401. - Avoid excessive retries during user authentication.
- Consider Autheona rate limits and API quotas.
- Use exponential backoff where appropriate.
For authentication flows, a short timeout with a deliberate fail open or fail closed policy is preferable to an unrestricted retry loop.
Choosing the Correct Better Auth Integration Point
Use validateUserInfo when your primary requirement is:
Evaluate the identity before Better Auth accepts it.
Use databaseHooks.user.create.before when your primary requirement is:
Validate or modify the user record before it is written to the database.
Use a request before hook when your primary requirement is:
Run custom validation for a specific Better Auth endpoint.
For most Autheona integrations, validateUserInfo provides the cleanest integration because the policy is attached to the identity rather than to one specific authentication endpoint.
Minimal Integration Architecture
The recommended architecture is:
flowchart TD
A[User] --> B[Better Auth]
B --> C[validateUserInfo]
C --> D[Autheona User Trust API]
D --> E{Policy Decision}
E -->|allow| F[Continue authentication]
E -->|ignore| G[Continue authentication]
E -->|block| H[Deny authentication]For email signup:
flowchart TD
A[User] --> B[Better Auth Signup]
B --> C[validateUserInfo]
C --> D[Autheona User Trust API]
D --> E{Policy Decision}
E -->|allow| F[Create Better Auth user]
E -->|ignore| G[Create Better Auth user]
E -->|block| H[Reject signup]For provider authentication:
flowchart TD
A[User] --> B[OAuth Provider]
B --> C[Better Auth]
C --> D[validateUserInfo]
D --> E[Autheona User Trust API]
E --> F{Policy Decision}
F -->|allow| G[Continue]
F -->|ignore| G
F -->|block| H[Deny]Production Recommendations
Before deploying this integration to production:
- Keep the Autheona access token in a server side secret.
- Never hardcode the token in your Better Auth configuration.
- Use a Production Autheona project for live traffic.
- Configure production policies in the Autheona Policy Editor.
- Test the
allow,block, andignoreoutcomes. - Test Autheona API failures and timeouts.
- Decide explicitly whether your integration should fail open or fail closed.
- Keep the HTTP timeout appropriate for your authentication UX.
- Avoid unnecessary retries.
- Monitor Autheona API usage and rate limits.
- Do not store the full Autheona intelligence response in the Better Auth user record unless required.
- Revoke and replace an Autheona access token immediately if it is exposed.
- Keep Autheona API calls on the server.
- Never expose the Autheona access token to browser code.