All files / src/security/userAndConnexion userAuthenticationTokenService.ts

32.3% Statements 73/226
100% Branches 0/0
0% Functions 0/7
32.3% Lines 73/226

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 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 2261x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                                           1x 1x 1x 1x 1x 1x 1x                                                                 1x 1x 1x 1x 1x 1x 1x 1x                                                                                   1x                                               1x 1x 1x 1x 1x 1x             1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x                       1x                                      
 
 
import jwt from 'jsonwebtoken'
import { getActiveAppConfig, getMainConfig } from '../../helpers/getGreenDotConfigs'
import { decryptToken, encryptToken } from '../encryptAndDecryptSafe'
import { generateUniqueToken } from '../../services/generateUniqueToken'
import { db } from '../../db'
import { ModelTypes } from '../../cache/dbs/index.generated'
import { setCsrfTokenCookie, setRefreshTokenCookie } from './cookieService'
import { getPluginConfig } from '../../plugins/pluginSystem'
 
 
 
 
//----------------------------------------
// USER AUTHENTICATION
//----------------------------------------
//
// user login with email + password + uniqueDeviceId
// he gets back accessToken and refreshToken
// on each login, we create a new refresh token and delete the previous one
// a total of 2 refresh tokens can be active at the same time (so 2 devices)
// accessToken can live up to one hour
 
export type JWTdataBase = {
    type: 'access' | 'refresh'
    deviceId: string
    deviceType: 'mobile' | 'web',
    /** a date in the future */
    expirationDate: number | 'never'
}
 
export type JWTdata = JWTdataBase & Omit<CtxUser, '_id' | 'role'> & { userId: string, role: GD['role'] | 'public' }
 
export type JWTdataWrite = Omit<JWTdata, 'expirationDate' | 'type'>
 
type JWTdataObfuscated = JWTdataBase & { d: string } // d is used to store userId, role and perms in a compressed way
 
export async function createToken(
    ctx: Ctx,
    data: Omit<JWTdata, 'expirationDate'>
) {

    const appConfig = await getActiveAppConfig()

    const { jwtRefreshExpirationMsMobile, jwtRefreshExpirationMsWeb, jwtSecret } = appConfig

    if (data.role === 'public') throw ctx.error.serverError('noTokenIsAllowedWithRolePublic')
    const expireInMs = data.type === 'access' ? appConfig.jwtExpirationMs /** do not spread */ : data.deviceType === 'web' ? jwtRefreshExpirationMsWeb : jwtRefreshExpirationMsMobile
    const expirationDate = expireInMs === 'never' ? expireInMs : Date.now() + expireInMs

    const { userId, role, permissions, ...otherFields } = data

    const jwtData: JWTdataObfuscated = { ...otherFields, expirationDate, d: encryptPermsInJwt(userId, role, permissions) }
    const originalJwt = jwt.sign(jwtData, jwtSecret)
    return {
        token: await encryptToken(originalJwt),
        expirationDate: expirationDate !== 'never' ? new Date(expirationDate) : expirationDate
    }
}
 
/** Parse token and throw errors if:
 * * wrong token
 * * expired token
 * * wrong token data format
 */
export async function parseToken(
    ctx: Ctx,
    token: string,
    checkExpiredToken = true
) {
    let data: JWTdata | undefined

    const appConfig = await getActiveAppConfig()
    const { jwtSecret } = appConfig

    const requiredTokenFields = ['type', 'userId', 'deviceId', 'expirationDate']

    try {
        const { d, ...otherFields } = jwt.verify(decryptToken(ctx, token), jwtSecret) as JWTdataObfuscated
        const { _id, permissions, role } = decryptPermInJwt(d)

        data = {
            userId: _id,
            permissions,
            role: role as GD['role'],
            ...otherFields
        }

    } catch (err) {
        throw ctx.error.wrongToken({ phase: 'verifyToken' })
    }

    if (!data) throw ctx.error.wrongToken({ phase: 'checkTokenDataExists' })
    else if (!requiredTokenFields.every(reqFld => !!data[reqFld])) throw ctx.error.wrongToken({ phase: 'JWTrequiredFields' })
    else if (checkExpiredToken && data.expirationDate !== 'never' && typeof data.expirationDate === 'number' && data.expirationDate < Date.now()) throw ctx.error.tokenExpired({ phase: 'expiredToken' })

    return data
}
 
/** This function will:
 * * GENERATE TOKENS
 * * DELETE PREVIOUS TOKEN ASSOCIATED WITH THIS DEVICEID
 * * CREATE AND UPDATE USER REFRESH TOKEN AND ACCESS TOKEN LIST
 * * PUT TOKEN IN COOKIE
 */
