Access tokens guard

Access tokens guard

Access tokens authenticate HTTP requests in API contexts where the server cannot persist cookies on the end-user device, for example, third-party access to an API or authentication for a mobile app.

Access tokens can be generated in any format; for instance, the tokens that conform to the JWT standard are known as JWT access tokens, and tokens in a proprietary format are known as opaque access tokens.

AdonisJS uses opaque access tokens that are structured and stored as follows.

  • A token is represented by a cryptographically secure random value suffixed with a CRC32 checksum.
  • A hash of the token value is persisted in the database. This hash is used to verify the token at the time of authentication.
  • The final token value is base64 encoded and prefixed with oat_. The prefix can be customized.
  • The prefix and the CRC32 checksum suffix help secret scanning tools identify a token and prevent them from leaking inside a codebase.

Configuring the user model

Before using the access tokens guard, you must set up a tokens provider with the User model. The tokens provider is used to create, list, and verify access tokens.

The auth package comes with a database tokens provider, which persists tokens inside an SQL database. You can configure it as follows.

import { BaseModel } from '@adonisjs/lucid/orm'
import { DbAccessTokensProvider } from '@adonisjs/auth/access_tokens'
export default class User extends BaseModel {
// ...rest of the model properties
static accessTokens = DbAccessTokensProvider.forModel(User)
}

The DbAccessTokensProvider.forModel accepts the User model as the first argument and an options object as the second argument.

export default class User extends BaseModel {
// ...rest of the model properties
static accessTokens = DbAccessTokensProvider.forModel(User, {
expiresIn: '30 days',
prefix: 'oat_',
table: 'auth_access_tokens',
type: 'auth_token',
tokenSecretLength: 40,
})
}

expiresIn

The duration after which the token will expire. You can pass a numeric value in seconds or a time expression as a string.

By default, tokens are long-lived and do not expire. Also, you can specify the expiry of a token at the time it is generated.

prefix

The prefix for the publicly shared token value. Defining a prefix helps secret scanning tools identify a token and prevent it from leaking inside the codebases.

Changing the prefix after issuing tokens will make them invalid. Therefore, choose the prefix carefully and do not change them often.

Defaults to oat_.

table

The database table name for storing the access tokens. Defaults to auth_access_tokens.

type

A unique type to identify a bucket of tokens. If you issue multiple types of tokens within a single application, you must define a unique type for all of them.

Defaults to auth_token.

tokenSecretLength

The length (in characters) of the random token value. Defaults to 40.


Once you have configured a token provider, you can start issuing tokens on behalf of a user. You do not have to set up an authentication guard for issuing tokens. The guard is needed to verify tokens.

Creating the access tokens database table

We create the migration file for the auth_access_tokens table during the initial setup. The migration file is stored inside the database/migrations directory.

You may create the database table by executing the migration:run command.

node ace migration:run

However, if you are configuring the auth package manually for some reason, you can create a migration file manually and copy-paste the following code snippet inside it.

node ace make:migration auth_access_tokens
import { BaseSchema } from '@adonisjs/lucid/schema'
export default class extends BaseSchema {
protected tableName = 'auth_access_tokens'
async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')
table
.integer('tokenable_id')
.notNullable()
.unsigned()
.references('id')
.inTable('users')
.onDelete('CASCADE')
table.string('type').notNullable()
table.string('name').nullable()
table.string('hash').notNullable()
table.text('abilities').notNullable()
table.timestamp('created_at')
table.timestamp('updated_at')
table.timestamp('last_used_at').nullable()
table.timestamp('expires_at').nullable()
})
}
async down() {
this.schema.dropTable(this.tableName)
}
}

Issuing tokens

Depending upon your application, you might issue a token during login or after login from the application dashboard. In either case, issuing a token requires a user object (for whom the token will be generated), and you can generate them directly using the User model.

In the following example, we find a user by id and issue them an access token using the User.accessTokens.create method. Of course, in a real-world application, you will have this endpoint guarded by authentication, but let's keep it simple for now.

