How To Make Your AdonisJS Authentication Login Case-Insensitive

Learn how to make the AdonisJS login attempt query case-insensitive easily by adding a simple method to our User Model.

Published
May 28, 22
Duration
6m 0s

Developer, dog lover, and burrito eater. Currently teaching AdonisJS, a fully featured NodeJS framework, and running Adocasts where I post new lessons weekly. Professionally, I work with JavaScript, .Net C#, and SQL Server.

Adocasts

Burlington, KY

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.

  1. Commented 7 days ago

    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

    1

    Please sign in or sign up for free to reply

  2. Commented 7 days ago
    export default class User extends compose(BaseModel, AuthFinder) {
      public static async findForAuth(identifier: any, value: any) {
    
        const field = /^\d+$/.test(value) ? 'phone' : 'username'
        const user= await User.query().where(field, value).first()
       if (!user) {
            👈 fill inertia errors.username prop   and redirect back to login page
       }
      if(!user.isActive){
        👈 fill inertia errors.username prop with user is inactive and redirect back to login page
      }
    return user
    }
    Copied!
    0

    Please sign in or sign up for free to reply

    1. Commented 4 days ago

      Hi mojtaba-rajabi! I wouldn't recommend trying to redirect the user from inside a method on the model.

      1. There's a high probability the redirect would get forgotten about should you ever need to debug

      2. 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 the verifyCredentials 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 the errorsBag flash message. That covers your if (!user) check.

      Then, I would let verifyCredentials return back the user if one is found, regardless of the isActive flag and instead check that isActive 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.

      public async login({ request, response, session, auth }: HttpContex) {
        const { uid, password } = await request.validateUsing(loginValidator)
        const user = await User.verifyCredentials(uid, password)
      
        if (!user.isActive) {
          session.flash('error', 'Please activate your account')
          return response.redirect().back()
        }
      
        await auth.use('web').login(user)
      
        return response.redirect().toRoute('dashboard')
      }
      Copied!

      Hope this helps!

      0

      Please sign in or sign up for free to reply