All files / src/plugins/managedLogin getNewTokenService.ts

13.86% Statements 14/101
100% Branches 0/0
0% Functions 0/1
13.86% Lines 14/101

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 1011x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                                                                                                                                                              
 
 
 
 
 
import { db } from '../../db'
import { getPlugin, getPluginConfig } from '../pluginSystem'
import { svc } from '../../service'
import { _ } from '../../validator'
import { parseToken, revokeToken, setConnexionTokens } from '../../security/userAndConnexion/userAuthenticationTokenService'
import { ensureUserIsNotLocked } from '../../security/userAndConnexion/userLockService'
 
 
export const getNewTokenService = () => {

    const doubleAuth = getPlugin('GDdoubleAuthentication')
    const { pinCodeLength } = getPluginConfig('GDdoubleAuthentication')

    return svc({
        doc: {
            description: `This route if to renew token periodically after login. Adding an optional pinCode or biometricAuthToken as input allow to use an expired token.`,
            errors: [
                [404, 'notFound', 'If user do not exist'],
                [403, 'wrongToken', `May throw for differnet reasons: 'noCookieProvided', 'notSameDevice', 'notExistingToken', 'noTokenRegistered1', 'noTokenRegistered2', 'verifyToken', 'checkTokenDataExists', 'JWTrequiredFields'`],
                [401, 'tokenExpired'],
                [401, 'userLocked'],
                [429, 'tooMuchPinCodeAttempts'],
                [403, 'wrongPinCode']
            ]
        },
        for: ['public', 'ALL'],
        input: {
            deviceId: _.string().required(),
            pinCode: _.regexp(/^\d+$/).length(pinCodeLength),
            biometricAuthToken: _.string(),
        },
        output: _.object({
            accessToken: _.string(),
            csrfToken: _.string(),
            expirationDate: _.typesOr([_.date(), _.stringConstant('never')]),
            biometricAuthToken: _.string(),
        }).complete(),
        rateLimiter: {
            default: '10/30s',
            test: '200/min',
        },
        async main(ctx, { deviceId, pinCode, biometricAuthToken }) {

            const refreshToken = ctx.api.req?.cookies?.refreshToken || (ctx.env === 'test' && ctx.api.req?.headers?.refreshtoken)

            if (!refreshToken) throw ctx.error.wrongToken({ additionalInfos: 'noCookieProvided' })

            const tokenData = await parseToken(ctx, refreshToken, false)

            const isSameDevice = tokenData.deviceId === deviceId

            if (!isSameDevice) {
                // provided device infos and token device infos are not the same
                await revokeToken(ctx, ctx.isPublic ? tokenData.userId : ctx._id, refreshToken)
                throw ctx.error.wrongToken({ additionalInfos: 'notSameDevice' })
            }

            const user = await db.user.getOne(ctx.GM, { refreshTokens: refreshToken })

            if (user) await ensureUserIsNotLocked(ctx, user)
            else {
                ctx.api.res.clearCookie('refreshToken')
                throw ctx.error.wrongToken({ additionalInfos: 'notExistingToken' })
            }

            if (tokenData.expirationDate !== 'never' && tokenData.expirationDate < Date.now()) {
                // EXPIRED but in case biometric auth or pincode is set we can still use the refresh token
                if (pinCode || biometricAuthToken) {
                    await doubleAuth.compareAndAddAttempt(ctx, pinCode ? 'pincode' : 'biometricAuthToken', pinCode || biometricAuthToken, user)
                } else {
                    throw ctx.error.tokenExpired({ phase: 'expiredToken' })
                }
            }

            const userCtx = ctx.clone().useRole(tokenData.role, user)

            if (user.refreshTokens) {
                const foundRefreshTkn = user.refreshTokens.find(r => r === refreshToken)
                if (foundRefreshTkn) {
                    const { accessToken, refreshToken, csrfToken, expirationDate, biometricAuthToken } = await setConnexionTokens(userCtx, deviceId, tokenData)

                    // TODO 129JDIE find a way to simulate prod env and test that this value is not returned
                    return {
                        accessToken,
                        csrfToken,
                        expirationDate,
                        biometricAuthToken,
                        ...({ refreshToken: ctx.env === 'test' || tokenData.deviceType === 'mobile' ? refreshToken as any : undefined }),
                    }
                } else {
                    throw ctx.error.wrongToken({ additionalInfos: 'noTokenRegistered1' })
                }
            } else throw ctx.error.wrongToken({ additionalInfos: 'noTokenRegistered2' })
        },
    })
}