The .create method accepts an instance of the User model and returns an instance of the AccessToken class.

The token.value property contains the value (wrapped as a Secret) that must be shared with the user. The value is only available when generating the token, and the user will not be able to see it again.

import router from '@adonisjs/core/services/router'
import User from '#models/user'
router.post('users/:id/tokens', ({ params }) => {
const user = await User.findOrFail(params.id)
const token = await User.accessTokens.create(user)
return {
type: 'bearer',
value: token.value!.release(),
}
})

You can also return the token directly in response, which will be serialized to the following JSON object.

router.post('users/:id/tokens', ({ params }) => {
const user = await User.findOrFail(params.id)
const token = await User.accessTokens.create(user)
return {
type: 'bearer',
value: token.value!.release(),
}
return token
})
/**
* response: {
* type: 'bearer',
* value: 'oat_MTA.aWFQUmo2WkQzd3M5cW0zeG5JeHdiaV9rOFQzUWM1aTZSR2xJaDZXYzM5MDE4MzA3NTU',
* expiresAt: null,
* }
*/

Defining abilities

Depending upon the application you are building, you might want to limit access tokens to only perform specific tasks. For example, issue a token that allows reading and listing projects without creating or deleting them.

In the following example, we define an array of abilities as the second parameter. The abilities are serialized to a JSON string and persisted inside the database.

For the auth package, the abilities have no real meaning. It is up to your application to check for token abilities before performing a given action.

await User.accessTokens.create(user, ['server:create', 'server:read'])

Token abilities vs. Bouncer abilities

You should not confuse token abilities with bouncer authorization checks. Let's try to understand the difference with a practical example.

  • Let's say you define a bouncer ability that allows admin users to create new projects.

  • The same admin user creates a token for themselves, but to prevent token abuse, they limit the token abilities to read projects.

  • Now, within your application, you will have to implement access control, which allows the admin users to create new projects while disallowing the token from creating new projects.

You can write a bouncer ability for this use case as follows.

The user.currentAccessToken refers to the access token used for authentication during the current HTTP request. You can learn more about it inside the authenticating requests section.

import { AccessToken } from '@adonisjs/auth/access_tokens'
import { Bouncer } from '@adonisjs/bouncer'
export const createProject = Bouncer.ability(
(user: User & { currentAccessToken?: AccessToken }) => {
/**
* If there is no "currentAccessToken" token property, it means
* the user authenticated without an access token
*/
if (!user.currentAccessToken) {
return user.isAdmin
}
/**
* Otherwise, check the user isAdmin and the token they
* used for authentication allows "project:create"
* ability.
*/
return user.isAdmin && user.currentAccessToken.allows('project:create')
}
)

Expiring tokens

By default, the tokens are long-lived, and they never expire. However, you define the expiration at the time of configuring the tokens provider or when generating a token.

The expiry can be defined as a numeric value representing seconds or a string-based time expression.

await User.accessTokens.create(
user, // for user
['*'], // with all abilities
{
expiresIn: '30 days' // expires in 30 days
}
)

Naming tokens

By default, the tokens are not named. However, you can assign them a name when generating the token. For example, if you allow the users of your application to self-generate tokens, you may ask them also to specify a recognizable name.

await User.accessTokens.create(
user,
['*'],
{
name: request.input('token_name'),
expiresIn: '30 days'
}
)

Configuring the guard

Now that we can issue tokens, let's configure an authentication guard to verify requests and authenticate users. The guard must be configured inside the config/auth.ts file under the guards object.

config/auth.ts
import { defineConfig } from '@adonisjs/auth'
import { tokensGuard, tokensUserProvider } from '@adonisjs/auth/access_tokens'
const authConfig = defineConfig({
default: 'api',
guards: {
api: tokensGuard({
provider: tokensUserProvider({
tokens: 'accessTokens',
model: () => import('#models/user'),
})
}),
},
})
export default authConfig