export async function setConnexionTokens(
    ctx: Ctx,
    deviceId: string,
    tokenData: JWTdataWrite,
) {

    const user = await ctx.getUser()

    const previousRefreshTokenList = user.refreshTokens
    const previousAccessTokenList = user.accessTokens

    const { maxRefreshTokenPerRole } = getPluginConfig('GDmanagedLogin')

    const { role } = tokenData
    // GENERATE TOKENS
    const { token: refreshToken, expirationDate } = await createToken(ctx, { ...tokenData, type: 'refresh' })
    const { token: accessToken } = await createToken(ctx, { ...tokenData, type: 'access' })
    const csrfToken = generateUniqueToken(24) // Simple Session Token
    const biometricAuthToken = generateUniqueToken(24) // biometric auth token

    //KEEP THE LATEST TOKENS

    const refreshTokenListWithoutPrevious = getTokenListWithoutPrevious(ctx, previousRefreshTokenList, deviceId, role, maxRefreshTokenPerRole[role] || 3)
    const accessTokenListWithoutPrevious = getTokenListWithoutPrevious(ctx, previousAccessTokenList, deviceId, role, maxRefreshTokenPerRole[role])

    await db.user.update(ctx.GM, ctx._id, {
        refreshTokens: [...refreshTokenListWithoutPrevious, refreshToken],
        accessTokens: [...accessTokenListWithoutPrevious, accessToken],
        biometricAuthToken,
    })

    setRefreshTokenCookie(ctx, refreshToken)
    setCsrfTokenCookie(ctx, csrfToken)

    return {
        refreshToken,
        accessToken,
        expirationDate,
        csrfToken,
        biometricAuthToken,
    }
}
 
function getTokenListWithoutPrevious(
    ctx: Ctx,
    previousTokenList: Array<string>,
    deviceId: string,
    role: Parameters<typeof setConnexionTokens>[2]['role'],
    maxTokenListLength: number
) {

    let tokenNbSessionsLeftForRole = maxTokenListLength - 1

    return previousTokenList.reverse().filter(async tkn => {
        try {
            const data = await parseToken(ctx, tkn)
            // FILTER OUT PREVIOUS TOKEN ASSOCIATED WITH THIS DEVICEID
            const isSameDeviceAndRole = data.deviceId === deviceId && data.role === role
            // OR TOKENS ABOVE MAX SESSIONS
            const isAboveMaxSession = !isSameDeviceAndRole && data.role === role && tokenNbSessionsLeftForRole-- === 0
            return !isSameDeviceAndRole && !isAboveMaxSession
        } catch (error) { // it may happens on server update
            return false
        }
    }).reverse()
}
 
 
 
/** Revoke a particular user token so it can't be used to login anymore. Note
that all accessTokens can still be used until peremtion date */
export async function revokeToken(ctx: Ctx, userId: string, token: string, tokenName: 'refreshTokens' | 'accessTokens' = 'refreshTokens', user?: ModelTypes['user']) {
    if (!user) user = await db.user.getById(ctx.GM, userId)
    const newTokens = user?.[tokenName]?.filter(t => t !== token)
    return newTokens ? await db.user.update(ctx.GM, userId, {
        [tokenName]: newTokens
    }) : null
}
 
 
 
 
 
 
//----------------------------------------
// COMPRESS PERMISSIONS
//----------------------------------------
 
/** This one is to transform the actual user perms that will be carried along in the JWT for it to be compressed and obfuscated */
function encryptPermsInJwt(_id: string, role: GD['role'], permissions: Partial<Ctx['permissions']>) {

    const { allRoles, allPermissions = [] } = getMainConfig()

    let encodedStr = _id + '.' + allRoles.indexOf(role as any) + '.'
    for (const permName of allPermissions) {
        const permVal = permissions[permName]
        encodedStr += permVal === true ? 1 : permVal === false ? 2 : 0
    }
    return encodedStr
}
 
function decryptPermInJwt(jwt: string) {

    const { allRoles, allPermissions = [] } = getMainConfig()

    const [_id, roleIndex, permStr] = jwt.split('.') || []

    const ctxUser: Partial<CtxUser> = {
        _id,
        role: allRoles[roleIndex] || 'public',
        permissions: {} as any
    }

    const permsNum = permStr.split('')
    permsNum.forEach((num: '0' | '1' | '2', i) => {
        const correspondingPerm = allPermissions[i]
        ctxUser.permissions[correspondingPerm] = num === '1' ? true : num === '2' ? false : undefined
    })
    return ctxUser
}