mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
first draft community authentication handshake
This commit is contained in:
parent
841979a360
commit
81bf608e2b
@ -20,7 +20,7 @@ export async function startCommunityAuthentication(
|
||||
const foreignCom = await DbCommunity.findOneByOrFail({ publicKey: foreignFedCom.publicKey })
|
||||
if (foreignCom && foreignCom.communityUuid === null && foreignCom.authenticatedAt === null) {
|
||||
try {
|
||||
const client = AuthenticationClientFactory.getInstance(homeFedCom)
|
||||
const client = AuthenticationClientFactory.getInstance(foreignFedCom)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
const args = new OpenConnectionArgs()
|
||||
|
||||
@ -59,7 +59,7 @@ export async function validateCommunities(): Promise<void> {
|
||||
const pubComInfo = await client.getPublicCommunityInfo()
|
||||
if (pubComInfo) {
|
||||
await writeForeignCommunity(dbCom, pubComInfo)
|
||||
void startCommunityAuthentication(dbCom)
|
||||
await startCommunityAuthentication(dbCom)
|
||||
logger.debug(`Federation: write publicInfo of community: name=${pubComInfo.name}`)
|
||||
} else {
|
||||
logger.warn('Federation: missing result of getPublicCommunityInfo')
|
||||
|
||||
@ -54,6 +54,7 @@
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"eslint-plugin-security": "^1.7.1",
|
||||
"eslint-plugin-type-graphql": "^1.0.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
"jest": "^27.2.4",
|
||||
"nodemon": "^2.0.7",
|
||||
"prettier": "^2.3.1",
|
||||
|
||||
@ -7,7 +7,6 @@ import { openConnectionCallback } from './query/openConnectionCallback'
|
||||
import { AuthenticationArgs } from '@/graphql/api/1_0/model/AuthenticationArgs'
|
||||
import { authenticate } from './query/authenticate'
|
||||
|
||||
|
||||
export class AuthenticationClient {
|
||||
dbCom: DbFederatedCommunity
|
||||
endpoint: string
|
||||
@ -27,12 +26,13 @@ export class AuthenticationClient {
|
||||
})
|
||||
}
|
||||
|
||||
async openConnectionCallback(args: OpenConnectionCallbackArgs): Promise<boolean | undefined> {
|
||||
async openConnectionCallback(args: OpenConnectionCallbackArgs): Promise<boolean> {
|
||||
logger.debug('Authentication: openConnectionCallback with endpoint', this.endpoint, args)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(openConnectionCallback, { args })
|
||||
if (!data?.openConnectionCallback) {
|
||||
const { data } = await this.client.rawRequest<any>(openConnectionCallback, { args })
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (data && data.openConnectionCallback) {
|
||||
logger.warn(
|
||||
'Authentication: openConnectionCallback without response data from endpoint',
|
||||
this.endpoint,
|
||||
@ -47,24 +47,24 @@ export class AuthenticationClient {
|
||||
} catch (err) {
|
||||
logger.error('Authentication: error on openConnectionCallback', err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async authenticate(args: AuthenticationArgs): Promise<string | undefined> {
|
||||
async authenticate(args: AuthenticationArgs): Promise<string | null> {
|
||||
logger.debug('Authentication: authenticate with endpoint=', this.endpoint)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const { data } = await this.client.rawRequest(authenticate, {})
|
||||
const { data } = await this.client.rawRequest<any>(authenticate, { args })
|
||||
logger.debug('Authentication: after authenticate: data:', data)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (!data?.authenticate) {
|
||||
logger.warn(
|
||||
'Authentication: authenticate without response data from endpoint',
|
||||
this.endpoint,
|
||||
)
|
||||
return
|
||||
const authUuid: string = data?.authenticate.uuid
|
||||
if (authUuid) {
|
||||
logger.debug('Authentication: received authenticated uuid', authUuid)
|
||||
return authUuid
|
||||
}
|
||||
const
|
||||
} catch (err) {
|
||||
logger.error('Authentication: authenticate failed for endpoint', this.endpoint)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import { gql } from 'graphql-request'
|
||||
|
||||
export const authenticate = gql`
|
||||
mutation ($args: AuthenticateArgs!) {
|
||||
authenticate(data: $args)
|
||||
authenticate(data: $args) {
|
||||
uuid
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class AuthenticationArgs {
|
||||
@Field(() => String)
|
||||
oneTimeCode: string
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class OpenConnectionArgs {
|
||||
@Field(() => String)
|
||||
publicKey: string
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
import { ArgsType, Field } from 'type-graphql'
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class OpenConnectionCallbackArgs {
|
||||
@Field(() => String)
|
||||
oneTimeCode: string
|
||||
|
||||
@Field(() => String)
|
||||
publicKey: string
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
}
|
||||
|
||||
@ -2,14 +2,13 @@
|
||||
import { Arg, Mutation, Resolver } from 'type-graphql'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFedCommunity } from '@entity/FederatedCommunity'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { OpenConnectionArgs } from '../model/OpenConnectionArgs'
|
||||
import {
|
||||
startOpenConnectionCallback,
|
||||
startOpenConnectionRedirect,
|
||||
} from '../util/authenticateCommunity'
|
||||
import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity'
|
||||
import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs'
|
||||
import { ApiVersionType } from '@/client/enum/apiVersionType'
|
||||
import { CONFIG } from '@/config'
|
||||
import { AuthenticationArgs } from '../model/AuthenticationArgs'
|
||||
|
||||
@Resolver()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@ -28,7 +27,8 @@ export class AuthenticationResolver {
|
||||
if (!requestedCom) {
|
||||
throw new LogError(`unknown requesting community with publicKey`, args.publicKey)
|
||||
}
|
||||
void startOpenConnectionRedirect(args, requestedCom, ApiVersionType.V1_0)
|
||||
// no await to respond immediatly and invoke callback-request asynchron
|
||||
void startOpenConnectionCallback(args, requestedCom, CONFIG.FEDERATION_API)
|
||||
return true
|
||||
}
|
||||
|
||||
@ -38,14 +38,37 @@ export class AuthenticationResolver {
|
||||
args: OpenConnectionCallbackArgs,
|
||||
): Promise<boolean> {
|
||||
logger.debug(`Authentication: openConnectionCallback() via apiVersion=1_0 ...`, args)
|
||||
// first find with args.publicKey the community, which invokes openConnectionCallback
|
||||
const callbackCom = await DbCommunity.findOneBy({
|
||||
publicKey: Buffer.from(args.publicKey),
|
||||
})
|
||||
if (!callbackCom) {
|
||||
throw new LogError(`unknown callback community with publicKey`, args.publicKey)
|
||||
// TODO decrypt args.url with homeCom.privateKey and verify signing with callbackFedCom.publicKey
|
||||
const endPoint = args.url.slice(0, args.url.lastIndexOf('/'))
|
||||
const apiVersion = args.url.slice(args.url.lastIndexOf('/'), args.url.length)
|
||||
const callbackFedCom = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
|
||||
if (!callbackFedCom) {
|
||||
throw new LogError(`unknown callback community with url`, args.url)
|
||||
}
|
||||
void startOpenConnectionCallback(args, callbackCom)
|
||||
// no await to respond immediatly and invoke authenticate-request asynchron
|
||||
void startAuthentication(args.oneTimeCode, callbackFedCom)
|
||||
return true
|
||||
}
|
||||
|
||||
@Mutation(() => String)
|
||||
async authenticate(
|
||||
@Arg('data')
|
||||
args: AuthenticationArgs,
|
||||
): Promise<string | null> {
|
||||
logger.debug(`Authentication: authenticate() via apiVersion=1_0 ...`, args)
|
||||
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: args.oneTimeCode })
|
||||
logger.debug('Authentication: found authCom:', authCom)
|
||||
if (authCom) {
|
||||
// TODO decrypt args.uuid with authCom.publicKey
|
||||
authCom.communityUuid = args.uuid
|
||||
await DbCommunity.save(authCom)
|
||||
logger.debug('Authentication: store authCom.uuid successfully:', authCom)
|
||||
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
// TODO encrypt homeCom.uuid with homeCom.privateKey
|
||||
if (homeCom.communityUuid) {
|
||||
return homeCom.communityUuid
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,19 @@
|
||||
import { OpenConnectionArgs } from '../model/OpenConnectionArgs'
|
||||
import { Community as DbCommunity } from '@entity/Community'
|
||||
import { FederatedCommunity as DbFederatedCommunity } from '@entity/FederatedCommunity'
|
||||
import { FederatedCommunity as DbFedCommunity } from '@entity/FederatedCommunity'
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory'
|
||||
import { ApiVersionType } from '@/client/enum/apiVersionType'
|
||||
// eslint-disable-next-line camelcase
|
||||
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/client/1_0/AuthenticationClient'
|
||||
import { AuthenticationArgs } from '../model/AuthenticationArgs'
|
||||
|
||||
export async function startOpenConnectionRedirect(
|
||||
export async function startOpenConnectionCallback(
|
||||
args: OpenConnectionArgs,
|
||||
requestedCom: DbCommunity,
|
||||
api: ApiVersionType,
|
||||
api: string,
|
||||
): Promise<void> {
|
||||
logger.debug(
|
||||
`Authentication: startOpenConnectionRedirect()...`,
|
||||
@ -22,14 +22,13 @@ export async function startOpenConnectionRedirect(
|
||||
requestedCom,
|
||||
)
|
||||
try {
|
||||
// TODO verify signing of args.url with requestedCom.publicKey and decrypt with homeCom.privateKey
|
||||
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
const homeFedCom = await DbFederatedCommunity.findOneByOrFail({
|
||||
const homeFedCom = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: api,
|
||||
})
|
||||
const oneTimeCode = randombytes_random()
|
||||
// store oneTimeCode in requestedCom.community_uuid for authenticate-request-identifier
|
||||
// store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier
|
||||
requestedCom.communityUuid = oneTimeCode.toString()
|
||||
await DbCommunity.save(requestedCom)
|
||||
|
||||
@ -38,8 +37,7 @@ export async function startOpenConnectionRedirect(
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
const callbackArgs = new OpenConnectionCallbackArgs()
|
||||
callbackArgs.oneTimeCode = oneTimeCode.toString()
|
||||
callbackArgs.publicKey = homeCom.publicKey.toString('hex')
|
||||
// TODO signing of callbackArgs.url with requestedCom.publicKey and decrypt with homeCom.privateKey
|
||||
// TODO encrypt callbackArgs.url with requestedCom.publicKey and sign it with homeCom.privateKey
|
||||
callbackArgs.url = homeFedCom.endPoint.endsWith('/')
|
||||
? homeFedCom.endPoint
|
||||
: homeFedCom.endPoint + '/' + homeFedCom.apiVersion
|
||||
@ -54,21 +52,45 @@ export async function startOpenConnectionRedirect(
|
||||
}
|
||||
}
|
||||
|
||||
export async function startOpenConnectionCallback(
|
||||
args: OpenConnectionCallbackArgs,
|
||||
callbackCom: DbCommunity,
|
||||
export async function startAuthentication(
|
||||
oneTimeCode: string,
|
||||
callbackFedCom: DbFedCommunity,
|
||||
): Promise<void> {
|
||||
logger.debug(
|
||||
`Authentication: startOpenConnectionCallback()...`,
|
||||
args.publicKey,
|
||||
args.url,
|
||||
callbackCom,
|
||||
)
|
||||
logger.debug(`Authentication: startAuthentication()...`, oneTimeCode, callbackFedCom)
|
||||
try {
|
||||
// TODO verify signing of args.url with requestedCom.publicKey and decrypt with homeCom.privateKey
|
||||
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
|
||||
|
||||
const homeFedCom = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: callbackFedCom.apiVersion,
|
||||
})
|
||||
|
||||
// TODO encrypt homeCom.uuid with homeCom.privateKey and sign it with callbackFedCom.publicKey
|
||||
const client = AuthenticationClientFactory.getInstance(homeFedCom)
|
||||
// eslint-disable-next-line camelcase
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
const authenticationArgs = new AuthenticationArgs()
|
||||
authenticationArgs.oneTimeCode = oneTimeCode
|
||||
// TODO encrypt callbackArgs.url with requestedCom.publicKey and sign it with homeCom.privateKey
|
||||
if (homeCom.communityUuid) {
|
||||
authenticationArgs.uuid = homeCom.communityUuid
|
||||
}
|
||||
logger.debug(`Authentication: vor authenticate()...`, authenticationArgs)
|
||||
const fedComUuid = await client.authenticate(authenticationArgs)
|
||||
logger.debug(`Authentication: nach authenticate()...`, fedComUuid)
|
||||
if (fedComUuid !== null) {
|
||||
// TODO decrypt fedComUuid with callbackFedCom.publicKey
|
||||
const callbackCom = await DbCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
publicKey: callbackFedCom.publicKey,
|
||||
})
|
||||
callbackCom.communityUuid = fedComUuid
|
||||
callbackCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(callbackCom)
|
||||
logger.debug('Authentication: Community Authentication successful:', callbackCom)
|
||||
} else {
|
||||
logger.error('Authentication: Community Authentication failed:', authenticationArgs)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('Authentication: error in startOpenConnectionCallback:', err)
|
||||
}
|
||||
|
||||
@ -11,6 +11,14 @@ const schema = async (): Promise<GraphQLSchema> => {
|
||||
resolvers: [getApiResolvers()],
|
||||
// authChecker: isAuthorized,
|
||||
scalarsMap: [{ type: Decimal, scalar: DecimalScalar }],
|
||||
validate: {
|
||||
validationError: { target: false },
|
||||
skipMissingProperties: true,
|
||||
skipNullProperties: true,
|
||||
skipUndefinedProperties: false,
|
||||
forbidUnknownValues: true,
|
||||
stopAtFirstError: true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
|
||||
expect.extend({
|
||||
decimalEqual(received, value) {
|
||||
const pass = new Decimal(value).equals(received.toString())
|
||||
if (pass) {
|
||||
return {
|
||||
message: () => `expected ${received} to not equal ${value}`,
|
||||
pass: true,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
message: () => `expected ${received} to equal ${value}`,
|
||||
pass: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
interface CustomMatchers<R = unknown> {
|
||||
decimalEqual(value: number): R
|
||||
}
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace jest {
|
||||
interface Expect extends CustomMatchers {}
|
||||
interface Matchers<R> extends CustomMatchers<R> {}
|
||||
interface InverseAsymmetricMatchers extends CustomMatchers {}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import { contributionDateFormatter } from '@test/helpers'
|
||||
|
||||
describe('contributionDateFormatter', () => {
|
||||
it('formats the date correctly', () => {
|
||||
expect(contributionDateFormatter(new Date('Thu Feb 29 2024 13:12:11'))).toEqual('2/29/2024')
|
||||
})
|
||||
})
|
||||
@ -1,62 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/unbound-method */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-return */
|
||||
import { entities } from '@entity/index'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
|
||||
import { createServer } from '@/server/createServer'
|
||||
|
||||
import { logger } from './testSetup'
|
||||
|
||||
export const headerPushMock = jest.fn((t) => {
|
||||
context.token = t.value
|
||||
})
|
||||
|
||||
const context = {
|
||||
token: '',
|
||||
setHeaders: {
|
||||
push: headerPushMock,
|
||||
forEach: jest.fn(),
|
||||
},
|
||||
clientTimezoneOffset: 0,
|
||||
}
|
||||
|
||||
export const cleanDB = async () => {
|
||||
// this only works as long we do not have foreign key constraints
|
||||
for (const entity of entities) {
|
||||
await resetEntity(entity)
|
||||
}
|
||||
}
|
||||
|
||||
export const testEnvironment = async (testLogger = logger) => {
|
||||
const server = await createServer(testLogger) // context, testLogger, testI18n)
|
||||
const con = server.con
|
||||
const testClient = createTestClient(server.apollo)
|
||||
const mutate = testClient.mutate
|
||||
const query = testClient.query
|
||||
return { mutate, query, con }
|
||||
}
|
||||
|
||||
export const resetEntity = async (entity: any) => {
|
||||
const items = await entity.find({ withDeleted: true })
|
||||
if (items.length > 0) {
|
||||
const ids = items.map((e: any) => e.id)
|
||||
await entity.delete(ids)
|
||||
}
|
||||
}
|
||||
|
||||
export const resetToken = () => {
|
||||
context.token = ''
|
||||
}
|
||||
|
||||
// format date string as it comes from the frontend for the contribution date
|
||||
export const contributionDateFormatter = (date: Date): string => {
|
||||
return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`
|
||||
}
|
||||
|
||||
export const setClientTimezoneOffset = (offset: number): void => {
|
||||
context.clientTimezoneOffset = offset
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import { federationLogger as logger } from '@/server/logger'
|
||||
|
||||
jest.setTimeout(1000000)
|
||||
|
||||
jest.mock('@/server/logger', () => {
|
||||
const originalModule = jest.requireActual('@/server/logger')
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
backendLogger: {
|
||||
addContext: jest.fn(),
|
||||
trace: jest.fn(),
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: jest.fn(),
|
||||
error: jest.fn(),
|
||||
fatal: jest.fn(),
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export { logger }
|
||||
@ -3058,7 +3058,7 @@ graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0:
|
||||
dependencies:
|
||||
iterall "^1.3.0"
|
||||
|
||||
graphql-tag@^2.11.0:
|
||||
graphql-tag@2.12.6, graphql-tag@^2.11.0:
|
||||
version "2.12.6"
|
||||
resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.12.6.tgz#d441a569c1d2537ef10ca3d1633b48725329b5f1"
|
||||
integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user