By default, AdonisJS Auth will be case-sensitive when checking your user’s UID value during login. This means, that if we allow our user’s to log in with their username, they’ll need to provide the username with the same casing they used when they signed up.
So long as you’re ensuring usernames within your application are case-insensitive unique, we can simplify things for our users by altering the Auth attempt call to be case-insensitive, allowing them to use their username with any casing combination. This comes in handy on mobile, tablets, and odd systems where the keyboard may default to the first character being capitalized.
If you’re new to AdonisJS authentication, you can get caught up in 15 minutes in our “AdonisJS Authentication in 15 Minutes” lesson.
Ensuring Usernames Are Unique Case-Insensitive
First, before we begin, let’s ensure we’re forcing all our usernames to be case-insensitive and unique within our database using the AdonisJS Validator.
const userSchema = schema.create({ username: schema.string( { trim: true }, [ rules.unique({ table: 'users', column: 'username', caseInsensitive: true // 👈 }) ] ), email: schema.string( { trim: true }, [ rules.email(), rules.unique({ table: 'users', column: 'email', caseInsensitive: true // 👈 }) ] ), password: schema.string({}, [rules.minLength(8)]) })
Copied!
Also, note that if you have a pre-existing database here, you’ll want to run a query to verify this won’t cause trouble for any existing users.
Making Auth Attempt Case-Insensitive
Now that we’ve verified that this change won’t impact any of our user’s abilities to log in or register, let’s go ahead and implement the ability to log in using a case-insensitive UID.
First, let’s head into our User model at App/Models/User.ts
. When authenticating a user, AdonisJS performs a check for a method on our User model, called findForAuth
, this check is shown below.
// AdonisJS' search for a findForAuth function if (typeof model.findForAuth === 'function') { const user = await model.findForAuth(this.config.uids, uidValue) return this.getUserFor(user) }
Copied!
If this method isn’t present on our User model, AdonisJS will perform an orWhere
for each UID defined within our config, shown below.
// the AdonisJS default const { query } = await this.getModelQuery() this.config.uids.forEach((uid) => query.orWhere(uid, uidValue)) return this.findUser(query)
Copied!
So, in order to make our UID search case-insensitive, we’ll want to define a method called findForAuth
on our User model. This method will be provided the uids
array from our config along with the login attempt’s uidValue
. then, inside our findForAuth
method we’ll want to create a query that’ll find our user.
public static async findForAuth(uids: string[], uidValue: string) { return this.query().where(query => uids.map(uid => query.orWhere(uid, 'ILIKE', uidValue)) ).firstOrFail() }
Copied!
By using an ILIKE
, we’re telling the query to perform a case-insensitive LIKE
check for the uidValue
. However, we aren’t adding any %
signs, so we’re essentially performing a case-insensitive equality check here which is exactly what we want.
With this in place, AdonisJS will now use the findForAuth
function we’ve defined on our User model when attempting to find a user during login. Which, we’ve defined to search for a UID match that’s case-insensitive. Job well done, everyone!
AdonisJS Lucid v18
If you’re running AdonisJS Lucid version 18 or later (the May 2022 release), there’s a new query builder method called whereILike
. You can swap the above query.orWhere(uid, 'ILIKE', uidValue)
for query.orWhereILike(uid, uidValue)
for a cleaner alternative.
TypeScript Is Complaining?
Though the above works just fine, you may run into TypeScript complaining about your findForAuth
method. There may be a better way to rectify this, however, I’ve found altering how we attach it to our User model resolves the TypeScript complaint.
First, switch your User model to export default at the end of the file.
class User extends BaseModel { // ... public static async findForAuth(uids, uidValue) { return this.query().where(query => uids.map(uid => query.orWhere(uid, 'ILIKE', uidValue)) ).firstOrFail() } } export default User
Copied!
Then, move your findForAuth
method declaration outside of the User
model.
class User extends BaseModel { // ... } User['findForAuth'] = function (uids, uidValue) { return this.query().where(query => uids.map(uid => query.orWhere(uid, 'ILIKE', uidValue)) ).firstOrFail() } export default User
Copied!
With that, TypeScript should no longer be complaining and our application will use a case-insensitive lookup on our user’s UID value when logging in.
Join The Discussion! (3 Comments)
Please sign in or sign up for free to join in on the dicussion.
mojtaba-rajabi
hi . thanks for excellent tutorials!
i added a custom findForAuth to my user class. i want check user is active after find user and befor send for password checking. i need a way to return exception in inertia errors props with username key but not any success:
in class User
Please sign in or sign up for free to reply
mojtaba-rajabi
Please sign in or sign up for free to reply
tomgobich
Hi mojtaba-rajabi! I wouldn't recommend trying to redirect the user from inside a method on the model.
There's a high probability the redirect would get forgotten about should you ever need to debug
You'd still need to terminate the request inside the controller method/route handler.
If this is for AdonisJS 6, what I would recommend instead is to let the framework do its thing. The
findForAuth
method is called by theverifyCredentials
method. This method purposefully hashes the provided password, regardless if a user is found or not, so that there isn't a timing difference between the two. Reducing the ability for a bad actor to scrape your login form for credentials &/or valid emails using a timing attack.This
verifyCredentials
method then throws an exception if the login is wrong, which you can capture and display on the frontend. This exception is available via theerrorsBag
flash message. That covers yourif (!user)
check.Then, I would let
verifyCredentials
return back the user if one is found, regardless of theisActive
flag and instead check thatisActive
flag after verifying the credentials. That way:You can keep timing attack protections AdonisJS set for you in place
You know the user is correct, since they know their credentials, so you can guide them (if applicable) on how to activate their account.
You don't have any sneaky code doing things in places you might not expect a year or two down the road.
You can then easily pass along whatever message you'd like via Inertia using flash messaging.
Hope this helps!
Please sign in or sign up for free to reply