try to make oneTimeCode more robust

This commit is contained in:
einhornimmond 2025-10-12 08:48:08 +02:00
parent 47b38ac58f
commit e8ef1bc310
10 changed files with 86 additions and 13 deletions

View File

@ -7,7 +7,7 @@ import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/
import { ensureUrlEndsWithSlash } from 'core'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
import { communityAuthenticatedSchema, encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
import { getLogger } from 'log4js'
import { AuthenticationClientFactory } from './client/AuthenticationClientFactory'
import { EncryptedTransferArgs } from 'core'
@ -34,13 +34,10 @@ export async function startCommunityAuthentication(
methodLogger.debug('started with comB:', new CommunityLoggingView(comB))
// check if communityUuid is not a valid v4Uuid
try {
if (
comB &&
((comB.communityUuid === null && comB.authenticatedAt === null) ||
(comB.communityUuid !== null &&
(!validateUUID(comB.communityUuid) ||
versionUUID(comB.communityUuid!) !== 4)))
) {
// communityAuthenticatedSchema.safeParse return true
// - if communityUuid is a valid v4Uuid and
// - if authenticatedAt is a valid date
if (comB && !communityAuthenticatedSchema.safeParse(comB).success) {
methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null')
const client = AuthenticationClientFactory.getInstance(fedComB)

View File

@ -3,6 +3,7 @@ import {
FederatedCommunity as DbFederatedCommunity,
FederatedCommunityLoggingView,
getHomeCommunity,
getNotReachableCommunities,
} from 'database'
import { IsNull } from 'typeorm'
@ -11,7 +12,7 @@ import { FederationClient as V1_0_FederationClient } from '@/federation/client/1
import { PublicCommunityInfo } from '@/federation/client/1_0/model/PublicCommunityInfo'
import { FederationClientFactory } from '@/federation/client/FederationClientFactory'
import { LogError } from '@/server/LogError'
import { createKeyPair } from 'shared'
import { createKeyPair, uint32Schema } from 'shared'
import { getLogger } from 'log4js'
import { startCommunityAuthentication } from './authenticateCommunities'
import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view'
@ -27,6 +28,16 @@ export async function startValidateCommunities(timerInterval: number): Promise<v
// delete all foreign federated community entries to avoid increasing validation efforts and log-files
await DbFederatedCommunity.delete({ foreign: true })
// clean community_uuid and authenticated_at fields for community with one-time-code in community_uuid field
const notReachableCommunities = await getNotReachableCommunities()
for (const community of notReachableCommunities) {
if (uint32Schema.safeParse(Number(community.communityUuid)).success) {
community.communityUuid = null
community.authenticatedAt = null
await DbCommunity.save(community)
}
}
// TODO: replace the timer-loop by an event-based communication to verify announced foreign communities
// better to use setTimeout twice than setInterval once -> see https://javascript.info/settimeout-setinterval
setTimeout(async function run() {

View File

@ -60,4 +60,13 @@ export async function getReachableCommunities(
],
order,
})
}
export async function getNotReachableCommunities(
order?: FindOptionsOrder<DbCommunity>
): Promise<DbCommunity[]> {
return await DbCommunity.find({
where: { authenticatedAt: IsNull(), foreign: true },
order,
})
}

View File

@ -127,21 +127,27 @@ export class AuthenticationResolver {
methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args)
try {
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs)
if (!authArgs) {
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
methodLogger.error(errmsg)
// no infos to the caller
return null
}
if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) {
const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32`
methodLogger.error(errmsg)
// no infos to the caller
return null
}
methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode)
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
if (authCom) {
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom))
methodLogger.debug('authCom.publicKey', authCom.publicKey.toString('hex'))
methodLogger.debug('args.publicKey', args.publicKey)
if (authCom.publicKey.compare(Buffer.from(args.publicKey, 'hex')) !== 0) {
const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}`
methodLogger.error(errmsg)

View File

@ -15,6 +15,7 @@ import { randombytes_random } from 'sodium-native'
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient'
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uint32Schema, uuidv4Schema, verifyAndDecrypt } from 'shared'
import { FEDERATION_AUTHENTICATION_TIMEOUT_MS } from 'shared'
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`)
@ -45,9 +46,12 @@ export async function startOpenConnectionCallback(
if (uuidv4Schema.safeParse(comA.communityUuid).success) {
methodLogger.debug('Community UUID is already a valid UUID')
return
// check for still ongoing authentication, but with timeout
} else if (uint32Schema.safeParse(Number(comA.communityUuid)).success) {
methodLogger.debug('Community UUID is still in authentication...oneTimeCode=', comA.communityUuid)
return
if (comA.updatedAt && (Date.now() - comA.updatedAt.getTime()) < FEDERATION_AUTHENTICATION_TIMEOUT_MS) {
methodLogger.debug('Community UUID is still in authentication...oneTimeCode=', comA.communityUuid)
return
}
}
// TODO: make sure it is unique
const oneTimeCode = randombytes_random().toString()

View File

@ -1,4 +1,6 @@
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
export const SECONDS_PER_YEAR_GREGORIAN_CALENDER = 31556952.0
export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
// 10 minutes
export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 60 * 1000 * 10

View File

@ -1,5 +1,6 @@
export * from './schema'
export * from './enum'
export * from './const'
export * from './helper'
export * from './logic/decay'
export * from './jwt/JWT'

View File

@ -0,0 +1,35 @@
import { v4 as uuidv4 } from 'uuid'
import { communityAuthenticatedSchema } from './community.schema'
import { describe, it, expect } from 'bun:test'
describe('communityAuthenticatedSchema', () => {
it('should return an error if communityUuid is not a uuidv4', () => {
const data = communityAuthenticatedSchema.safeParse({
communityUuid: '1234567890',
authenticatedAt: new Date(),
})
expect(data.success).toBe(false)
expect(data.error?.issues[0].path).toEqual(['communityUuid'])
})
it('should return an error if authenticatedAt is not a date', () => {
const data = communityAuthenticatedSchema.safeParse({
communityUuid: uuidv4(),
authenticatedAt: '2022-01-01',
})
expect(data.success).toBe(false)
expect(data.error?.issues[0].path).toEqual(['authenticatedAt'])
})
it('should return no error for valid data and valid uuid4', () => {
const data = communityAuthenticatedSchema.safeParse({
communityUuid: uuidv4(),
authenticatedAt: new Date(),
})
expect(data.success).toBe(true)
})
})

View File

@ -0,0 +1,7 @@
import { object, date } from 'zod'
import { uuidv4Schema } from './base.schema'
export const communityAuthenticatedSchema = object({
communityUuid: uuidv4Schema,
authenticatedAt: date(),
})

View File

@ -1,2 +1,3 @@
export * from './user.schema'
export * from './base.schema'
export * from './base.schema'
export * from './community.schema'