API tokens

The API guard uses the database backed opaque access token to authenticate the user requests. You may want to use the api guard when creating an API that should be accessed by a third-party client, or for any other system that does not support cookies.

Tokens storage

The API tokens guard allows you store tokens either in a SQL database or store them inside Redis. Both the storage options have their own use cases.

SQL storage

The SQL storage is suited when api tokens are not primary mode of authentication. For example: You may want to allow the users of your application to create personal access tokens (just like the way github does) and authenticate the API requests using that.

In this scenario, you will not generate too many tokens in bulk and also most of the tokens will live forever.

Configuration for tokens is managed inside the config/auth.ts file under the guard config object.

{
api: {
driver: 'oat',
tokenProvider: {
type: 'api',
driver: 'database',
table: 'api_tokens',
foreignKey: 'user_id',
},
}
}

type

The type property holds the type of the token you are generating. Make sure to give it a unique name when you have multiple API token guards in use.

The unique name ensures that two guards generating the token for the same user does not have overlap or any conflicts.


driver

The name of the driver. It will always be database when storing the tokens inside a SQL table.


table

The database table to use for storing the tokens. During the initial setup process, AdonisJS will create the migration file for the tokens table. However, you can also create a migration manually and copy the contents from the stub file


foreignKey

The foreign key to build the relationship between the user and the token. Later, this will allow you to also list all the tokens for a given user.


Redis storage

The redis storage is suitable when API tokens are the primary mode of authentication. For example: You authenticate the requests from your mobile app using token based authentication.

In this scenario, you would also want tokens to expire after a given period of time and redis can automatically clear the expired tokens from its storage.

Configuration for tokens is managed inside the config/auth.ts file under the guard config object.

{
api: {
driver: 'oat',
tokenProvider: {
type: 'api',
driver: 'redis',
redisConnection: 'local',
foreignKey: 'user_id',
},
}
}

type

The type property holds the type of the token you are generating. Make sure to give it a unique name when you have multiple API token guards in use.

The unique name ensures that two guards generating the token for the same user does not have overlap or any conflicts.


driver

The name of the driver. It will always be redis when storing the tokens in a redis database.


redisConnection

Reference to a connection defined inside the config/redis.ts file. Make sure to read the redis guide for the initial setup.


foreignKey

The foreign key to build the relationship between the user and the token.


Generating tokens

You can generate an API token for a user using the auth.generate or the auth.attempt method. The auth.attempt method lookup the user from the database and verifies their password.

import Route from '@ioc:Adonis/Core/Auth'
Route.post('login', async ({ auth, request, response }) => {
const email = request.input('email')
const password = request.input('password')
try {
const token = await auth.use('api').attempt(email, password)
return token
} catch {
return response.badRequest('Invalid credentials')
}
})

You can either manually handle the exception and return a response, or let the exception handle itself and create a response using content negotiation .


auth.generate

If the auth.attempt lookup strategy does not fit your use case, then you can manually lookup the user, verify their password and call the auth.generate method to generate a token for them.

The auth.login method is an alias for the auth.generate method.

import User from 'App/Models/User'
import Route from '@ioc:Adonis/Core/Auth'
import Hash from '@ioc:Adonis/Core/Hash'
Route.post('login', async ({ auth, request, response }) => {
const email = request.input('email')
const password = request.input('password')
// Lookup user manually
const user = await User
.query()
.where('email', email)
.where('tenant_id', getTenantIdFromSomewhere)
.whereNull('is_deleted')
.firstOrFail()
// Verify password
if (!(await Hash.verify(user.password, password))) {
return response.badRequest('Invalid credentials')
}
// Generate token
const token = await auth.use('api').generate(user)
})

Managing tokens expiry

You can also define the expiry for the token at the time of the generating it.

await auth.use('api').attempt(email, password, {
expiresIn: '7days'
})
await auth.use('api').generate(user, {
expiresIn: '30mins'
})

The redis driver will automatically delete the expired tokens. However, for SQL storage, you will have write a custom script and delete token with expires_at timestamp smaller than today.

Token properties

Following is the list of properties on the token object generated using the auth.generate method.

type

The token is always set to 'bearer'.


user

The user for which the token was generated. The value of the user relies on the underlying user provider used by the guard.


