Skip to content

Commit

Permalink
feat: add refresh token
Browse files Browse the repository at this point in the history
  • Loading branch information
DeVoresyah committed Jan 2, 2024
1 parent 1d00ab8 commit c6d502c
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 2 deletions.
77 changes: 76 additions & 1 deletion app/Controllers/Http/v1/Auth/AuthsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import LoginWithOtpValidator from 'App/Validators/v1/Auth/LoginWithOtpValidator'
import LogoutValidator from 'App/Validators/v1/Auth/LogoutValidator'
import ResendValidator from 'App/Validators/v1/Auth/ResendValidator'
import ForgotPasswordValidator from 'App/Validators/v1/Auth/ForgotPasswordValidator'
import RefreshSessionValidator from 'App/Validators/v1/Auth/RefreshSessionValidator'

// Models
import User from 'App/Models/User'
Expand Down Expand Up @@ -404,7 +405,10 @@ export default class AuthsController {

try {
if (!sessions.currentSession) {
return response.api({ message: 'Invalid session.' }, StatusCodes.UNAUTHORIZED)
return response.api(
{ message: 'Invalid session, please login again.' },
StatusCodes.UNAUTHORIZED
)
}

if (payload.scope === 'global') {
Expand Down Expand Up @@ -478,4 +482,75 @@ export default class AuthsController {
)
}
}

public async refreshSession({ request, response }: HttpContextContract) {
const payload = await request.validate(RefreshSessionValidator)
const userId = request.decoded!.user_id
const sessionId = request.decoded!.session_id
const ip = request.ips()[0]

const session = await Session.query().where('user_id', userId).andWhere('id', sessionId).first()

if (!session || session!.ip !== ip) {
return response.api(
{ message: 'Invalid session, please login again.' },
StatusCodes.UNAUTHORIZED
)
}

const refreshToken = await RefreshToken.query()
.where('user_id', userId)
.andWhere('session_id', sessionId)
.andWhere('token', payload.refresh_token)
.first()

if (!refreshToken || refreshToken?.revoked) {
return response.api({ message: 'Invalid refresh token.' }, StatusCodes.FORBIDDEN)
}

const refreshSession = await Database.transaction(async (trx) => {
refreshToken.useTransaction(trx)

refreshToken.revoked = true
await refreshToken.save()

if (session) {
session.useTransaction(trx)
session.refreshedAt = DateTime.now()
await session.save()
}

const newRefreshToken = await RefreshToken.create(
{
userId: userId,
sessionId: sessionId,
parent: refreshToken.id,
revoked: false,
token: cuid(),
},
{ client: trx }
)

return {
newRefreshToken,
}
})

if (refreshSession.newRefreshToken) {
const accessToken = this.jwt.generate({ user_id: userId, session_id: sessionId }).make()

return response.api(
{
access_token: accessToken,
refreshToken: refreshSession.newRefreshToken.token,
},
StatusCodes.OK
)
} else {
return response.api(
{ message: 'Failed to refresh session.' },
StatusCodes.INTERNAL_SERVER_ERROR
)
}
}
}
18 changes: 17 additions & 1 deletion app/Middleware/UserSessionMiddleware.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import JwtService from 'App/Services/JwtService'
import { StatusCodes } from 'http-status-codes'

// Models
import Session from 'App/Models/Session'

export default class UserSessionMiddleware {
public jwtService = new JwtService()
Expand All @@ -12,12 +16,24 @@ export default class UserSessionMiddleware {

// If token is not present, return unauthorized
if (!token) {
return response.api({ message: 'Token cannot be empty' }, 401)
return response.api({ message: 'Token cannot be empty' }, StatusCodes.UNAUTHORIZED)
}

// Verify token
const decoded = this.jwtService.verify(token).extract()

const session = await Session.query()
.where('user_id', decoded['user_id'])
.andWhere('id', decoded['session_id'])
.first()

if (!session) {
return response.api(
{ message: 'Invalid session, please login again.' },
StatusCodes.UNAUTHORIZED
)
}

request.decoded = {
user_id: decoded['user_id'],
session_id: decoded['session_id'],
Expand Down
14 changes: 14 additions & 0 deletions app/Validators/v1/Auth/RefreshSessionValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { schema, CustomMessages } from '@ioc:Adonis/Core/Validator'
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class RefreshSessionValidator {
constructor(protected ctx: HttpContextContract) {}

public schema = schema.create({
refresh_token: schema.string(),
})

public messages: CustomMessages = {
required: '{{ field }} cannot be empty.',
}
}
1 change: 1 addition & 0 deletions routes/auth/v1/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Route.group(() => {
Route.post('/login/otp', 'AuthsController.signInWithOtp')
Route.post('/resend', 'AuthsController.resend')
Route.post('/forgot-password', 'AuthsController.forgotPassword')
Route.post('/refresh', 'AuthsController.refreshSession').middleware('userSession')
Route.delete('/logout', 'AuthsController.signOut').middleware('userSession')
require('./verify')
})
Expand Down

0 comments on commit c6d502c

Please sign in to comment.