Generate new jwt token after its expiration

Hi, could anyone assist me with renewing jwt token?

This is my scenario…

Inside auth.js I set up authenticator to this:

jwt: {
serializer: ‘lucid’,
model: ‘User’,
scheme: ‘jwt’,
uid: ‘username’,
password: ‘password’,
options: {
secret: Config.get(‘app.appKey’),
expiresIn: ‘30m’
},
},

One particular route has this stack of middlewares:

Route.get(’/profile’, ‘UserController.me’).middleware([‘auth:jwt’, ‘hasToken’])

Inside login action in UserController I have this at the end:


const token = await auth.withRefreshToken().generate(user,
{
username: user.username,
userType: user.user_type
})

return response.ok({
  token: token.token,
  refresh_token: token.refreshToken,
  user
})

Where should I renew jwt token after its expiration.
HasToken middleware looks like this:

‘use strict’

class HasToken {
async handle({ request, response, auth }, next) {
try {
await auth.current.getAuthHeader()
} catch (error) {
return response.badRequest(error)
}

await next()

}
}
module.exports = HasToken

There I check for existence of Authorization header but I cannot renew jwt token there, auth:jwt stops me. I cannot do it in a controller either.
I’m creating api-only app.

E_JWT_TOKEN_EXPIRED: The jwt token has been expired. Generate a new one to continue" - where do I generate it without logging users out?

I would really appreciate some help with this. Thanks.

1 Like

You should handle JWT creation and renewal apart from your application. The JWT generation process is like logging in a user, it has nothing to do with his profile. See my approach (shortened):

class AuthController {

    async login({ request, auth }) {
        const { email, password } = request.all()

        const token = await auth
            .withRefreshToken()
            .attempt(email, password)

        return token
    }

    async refresh({request, auth}) {
        // not implemented yet, but would be something like ...
        const refreshToken = request.input('refresh_token')

        return await auth.generateForRefreshToken(refreshToken)
    }

    async getUser({ response, auth }) {
        try {
            const user = await auth.getUser()

            // I personally do not want to return the full user object here
            return {
                id: user.id,
                forename: user.forename,
                lastname: user.lastname,
                email: user.email
            }
        } catch (error) {
            response.status(401).send(error)
        }
    }
}

routes:

// logged in users
Route.group(() => {
    Route.resource('users', 'UserController').apiOnly() // some example resource
})
.prefix('api')
.middleware(['auth'])

// auth stuff
Route.group(() => {
    Route.post('/login', 'AuthController.login')
    Route.get('/user', 'AuthController.getUser').middleware(['auth'])
    Route.post('/refresh', 'AuthController.refresh')
})
.prefix('api/auth')
3 Likes

I’ll try it. Thanks Ringelnatz!

Hi @Ringelnatz , what is default authenticator in your auth config? Jwt or session?

I’m sending refresh_token to /auth/refresh right? When I send it, I get this response:
{
“data”: [],
“message”: “Undefined binding(s) detected when compiling SELECT query: select * from tokens where token = ? and type = ? and is_revoked = ? and users.id = tokens.user_id”,
“options”: {
“defaultMsg”: “serverError”
},

My bad, it works! It’s late and my mind is playing tricks on me.

1 Like

Thanks again @Ringelnatz, cool.

So, I found a way not to generate any additonal refresh tokens in the database but it looks pretty dirty. Is there a better way. I don’t want my db cluttered with many tokens for each user.

async login({ request, response, auth }) {

const params = request.only(['username', 'password'])

const rules = {
  password: 'required',
  username: 'required'
}

const validation = await validate(params, rules)

if (validation.fails()) return response.badRequest('provide required params')

const user = await User.findByOrFail('username', params.username)
const isSame = await Hash.verify(params.password, user.password)
if (!isSame) return response.badRequest('invalid password')

if (user.is_deleted) return response.badRequest('User account is temporarily suspended. Please log in to reactivate your account.')
if (user.is_banned) return response.badRequest('User account is banned. Please send mail to postmaster@posao.hr for more details.')

if (!user.is_activated) return response.badRequest('Email is not verified. Please provide verification token.')

user.from_ip = request.ip()
user.login_number = ++user.login_number
if (!user.first_login_at) {
  user.first_login_at = Date.now()
}
user.last_login_at = Date.now()
if (user.user_type === 'employee') {
  user.is_activated = true
}
await user.save()

const foundRefreshToken = await Token.findBy('user_id', user.id)

if (!foundRefreshToken) {
  const tokenData_1 = await auth
    .withRefreshToken()
    .generate(user,
    {
      username: user.username,
      userType: user.user_type
    })

  return response.ok({
    token: tokenData_1.token,
    refreshToken: tokenData_1.refreshToken
  })
} else {
  const tokenData_2 = await auth
    .generate(user,
    {
      username: user.username,
      userType: user.user_type
    })

  const refreshToken2 = await user
    .tokens()
    .where('type', 'jwt_refresh_token')
    .where('is_revoked', false)

  const encToken = Encryption.encrypt(refreshToken2[0].token)
  console.log(refreshToken2[0].token)

  return response.ok({
    token: tokenData_2.token,
    refreshToken: encToken
  })
}

}

2 Likes

JWT, as we are talking about an API only app.

@jbjezanc Maybe the simplest approach would be deleting older tokens before creating a new one:

        const validUntil = new Date(new Date().setDate(new Date().getDate() - 7)) // valid for 7 days

        await user.tokens()
            .where('created_at', '<', validUntil)
            .delete()

        const token = await auth
            .withRefreshToken()
            .attempt(email, password)

        return token

Another option would be to delete every token before login so that every user has only one token, which of course only works if there is max. one session per user. Or you could give one token per device (add a “device_id” column), if the client is a mobile app or something. This highly depends on your use case and I do not have a perfect answer for that.
But only storing one token per user and returning that to every client is probably not the intended way because you cannot revoke access on a per-device basis.

3 Likes

Yup, this looks much cleaner. I have to cosult with the senior developers about that ‘one token per device’ thing but it also makes sense. Thanks again @Ringelnatz, you helped me a lot.

That is why soft deletion is bad in this context.
Before the generation of any new token, simply delete the existing one. This way you will have always one token at a time per user.