expiresAt

An instance of the luxon Datetime representing a static time at which the token will expire. Only exists, if have explicitly defined the expiry for the token.


expiresIn

Time in seconds after which the token will expire. It is a static value and does not change as the time passes by.


meta

Any meta data attached with the token. You can define the meta data in the options object at the time of generating the token.

The underlying storage drivers will persist the meta data to the database. In case of SQL, make sure to also create the required columns.

await auth.use('api').attempt(email, password, {
ip_address: '192.168.1.0'
})

name

The name to associate with the token. This is usually helpful when you allow the users of your application to generate personal access tokens (just like the way Github does) and give them a memorable name.

The name property only exists, when you have defined it at the time of generating the token.

await auth.use('api').attempt(email, password, {
name: 'For the CLI app'
})

token

The value for the generated token. You must share this value with the client and the client must store it securely.

You cannot get access to this value later, as the value stored inside the database is a hash of the token that cannot be converted to a plain value.


tokenHash

The value stored inside the database. Make sure to never share the token hash with the client.

During the auth.authenticate request, we will compare the value provided by the client against the token hash.


toJSON

Converts the token to an object that you can send back in response to a request. The toJSON method contains the following properties.

{
type: 'bearer',
token: 'the-token-value',
expires_at: '2021-04-28T17:43:37.235+05:30'
expires_in: 604800
}

Authenticate subsequent requests

Once the client receives the API token, they must send it back on every HTTP request under the Authorization header. The header must be formatted as follows:

Authorization = Bearer TOKEN_VALUE

You can verify if the token is valid or not using the auth.authenticate method. The AuthenticationException is raised, if the token is invalid or the user does not exists inside the database.

Otherwise, you can access the logged-in user using the auth.user property.

import Route from '@ioc:Adonis/Core/Auth'
Route.get('dashboard', async ({ auth }) => {
await auth.use('api').authenticate()
console.log(auth.use('api').user!)
})

Calling this method manually inside every single route is not practical and hence you can make use of the auth middleware stored inside the ./app/Middleware/Auth.ts file.

Revoke tokens

During the logout phase, you can revoke the token by deleting it from the database. The token again must be sent under the Authorization header.

The auth.revoke method will remove the token sent during the current request from the database.

import Route from '@ioc:Adonis/Core/Auth'
Route.post('/logout', async ({ auth, response }) => {
await auth.use('api').revoke()
return {
revoked: true
}
})

Other methods/properties

Following is the list of available methods/properties available for the api guard.

isLoggedIn

Find if user is logged in or not. The value is true right after calling the auth.generate method or when the auth.authenticate check passes.

await auth.use('api').authenticate()
auth.use('api').isLoggedIn // true
await auth.use('api').attempt(email, password)
auth.use('api').isLoggedIn // true

isGuest

Find if the user is a guest (meaning not logged in). The value is always the opposite of the isLoggedIn flag.


isAuthenticated

Find if the current request has passed the authentication check. This flag is different from the isLoggedIn flag and NOT set to true during the auth.login call.

await auth.use('api').authenticate()
auth.use('api').isAuthenticated // true
await auth.use('api').attempt(email, password)
auth.use('api').isAuthenticated // false

isLoggedOut

Find if the token was revoked during the current request. The value will be true right after calling the auth.revoke method.

await auth.use('api').revoke()
auth.use('api').isLoggedOut

authenticationAttempted

Find if an attempt to authenticate the current request has been made. The value is set to true when you call the auth.authenticate method

auth.use('api').authenticationAttempted // false
await auth.use('api').authenticate()
auth.use('api').authenticationAttempted // true

provider

Reference to the underlying user provider used by the guard.


tokenProvider

Reference to the underlying token provider used by the guard.


verifyCredentials

A method to verify the user credentials. The auth.attempt method uses this method under the hood. The InvalidCredentialsException exception is raised when the credentials are invalid.

try {
await auth.use('api').verifyCredentials(email, password)
} catch (error) {
console.log(error)
}

check

The method is same as the auth.authenticate method. However, it does not raise any exception when the request is not authenticated. Think of it as an optional attempt to check if the token is valid for the current request or not.

await auth.use('api').check()
if (auth.use('api').isLoggedIn) {
}