diff --git a/backend/src/federation/authenticateCommunities.ts b/backend/src/federation/authenticateCommunities.ts index ad2d91469..f186adb35 100644 --- a/backend/src/federation/authenticateCommunities.ts +++ b/backend/src/federation/authenticateCommunities.ts @@ -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) diff --git a/backend/src/federation/validateCommunities.ts b/backend/src/federation/validateCommunities.ts index 36c9985c9..cb56a8c53 100644 --- a/backend/src/federation/validateCommunities.ts +++ b/backend/src/federation/validateCommunities.ts @@ -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 see https://javascript.info/settimeout-setinterval setTimeout(async function run() { diff --git a/database/src/queries/communities.ts b/database/src/queries/communities.ts index e216f8af6..cdb7cabb6 100644 --- a/database/src/queries/communities.ts +++ b/database/src/queries/communities.ts @@ -60,4 +60,13 @@ export async function getReachableCommunities( ], order, }) +} + +export async function getNotReachableCommunities( + order?: FindOptionsOrder +): Promise { + return await DbCommunity.find({ + where: { authenticatedAt: IsNull(), foreign: true }, + order, + }) } \ No newline at end of file diff --git a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts index 84d85a0af..fd948300e 100644 --- a/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/AuthenticationResolver.ts @@ -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) diff --git a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts index a5a3f532a..4d8af9187 100644 --- a/federation/src/graphql/api/1_0/util/authenticateCommunity.ts +++ b/federation/src/graphql/api/1_0/util/authenticateCommunity.ts @@ -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() diff --git a/shared/src/const/index.ts b/shared/src/const/index.ts index e6fb80990..549991c61 100644 --- a/shared/src/const/index.ts +++ b/shared/src/const/index.ts @@ -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' \ No newline at end of file +export const REDEEM_JWT_TOKEN_EXPIRATION = '10m' +// 10 minutes +export const FEDERATION_AUTHENTICATION_TIMEOUT_MS = 60 * 60 * 1000 * 10 \ No newline at end of file diff --git a/shared/src/index.ts b/shared/src/index.ts index a9e070d7f..1e44e2c48 100644 --- a/shared/src/index.ts +++ b/shared/src/index.ts @@ -1,5 +1,6 @@ export * from './schema' export * from './enum' +export * from './const' export * from './helper' export * from './logic/decay' export * from './jwt/JWT' diff --git a/shared/src/schema/community.schema.test.ts b/shared/src/schema/community.schema.test.ts new file mode 100644 index 000000000..a46ea9c94 --- /dev/null +++ b/shared/src/schema/community.schema.test.ts @@ -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) + }) +}) diff --git a/shared/src/schema/community.schema.ts b/shared/src/schema/community.schema.ts new file mode 100644 index 000000000..6067e3538 --- /dev/null +++ b/shared/src/schema/community.schema.ts @@ -0,0 +1,7 @@ +import { object, date } from 'zod' +import { uuidv4Schema } from './base.schema' + +export const communityAuthenticatedSchema = object({ + communityUuid: uuidv4Schema, + authenticatedAt: date(), +}) \ No newline at end of file diff --git a/shared/src/schema/index.ts b/shared/src/schema/index.ts index d8c9f9e4c..83455fb73 100644 --- a/shared/src/schema/index.ts +++ b/shared/src/schema/index.ts @@ -1,2 +1,3 @@ export * from './user.schema' -export * from './base.schema' \ No newline at end of file +export * from './base.schema' +export * from './community.schema' \ No newline at end of file