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 226 | 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 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
} |