Usage

Usage

Once the package has been configured, you can interact with 2FA API's using the service to create Secret and Recovery Codes to the user and validate the OTP (One-time password).

All the examples below will be using Lucid as database store, and Adonis Auth as a way to retrieve the authenticated user. If that is not your case, you can use the examples as a guide to make it in your own situation.

Creating a user Secret

The very first step is to create a Secret to user.

The Secret is an object containing a 32-character secret (keep user specific), a uri (if you want to make your own QR / barcode) and a direct link to a QR code served via HTTPS by the Google Chart API.

To create that we should call the generateSecret method passing some user information, like an email or username, that will also show up in the user's authenticator app.

import type { HttpContext } from '@adonisjs/core/http'
import twoFactorAuth from '@nulix/adonis-2fa/services/main'
export default class TwoFactorAuthController {
async generate({ auth }: HttpContext) {
const user = auth.user!
user.twoFactorSecret = twoFactorAuth.generateSecret(user.email)
/*
{
secret: 'XDQXYCP5AC6FA32FQXDGJSPBIDYNKK5W',
uri: 'otpauth://totp/My%20Awesome%20App:johndoe@test.com?secret=XDQXYCP5AC6FA32FQXDGJSPBIDYNKK5W&issuer=My%20Awesome%20App',
qr: 'https://chart.googleapis.com/chart?chs=166x166&chld=L|0&cht=qr&chl=otpauth://totp/My%20Awesome%20App:johndoe%3Fsecret=XDQXYCP5AC6FA32FQXDGJSPBIDYNKK5W%26issuer=My%20Awesome%20App'
}
*/
await user.save()
return user.twoFactorSecret
}
}

As a good practice, you should store that object encrypted in the database. You can use the Adonis encryption service to encrypt and decrypt.

If you are using Lucid, you can automate this process in your User model for example:

// ...other imports
import encryption from '@adonisjs/core/services/encryption'
import { TwoFactorSecret } from '@nulix/adonis-2fa/types'
export default class User extends compose(BaseModel, AuthFinder) {
// ...other user columns
@column({
consume: (value: string) => (value ? encryption.decrypt(value) : null),
prepare: (value: string) => encryption.encrypt(value),
})
declare twoFactorSecret: TwoFactorSecret | null
}

Generate Recovery Codes

If you don't know what is a recovery code, or why are they important, please read this article. Simply put, in case that your user lost their authenticator app, they can user the recovery code to access your project and maybe create another Secret.

To create them we should call the generateRecoveryCodes method. As default, it will create 16 codes. If you want to change that, you can pass the number as an argument of the method.

import type { HttpContext } from '@adonisjs/core/http'
import twoFactorAuth from '@nulix/adonis-2fa/services/main'
export default class TwoFactorAuthController {
async generateRecoveryCodes({ auth }: HttpContext) {
const user = auth.user!
user.twoFactorRecoveryCodes = twoFactorAuth.generateRecoveryCodes() // or .generateRecoveryCodes(32)
// ['XMCIM 5WAGK', 'MYM50 GHZJW', 'YWCHF 0TWRE', ...]
await user.save()
return { recovery_codes: user.twoFactorRecoveryCodes }
}
}

You should store that array encrypted as well in the database just like the Secret. User model example:

// ...other imports
import encryption from '@adonisjs/core/services/encryption'
export default class User extends compose(BaseModel, AuthFinder) {
// ...other user columns
@column({
consume: (value: string) => (value ? encryption.decrypt(value) : []),
prepare: (value: string[]) => encryption.encrypt(value),
})
declare twoFactorRecoveryCodes: string[]
}

Verify OTP

To verify the OTP (One-time password) we should use the verifyToken method passing the user 32-character secret, the otp string that he is trying to verify and his array of recovery codes.

import type { HttpContext } from '@adonisjs/core/http'
import twoFactorAuth from '@nulix/adonis-2fa/services/main'
export default class TwoFactorAuthController {
async verify({ auth, request, response }: HttpContext) {
const otp = await request.input('otp')
const user = auth.user!
const isValid = twoFactorAuth.verifyToken(
user.twoFactorSecret?.secret,
otp,
user.twoFactorRecoveryCodes
)
if (!isValid) {
return response.badRequest({ message: 'OTP invalid' })
}
return response.ok({ message: 'OTP valid' })
}
}

This method will try to verify if the otp is valid to the user secret, or if the recovery codes includes the otp.