In the last post, we setup Kysely inside an AdonisJS application with commands to make, run, and rollback migrations. In this post, we will take a step ahead and integrate Kysely with the AdonisJS auth package.
If you want to follow along, I recommend first reading the Kysely setup post.
Installing the Auth package
The first step is to install and configure the Auth package. Since we will create a provider for the session guard, you must configure the package with the --guard=session
flag.
The session guard requires the @adonisjs/session
package. So make sure to configure the session package as well.
npm i @adonisjs/auth
node ace configure @adonisjs/auth --guard=session
The initial setup process assumes your application uses Lucid and creates the User
model and the users
table migration files. You will have to manually delete these files.
rm database/migrations/<file_create_by_auth_package>
rm app/models/user.ts
Creating a User provider for the session guard
Let's start by creating the User provider for the session guard. We will store it inside the app/auth_providers
directory.
mkdir app/auth_providers
touch app/auth_providers/session_user_provider.ts
Open the newly created file and paste the following code inside it. The first step is to create a class that implements the SessionUserProviderContract interface.
import type { Users } from '../../types/db.js'
import { symbols } from '@adonisjs/auth'
import { SessionUserProviderContract } from '@adonisjs/auth/types/session'
export class SessionKyselyUserProvider implements SessionUserProviderContract<Users> {
declare [symbols.PROVIDER_REAL_USER]: Users
}
-
The
SessionUserProviderContract
interface needs a generic User property your guard will accept and return when interacting with the session guard. This generic property adds type safety to your codebase. -
We read the
Users
property from thetypes/db.ts
file. This file is created using the kysely-codegen CLI. -
The
symbols.PROVIDER_REAL_USER
property is used by the event emitter to add type information to the events emitted by the session guard.
Implementing the createUserForGuard method
The createUserForGuard
method is a Bridge (or Adapter) between the session guard and your provider. Since the guard is user-agnostic, it needs this adapter to fetch the user's unique ID and store it inside the session.
import type { Users } from '../../types/db.js'
import { symbols } from '@adonisjs/auth'
import {
SessionGuardUser,
SessionUserProviderContract
} from '@adonisjs/auth/types/session'
export class SessionKyselyUserProvider implements SessionUserProviderContract<Users> {
declare [symbols.PROVIDER_REAL_USER]: Users
async createUserForGuard(user: Users): Promise<SessionGuardUser<Users>> {
return {
getId() {
return user.id
},
getOriginal() {
return user
},
}
}
}
The createUserForGuard
must return an object with the following two properties.
getId
: The unique ID for the user. Usually, it will be the primary key from the database.getOriginal
: Reference to the user fetched from the database. The return value should satisfy the genericUser
property defined on theSessionUserProviderContract
interface.
Implementing the findById method
The findById
method is responsible for finding the user by their ID during authentication. This method should return null
when the user does not exist or return the SessionGuardUser
object.
import { db } from '#services/db'
import type { Users } from '../../types/db.js'
import { symbols } from '@adonisjs/auth'
import {
SessionGuardUser,
SessionUserProviderContract
} from '@adonisjs/auth/types/session'
export class SessionKyselyUserProvider implements SessionUserProviderContract<Users> {
declare [symbols.PROVIDER_REAL_USER]: Users
async createUserForGuard(user: Users): Promise<SessionGuardUser<Users>> {
return {
getId() {
return user.id
},
getOriginal() {
return user
},
}
}
async findById(identifier: number): Promise<SessionGuardUser<Users> | null> {
const user = await db
.selectFrom('users')
.selectAll()
.where('id', '=', identifier)
.executeTakeFirst()
if (!user) {
return null
}
return this.createUserForGuard(user)
}
}
Configuring the provider inside the config file
As the last step, we must register the SessionKyselyUserProvider
within the config/auth.ts
file. By default, the config file uses the sessionUserProvider
helper from the session package. So, we will have to replace this with our implementation.
import { defineConfig } from '@adonisjs/auth'
import { configProvider } from '@adonisjs/core'
import { sessionGuard } from '@adonisjs/auth/session'
import { InferAuthEvents, Authenticators } from '@adonisjs/auth/types'
const authConfig = defineConfig({
default: 'web',
guards: {
web: sessionGuard({
useRememberMeTokens: false,
provider: sessionUserProvider({
model: () => import('#models/user'),
})
provider: configProvider.create(async () => {
const { SessionKyselyUserProvider } = await import(
'../app/auth_providers/session_user_provider.js'
)
return new SessionKyselyUserProvider()
}),
}),
},
})
As you notice, we do not import the SessionKyselyUserProvider
at the top-level of the config file and instead lazy import it using the configProvider.create
method.
This is because AdonisJS loads the config files at the start of the application, and during that time, your app is not fully ready to interact with other parts of the codebase. You might encounter errors if you import application-level code inside a config file.
Therefore, as a general principle, never import application code inside config files.
Creating a user
Before we can test our implementation, we need a user inside the database. So let's create one using the AdonisJS REPL.
node ace repl
// Type ".ls" to view a list of available context methods/properties
// > (js)
await import('#services/db')
db = _.db
await loadHash()
After importing the hash
and the db
services, you can get into the editor mode by typing .editor
in the REPL and copy-paste the following code.
// .editor
// Entering editor mode (Ctrl+D to finish, Ctrl+C to cancel)
await db
.insertInto('users')
.values({
email: 'virk@adonisjs.com',
password: await hash.make('secret'),
full_name: 'Harminder Virk',
created_at: new Date().getTime(),
updated_at: new Date().getTime()
})
.execute()
Finally, press Ctrl+D
to execute the code and exit the editor mode.
Time for the test drive
Now that we have created and registered the User provider with the auth config, we are ready to test-drive it. Let's create the following three routes.
-
The root route is protected using the
auth
middleware and will disallow unauthenticated requests. -
The
/login
route displays the Login form and uses theguest
route to disallow logged-in users from accessing this page. -
The
POST /login
route will be responsible for creating the authentication session for the user.
import { middleware } from '#start/kernel'
import router from '@adonisjs/core/services/router'
router.on('/').render('pages/home').use(middleware.auth())
router.on('/login').render('pages/login').use(middleware.guest())
router.post('login', async ({ auth, request, response }) => {
// Will implement the logic for authentication here.
})
import { db } from '#services/db'
import { errors } from '@adonisjs/auth'
import hash from '@adonisjs/core/services/hash'
router.post('login', async ({ auth, request, response }) => {
/**
* Step 1: Throw an error when the user has not provided the email
* or the password
*/
const { email, password } = request.only(['email', 'password'])
if (!email || !password) {
throw new errors.E_INVALID_CREDENTIALS('Invalid credentials')
}
const user = await db
.selectFrom('users')
.selectAll()
.where('email', '=', email)
.executeTakeFirst()
/**
* Step 2: Throw an error when we are not able to find the user
* by their email address
*/
if (!user) {
throw new errors.E_INVALID_CREDENTIALS('Invalid credentials')
}
/**
* Step 3: Throw an error when the password is incorrect
*/
const hasValidPassword = await hash.verify(user.password, password)
if (!hasValidPassword) {
throw new errors.E_INVALID_CREDENTIALS('Invalid credentials')
}
/**
* Step 4: Finally, log in the user and redirect them to the
* homepage.
*/
await auth.use('web').login(user)
response.redirect('/')
})
Since we are not using Lucid models, we cannot abstract the logic of verifying user credentials to a model method. Instead, we have to write the inline code within the route handler.
-
First, we ensure the user has supplied the
email
andpassword
. Otherwise, we should throw the E_INVALID_CREDENTIALS exception. -
Next, we query the database with the user's email and throw an
E_INVALID_CREDENTIALS
exception when the user does not exist. -
Next, we verify the user's password with the hash saved inside the database.
-
Finally, we log in using the session guard and redirect the user to the home page.
Creating the home page
Create the resources/views/pages/home.edge
file and copy-paste the following code inside it.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Protected area </title>
</head>
<body>
<h1> Hello {{ auth.user.full_name }}! </h1>
<p>You are logged in as {{ auth.user.email }}</p>
</body>
</html>
Creating the login page
Create the resources/views/pages/login.edge
file and copy-paste the following code inside it.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title> Login </title>
</head>
<body>
@flashMessage('errorsBag')
@each(error in $message)
<p>{{ error }}</p>
@end
@end
<form action="/login" method="post">
<div>
<label for="email"> Email </label>
<input type="email" name="email" id="email">
</div>
<div>
<label for="password"> Password </label>
<input type="password" name="password" id="password">
</div>
<div>
<button type="submit"> Login </button>
</div>
</form>
</body>
</html>
Conclusion
In this post, we create a custom user provider for the session guard. Even though we are using Kysely in this example, you can replace the code specific to Kysely with the ORM of your choice.
All you have to do is create a JavaScript class that implements SessionUserProviderContract.
import { symbols } from '@adonisjs/auth'
import { SessionUserProviderContract } from '@adonisjs/auth/types/session'
/**
* The user type your provider will return
*/
export type User = {}
export class MySessionUserProvider implements SessionUserProviderContract<Users> {
declare [symbols.PROVIDER_REAL_USER]: User
/**
* Create the user adapter that the guard can use
* to fetch the ID.
*/
async createUserForGuard(user: Users): Promise<SessionGuardUser<Users>> {}
/**
* Find a user by their ID
*/
async findById(identifier: number): Promise<SessionGuardUser<Users> | null> {}
}