The tokensGuard method creates an instance of the AccessTokensGuard class. It accepts a user provider that can be used for verifying tokens and finding users.

The tokensUserProvider method accepts the following options and returns an instance of the AccessTokensLucidUserProvider class.

  • model: The Lucid model to use for finding users.
  • tokens: The static property name of the model to reference the tokens provider.

Authenticating requests

Once the guard has been configured, you can start authenticating requests using the auth middleware or manually calling the auth.authenticate method.

The auth.authenticate method returns an instance of the User model for the authenticated user, or it throws an E_UNAUTHORIZED_ACCESS exception when unable to authenticate the request.

import router from '@adonisjs/core/services/router'
router.post('projects', async ({ auth }) => {
// Authenticate using the default guard
const user = await auth.authenticate()
// Authenticate using a named guard
const user = await auth.authenticateUsing('api')
})

Using the auth middleware

Instead of manually calling the authenticate method. You can use the auth middleware to authenticate the request or throw an exception.

The auth middleware accepts an array of guards to use for authenticating the request. The authentication process stops after one of the mentioned guards authenticates the request.

import router from '@adonisjs/core/services/router'
import { middleware } from '#start/kernel'
router
.post('projects', async ({ auth }) => {
console.log(auth.user) // User
console.log(auth.authenticatedViaGuard) // 'api'
console.log(auth.user!.currentAccessToken) // AccessToken
})
.use(middleware.auth({
guards: ['api']
}))

Check if the request is authenticated

You can check if a request has been authenticated using the auth.isAuthenticated flag. The value of auth.user will always be defined for an authenticated request.

import { HttpContext } from '@adonisjs/core/http'
class PostsController {
async store({ auth }: HttpContext) {
if (auth.isAuthenticated) {
await auth.user!.related('posts').create(postData)
}
}
}

Get authenticated user or fail

If you do not like using the non-null assertion operator on the auth.user property, you may use the auth.getUserOrFail method. This method will return the user object or throw E_UNAUTHORIZED_ACCESS exception.

import { HttpContext } from '@adonisjs/core/http'
class PostsController {
async store({ auth }: HttpContext) {
const user = auth.getUserOrFail()
await user.related('posts').create(postData)
}
}

The current access token

The access token guard defines the currentAccessToken property on the user object after successfully authenticating the request. The currentAccessToken property is an instance of the AccessToken class.

You may use the currentAccessToken object to get the token's abilities or check the expiration of the token. Also, during authentication, the guard will update the last_used_at column to reflect the current timestamp.

If you reference the User model with currentAccessToken as a type in the rest of the codebase, you may want to declare this property on the model itself.

Instead of merging currentAccessToken

import { AccessToken } from '@adonisjs/auth/access_tokens'
Bouncer.ability((
user: User & { currentAccessToken?: AccessToken }
) => {
})

Declare it as a property on the model

import { AccessToken } from '@adonisjs/auth/access_tokens'
export default class User extends BaseModel {
currentAccessToken?: AccessToken
}
Bouncer.ability((user: User) => {
})

Listing all tokens

You may use the tokens provider to get a list of all the tokens using the accessTokens.all method. The return value will be an array of AccessToken class instances.

router
.get('/tokens', async ({ auth }) => {
return User.accessTokens.all(auth.user!)
})
.use(
middleware.auth({
guards: ['api'],
})
)

The all method also returns expired tokens. You may want to filter them before rendering the list or display a "Token expired" message next to the token. For example

@each(token in tokens)
<h2> {{ token.name }} </h2>
@if(token.isExpired())
<p> Expired </p>
@end
<p> Abilities: {{ token.abilities.join(',') }} </p>
@end

Deleting tokens

You may delete a token using the accessTokens.delete method. The method accepts the user as the first parameter and the token id as the second parameter.

await User.accessTokens.delete(user, token.identifier)

Events

Please check the events reference guide to view the list of available events emitted by the access tokens guard.