Custom user provider
User providers are used for looking up a user for authentication. The auth module ships with a Database provider and a Lucid provider to look up users from a SQL database using the Lucid ORM.
You can also extend the Auth module and add custom User providers if you want to look up users from a different data source. In this guide, we will go through the process of adding a custom user provider.
Here is an example project using mongoose to lookup users from a MongoDB database. You can use it as an inspiration to create your own provider.
Extending from outside in
Anytime you are extending the core of the framework. It is better to assume that you do not have access to the application code and its dependencies. In other words, write your extensions as if you are writing a third-party package and use dependency injection to rely on other dependencies.
Let's begin by creating a user provider that relies on a MongoDB client to look up the users from the database. The following examples use dummy code for the MongoDB queries, and you must replace them with your own implementation.
mkdir providers/MongoDbAuthProvider
touch providers/MongoDbAuthProvider/index.ts
The directory structure will look as follows.
providers
└── MongoDbAuthProvider
└── index.ts
Open the newly created MongoDbAuthProvider/index.ts
file and paste the following code inside it.
import type { HashContract } from '@ioc:Adonis/Core/Hash'
import type {
UserProviderContract,
ProviderUserContract
} from '@ioc:Adonis/Addons/Auth'
/**
* Shape of the user object returned by the "MongoDbAuthProvider"
* class. Feel free to change the properties as you want
*/
export type User = {
id: string
email: string
password: string
rememberMeToken: string | null
}
/**
* The shape of configuration accepted by the MongoDbAuthProvider.
* At a bare minimum, it needs a driver property
*/
export type MongoDbAuthProviderConfig = {
driver: 'mongo'
}
/**
* Provider user works as a bridge between your User provider and
* the AdonisJS auth module.
*/
class ProviderUser implements ProviderUserContract<User> {
constructor(public user: User | null, private hash: HashContract) {}
public getId() {
return this.user ? this.user.id : null
}
public getRememberMeToken() {
return this.user ? this.user.rememberMeToken : null
}
public setRememberMeToken(token: string) {
if (!this.user) {
return
}
this.user.rememberMeToken = token
}
public async verifyPassword(plainPassword: string) {
if (!this.user) {
throw new Error('Cannot verify password for non-existing user')
}
return this.hash.verify(this.user.password, plainPassword)
}
}
/**
* The User provider implementation to lookup a user for different
* operations
*/
export class MongoDbAuthProvider implements UserProviderContract<User> {
constructor(
public config: MongoDbAuthProviderConfig,
private hash: HashContract
) {}
public async getUserFor(user: User | null) {
return new ProviderUser(user, this.hash)
}
public async updateRememberMeToken(user: ProviderUser) {
await mongoDbClient.updateOne(
{ _id: user.getId() },
{ rememberMeToken: user.getRememberMeToken() }
)
}
public async findById(id: string | number) {
const user = await mongoDbClient.findById(id)
return this.getUserFor(user || null)
}
public async findByUid(uidValue: string) {
const user = await mongoDbClient.findOne().where('email').equals(uidValue)
return this.getUserFor(user || null)
}
public async findByRememberMeToken(userId: string | number, token: string) {
const user = await mongoDbClient
.findOne()
.where('_id').equals(userId)
.where('rememberMeToken').equals(token)
return this.getUserFor(user || null)
}
}
That's a lot of code, so let's break it down and understand the purpose of each class and its methods.
User type
The export type User
block defines the shape of the user your provider will return. If you are using an ORM, you can infer the User type from some model, but the main goal is to have a pre-defined representation of a user.
export type User = {
id: string
email: string
password: string
rememberMeToken: string | null
}
MongoDb Provider config
The MongoDbAuthProviderConfig
type defines the shape of the config accepted by your provider. It must have at least the driver
property reflecting the driver's name you want to register with the auth module.
export type MongoDbAuthProviderConfig = {
driver: 'mongo'
}
ProviderUser class
The ProviderUser
class is a bridge between your UserProvider and the AdonisJS auth module. The auth module does not know the properties that exist on the user object you fetched from a data source, and hence it needs some way to lookup the id or verify the user password.
This is where the ProviderUser
class comes into the picture. It must implement the ProviderUserContract
interface.
We also accept the hash
module as a constructor argument to verify the user passwords using the AdonisJS hash module
.
class ProviderUser implements ProviderUserContract<User> {
constructor(public user: User | null, private hash: HashContract) {}
public getId() {
return this.user ? this.user.id : null
}
public getRememberMeToken() {
return this.user ? this.user.rememberMeToken : null
}
public setRememberMeToken(token: string) {
if (!this.user) {
return
}
this.user.rememberMeToken = token
}
public async verifyPassword(plainPassword: string) {
if (!this.user) {
throw new Error('Cannot verify password for non-existing user')
}
return this.hash.verify(this.user.password, plainPassword)
}
}
getId
Return the value for a property that uniquely identifies the user.
getRememberMeToken
Return the value of the remember me token. It must be a string or null
when no remember me token was generated.
setRememberMeToken
Get the remember me token property on the user object. Do note; you do not persist the remember me token inside this method. You just update the property.
The token is persisted by the updateRememberMeToken
on the UserProvider
class.
verifyPassword
Verify the user password. This method receives the plain text password from the auth module and must return true
if the password matches or false
if the password is incorrect.
UserProvider class
The UserProvider
class is used to look up a user or persist the remember me token for a given user. This is the class that you will later register with the AdonisJS auth module.
The UserProvider
must implement the UserProviderContract
interface.
export class MongoDbAuthProvider implements UserProviderContract<User> {
constructor(
public config: MongoDbAuthProviderConfig,
private hash: HashContract
) {}
public async getUserFor(user: User | null) {
return new ProviderUser(user, this.hash)
}
public async updateRememberMeToken(user: ProviderUser) {
await mongoDbClient.updateOne(
{ _id: user.getId() },
{ rememberMeToken: user.getRememberMeToken() }
)
}
public async findById(id: string | number) {
const user = await mongoDbClient.findById(id)
return this.getUserFor(user || null)
}
public async findByUid(uidValue: string) {
const user = await mongoDbClient.findOne().where('email').equals(uidValue)
return this.getUserFor(user || null)
}
public async findByRememberMeToken(userId: string | number, token: string) {
const user = await mongoDbClient
.findOne()
.where('_id').equals(userId)
.where('rememberMeToken').equals(token)
return this.getUserFor(user || null)
}
}
getUserFor
Returns a ProviderUser instance for the user object you lookup from a data source.
updateRememberMeToken
Update the data source with the new remember me token. This method receives an instance of the ProviderUser
class with the update rememberMeToken
property.
findById
Find a user by their unique id. This method must return an instance of the ProviderUser class.
findByUid
Find a user for login by using either their email address or username or any other property applicable to your data source.
For example, The Lucid provider relies on the config to look up a user by uid.
This method must return an instance of the ProviderUser class.
findByRememberMeToken
Find a user by their remember me token. The method receives the user id, and the remember me token both.
This method must return an instance of the ProviderUser class.
Registering the User provider
The next step is to register the User provider with the auth module. You must do it inside the provider's boot
method. For this example, we will make use of the providers/AppProvider.ts
file.
// title providers/AppProvider.ts
import { ApplicationContract } from '@ioc:Adonis/Core/Application'
export default class AppProvider {
constructor(protected app: ApplicationContract) {}
public async boot() {
const Auth = this.app.container.resolveBinding('Adonis/Addons/Auth')
const Hash = this.app.container.resolveBinding('Adonis/Core/Hash')
const { MongoDbAuthProvider } = await import('./MongoDbAuthProvider')
Auth.extend('provider', 'mongo', (_, __, config) => {
return new MongoDbAuthProvider(config, Hash)
})
}
}
The Auth.extend
method accepts a total of three arguments.
- The type of extension. It should always be set to
provider
when adding a user provider. - The name of the provider
- And finally, a callback that returns an instance of the User provider. The callback method receives the
config
as the 3rd argument.
Update types and config
Before you can start using the mongo
provider, you will have to define it inside the contract file and define its config.
Open the contracts/auth.ts
file and append the following code snippet inside it.
import type {
MongoDbAuthProvider,
MongoDbAuthProviderConfig,
} from '../providers/MongoDbAuthProvider'
declare module '@ioc:Adonis/Addons/Auth' {
interface ProvidersList {
user: {
implementation: MongoDbAuthProvider
config: MongoDbAuthProviderConfig
}
}
interface GuardsList {
web: {
implementation: SessionGuardContract<'user', 'web'>
config: SessionGuardConfig<'user'>
}
}
}
Finally, let's update the config/auth.ts
file and define the config for the user provider.
const authConfig: AuthConfig = {
guard: 'web',
guards: {
web: {
driver: 'session',
provider: {
driver: 'mongo'
}
}
}
}