mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3517 from gradido/communityHandshake_fixAttackVectors
fix(federation): fix some attack vectors in communities handshake
This commit is contained in:
commit
90d3f00266
@ -38,6 +38,7 @@ export class CommunityResolver {
|
||||
@Authorized([RIGHTS.COMMUNITIES])
|
||||
@Query(() => [AdminCommunityView])
|
||||
async allCommunities(@Args() paginated: Paginated): Promise<AdminCommunityView[]> {
|
||||
// communityUUID could be oneTimePassCode (uint32 number)
|
||||
return (await getAllCommunities(paginated)).map((dbCom) => new AdminCommunityView(dbCom))
|
||||
}
|
||||
|
||||
@ -58,6 +59,7 @@ export class CommunityResolver {
|
||||
async communityByIdentifier(
|
||||
@Arg('communityIdentifier') communityIdentifier: string,
|
||||
): Promise<Community> {
|
||||
// communityUUID could be oneTimePassCode (uint32 number)
|
||||
const community = await getCommunityByIdentifier(communityIdentifier)
|
||||
if (!community) {
|
||||
throw new LogError('community not found', communityIdentifier)
|
||||
|
||||
@ -30,6 +30,10 @@ export class Community extends BaseEntity {
|
||||
@Column({ name: 'private_key', type: 'binary', length: 64, nullable: true })
|
||||
privateKey: Buffer | null
|
||||
|
||||
/**
|
||||
* Most of time a uuidv4 value, but could be also a uint32 number for a short amount of time, so please check before use
|
||||
* in community authentication this field is used to store a oneTimePassCode (uint32 number)
|
||||
*/
|
||||
@Column({
|
||||
name: 'community_uuid',
|
||||
type: 'char',
|
||||
|
||||
@ -9,11 +9,11 @@ import {
|
||||
getHomeCommunity,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType } from 'shared'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType, uint32Schema, uuidv4Schema } from 'shared'
|
||||
import { Arg, Mutation, Resolver } from 'type-graphql'
|
||||
import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver`)
|
||||
const createLogger = (method: string ) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.${method}`)
|
||||
|
||||
@Resolver()
|
||||
export class AuthenticationResolver {
|
||||
@ -22,45 +22,49 @@ export class AuthenticationResolver {
|
||||
@Arg('data')
|
||||
args: EncryptedTransferArgs,
|
||||
): Promise<boolean> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnection`)
|
||||
const methodLogger = createLogger('openConnection')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`openConnection() via apiVersion=1_0:`, args)
|
||||
const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType
|
||||
methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload)
|
||||
if (!openConnectionJwtPayload) {
|
||||
const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) {
|
||||
const errmsg = `invalid tokentype of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
if (!openConnectionJwtPayload.url) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA)
|
||||
methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA))
|
||||
if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
try {
|
||||
const openConnectionJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionJwtPayloadType
|
||||
methodLogger.debug('openConnectionJwtPayload', openConnectionJwtPayload)
|
||||
if (!openConnectionJwtPayload) {
|
||||
const errmsg = `invalid OpenConnection payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
if (openConnectionJwtPayload.tokentype !== OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE) {
|
||||
const errmsg = `invalid tokentype of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
if (!openConnectionJwtPayload.url) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
methodLogger.debug(`vor DbFedCommunity.findOneByOrFail()...`, { publicKey: args.publicKey })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
methodLogger.debug(`nach DbFedCommunity.findOneByOrFail()...`, fedComA)
|
||||
methodLogger.debug('fedComA', new FederatedCommunityLoggingView(fedComA))
|
||||
if (!openConnectionJwtPayload.url.startsWith(fedComA.endPoint)) {
|
||||
const errmsg = `invalid url of community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
|
||||
// no await to respond immediately and invoke callback-request asynchronously
|
||||
void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API)
|
||||
methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...')
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return true
|
||||
// no await to respond immediately and invoke callback-request asynchronously
|
||||
void startOpenConnectionCallback(args.handshakeID, args.publicKey, CONFIG.FEDERATION_API)
|
||||
methodLogger.debug('openConnection() successfully initiated callback and returns true immediately...')
|
||||
return true
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@ -68,37 +72,41 @@ export class AuthenticationResolver {
|
||||
@Arg('data')
|
||||
args: EncryptedTransferArgs,
|
||||
): Promise<boolean> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnectionCallback`)
|
||||
const methodLogger = createLogger('openConnectionCallback')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args)
|
||||
try {
|
||||
// decrypt args.url with homeCom.privateJwtKey and verify signing with callbackFedCom.publicKey
|
||||
const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType
|
||||
if (!openConnectionCallbackJwtPayload) {
|
||||
const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
const openConnectionCallbackJwtPayload = await interpretEncryptedTransferArgs(args) as OpenConnectionCallbackJwtPayloadType
|
||||
if (!openConnectionCallbackJwtPayload) {
|
||||
const errmsg = `invalid OpenConnectionCallback payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
|
||||
const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1)
|
||||
const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length)
|
||||
methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
|
||||
if (!fedComB) {
|
||||
const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1)
|
||||
const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length)
|
||||
methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
|
||||
if (!fedComB) {
|
||||
const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
methodLogger.debug(
|
||||
`found fedComB and start authentication:`,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
)
|
||||
// no await to respond immediately and invoke authenticate-request asynchronously
|
||||
void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB)
|
||||
methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...')
|
||||
return true
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
return true
|
||||
}
|
||||
methodLogger.debug(
|
||||
`found fedComB and start authentication:`,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
)
|
||||
// no await to respond immediately and invoke authenticate-request asynchronously
|
||||
void startAuthentication(args.handshakeID, openConnectionCallbackJwtPayload.oneTimeCode, fedComB)
|
||||
methodLogger.debug('openConnectionCallback() successfully initiated authentication and returns true immediately...')
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return true
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
@ -106,32 +114,54 @@ export class AuthenticationResolver {
|
||||
@Arg('data')
|
||||
args: EncryptedTransferArgs,
|
||||
): Promise<string | null> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.authenticate`)
|
||||
const methodLogger = createLogger('authenticate')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args)
|
||||
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
|
||||
if (!authArgs) {
|
||||
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
|
||||
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom))
|
||||
if (authCom) {
|
||||
authCom.communityUuid = authArgs.uuid
|
||||
authCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(authCom)
|
||||
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
|
||||
const homeComB = await getHomeCommunity()
|
||||
if (homeComB?.communityUuid) {
|
||||
const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid)
|
||||
const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return responseJwt
|
||||
try {
|
||||
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
|
||||
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(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
|
||||
}
|
||||
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
|
||||
if (authCom) {
|
||||
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom))
|
||||
if (authCom.publicKey !== authArgs.publicKey) {
|
||||
const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${authArgs.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
}
|
||||
const communityUuid = uuidv4Schema.safeParse(authArgs.uuid)
|
||||
if (!communityUuid.success) {
|
||||
const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authArgs.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
}
|
||||
authCom.communityUuid = communityUuid.data
|
||||
authCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(authCom)
|
||||
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
|
||||
const homeComB = await getHomeCommunity()
|
||||
if (homeComB?.communityUuid) {
|
||||
const responseArgs = new AuthenticationResponseJwtPayloadType(args.handshakeID,homeComB.communityUuid)
|
||||
const responseJwt = await encryptAndSign(responseArgs, homeComB.privateJwtKey!, authCom.publicJwtKey!)
|
||||
return responseJwt
|
||||
}
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
return null
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,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, verifyAndDecrypt } from 'shared'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, uuidv4Schema, verifyAndDecrypt } from 'shared'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`)
|
||||
|
||||
@ -40,8 +40,13 @@ export async function startOpenConnectionCallback(
|
||||
apiVersion: api,
|
||||
publicKey: comA.publicKey,
|
||||
})
|
||||
const oneTimeCode = randombytes_random().toString()
|
||||
// store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier
|
||||
// prevent overwriting valid UUID with oneTimeCode, because this request could be initiated at any time from federated community
|
||||
if (uuidv4Schema.safeParse(comA.communityUuid).success) {
|
||||
throw new Error('Community UUID is already a valid UUID')
|
||||
}
|
||||
// TODO: make sure it is unique
|
||||
const oneTimeCode = randombytes_random().toString()
|
||||
comA.communityUuid = oneTimeCode
|
||||
await DbCommunity.save(comA)
|
||||
methodLogger.debug(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { string } from 'zod'
|
||||
import { string, number } from 'zod'
|
||||
import { validate, version } from 'uuid'
|
||||
|
||||
export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid')
|
||||
export const emailSchema = string().email()
|
||||
export const urlSchema = string().url()
|
||||
export const urlSchema = string().url()
|
||||
export const uint32Schema = number().positive().lte(4294967295)
|
||||
Loading…
x
Reference in New Issue
Block a user