mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge pull request #3510 from gradido/3505-feature-introduce-encrypted-jwts-in-backend-federation-communication
feat(backend): introduce encrypted jwts in backend federation communication
This commit is contained in:
commit
76f904e9df
@ -1,72 +0,0 @@
|
||||
import { SignJWT, decodeJwt, jwtVerify } from 'jose'
|
||||
|
||||
import { LogError } from '@/server/LogError'
|
||||
|
||||
import { JwtPayloadType } from './payloadtypes/JwtPayloadType'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT`)
|
||||
|
||||
export const verify = async (token: string, signkey: string): Promise<JwtPayloadType | null> => {
|
||||
if (!token) {
|
||||
throw new LogError('401 Unauthorized')
|
||||
}
|
||||
logger.info('JWT.verify... token, signkey=', token, signkey)
|
||||
|
||||
try {
|
||||
/*
|
||||
const { KeyObject } = await import('node:crypto')
|
||||
const cryptoKey = await crypto.subtle.importKey('raw', signkey, { name: 'RS256' }, false, [
|
||||
'sign',
|
||||
])
|
||||
const keyObject = KeyObject.from(cryptoKey)
|
||||
logger.info('JWT.verify... keyObject=', keyObject)
|
||||
logger.info('JWT.verify... keyObject.asymmetricKeyDetails=', keyObject.asymmetricKeyDetails)
|
||||
logger.info('JWT.verify... keyObject.asymmetricKeyType=', keyObject.asymmetricKeyType)
|
||||
logger.info('JWT.verify... keyObject.asymmetricKeySize=', keyObject.asymmetricKeySize)
|
||||
*/
|
||||
const secret = new TextEncoder().encode(signkey)
|
||||
const { payload } = await jwtVerify(token, secret, {
|
||||
issuer: 'urn:gradido:issuer',
|
||||
audience: 'urn:gradido:audience',
|
||||
})
|
||||
logger.info('JWT.verify after jwtVerify... payload=', payload)
|
||||
return payload as JwtPayloadType
|
||||
} catch (err) {
|
||||
logger.error('JWT.verify after jwtVerify... error=', err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const encode = async (payload: JwtPayloadType, signkey: string): Promise<string> => {
|
||||
logger.info('JWT.encode... payload=', payload)
|
||||
logger.info('JWT.encode... signkey=', signkey)
|
||||
try {
|
||||
const secret = new TextEncoder().encode(signkey)
|
||||
const token = await new SignJWT({ payload, 'urn:gradido:claim': true })
|
||||
.setProtectedHeader({
|
||||
alg: 'HS256',
|
||||
})
|
||||
.setIssuedAt()
|
||||
.setIssuer('urn:gradido:issuer')
|
||||
.setAudience('urn:gradido:audience')
|
||||
.setExpirationTime(payload.expiration)
|
||||
.sign(secret)
|
||||
return token
|
||||
} catch (e) {
|
||||
logger.error('Failed to sign JWT:', e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export const verifyJwtType = async (token: string, signkey: string): Promise<string> => {
|
||||
const payload = await verify(token, signkey)
|
||||
return payload ? payload.tokentype : 'unknown token type'
|
||||
}
|
||||
|
||||
export const decode = (token: string): JwtPayloadType => {
|
||||
const { payload } = decodeJwt(token)
|
||||
return payload as JwtPayloadType
|
||||
}
|
||||
@ -42,7 +42,7 @@ export const schema = Joi.object({
|
||||
OPENAI_ACTIVE,
|
||||
PRODUCTION,
|
||||
|
||||
COMMUNITY_REDEEM_URL: Joi.string()
|
||||
COMMUNITY_REDEEM_URL: Joi.string()
|
||||
.uri({ scheme: ['http', 'https'] })
|
||||
.description('The url for redeeming link transactions, must start with frontend base url')
|
||||
.default('http://0.0.0.0/redeem/')
|
||||
|
||||
@ -1,61 +1,78 @@
|
||||
import { Community as DbCommunity, FederatedCommunity as DbFederatedCommunity } from 'database'
|
||||
import { CommunityLoggingView, Community as DbCommunity, FederatedCommunity as DbFederatedCommunity, FederatedCommunityLoggingView, getHomeCommunity } from 'database'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
|
||||
import { randombytes_random } from 'sodium-native'
|
||||
import { CONFIG } from '@/config'
|
||||
|
||||
import { AuthenticationClient as V1_0_AuthenticationClient } from '@/federation/client/1_0/AuthenticationClient'
|
||||
import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
import { getLogger } from 'log4js'
|
||||
import { OpenConnectionArgs } from './client/1_0/model/OpenConnectionArgs'
|
||||
import { AuthenticationClientFactory } from './client/AuthenticationClientFactory'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { encryptAndSign, OpenConnectionJwtPayloadType } from 'shared'
|
||||
import { getLogger } from 'log4js'
|
||||
import { AuthenticationClientFactory } from './client/AuthenticationClientFactory'
|
||||
import { EncryptedTransferArgs } from 'core'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities`)
|
||||
|
||||
export async function startCommunityAuthentication(
|
||||
foreignFedCom: DbFederatedCommunity,
|
||||
fedComB: DbFederatedCommunity,
|
||||
): Promise<void> {
|
||||
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
const homeFedCom = await DbFederatedCommunity.findOneByOrFail({
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.authenticateCommunities.startCommunityAuthentication`)
|
||||
const handshakeID = randombytes_random().toString()
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`startCommunityAuthentication()...`, {
|
||||
fedComB: new FederatedCommunityLoggingView(fedComB),
|
||||
})
|
||||
const homeComA = await getHomeCommunity()
|
||||
methodLogger.debug('homeComA', new CommunityLoggingView(homeComA!))
|
||||
const homeFedComA = await DbFederatedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: CONFIG.FEDERATION_BACKEND_SEND_ON_API,
|
||||
})
|
||||
const foreignCom = await DbCommunity.findOneByOrFail({ publicKey: foreignFedCom.publicKey })
|
||||
logger.debug(
|
||||
'Authentication: started with foreignFedCom:',
|
||||
foreignFedCom.endPoint,
|
||||
foreignFedCom.publicKey.toString('hex'),
|
||||
)
|
||||
// check if communityUuid is a valid v4Uuid and not still a temporary onetimecode
|
||||
if (
|
||||
foreignCom &&
|
||||
((foreignCom.communityUuid === null && foreignCom.authenticatedAt === null) ||
|
||||
(foreignCom.communityUuid !== null &&
|
||||
!validateUUID(foreignCom.communityUuid) &&
|
||||
versionUUID(foreignCom.communityUuid) !== 4))
|
||||
) {
|
||||
try {
|
||||
const client = AuthenticationClientFactory.getInstance(foreignFedCom)
|
||||
methodLogger.debug('homeFedComA', new FederatedCommunityLoggingView(homeFedComA))
|
||||
const comB = await DbCommunity.findOneByOrFail({ publicKey: fedComB.publicKey })
|
||||
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)))
|
||||
) {
|
||||
methodLogger.debug('comB.uuid is null or is a not valid v4Uuid...', comB.communityUuid || 'null', comB.authenticatedAt || 'null')
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
const args = new OpenConnectionArgs()
|
||||
args.publicKey = homeCom.publicKey.toString('hex')
|
||||
// TODO encrypt url with foreignCom.publicKey and sign it with homeCom.privateKey
|
||||
args.url = ensureUrlEndsWithSlash(homeFedCom.endPoint).concat(homeFedCom.apiVersion)
|
||||
logger.debug(
|
||||
'Authentication: before client.openConnection() args:',
|
||||
homeCom.publicKey.toString('hex'),
|
||||
args.url,
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error('Public JWT key still not exist for comB ' + comB.name)
|
||||
}
|
||||
//create JWT with url in payload encrypted by foreignCom.publicJwtKey and signed with homeCom.privateJwtKey
|
||||
const payload = new OpenConnectionJwtPayloadType(handshakeID,
|
||||
ensureUrlEndsWithSlash(homeFedComA.endPoint).concat(homeFedComA.apiVersion),
|
||||
)
|
||||
if (await client.openConnection(args)) {
|
||||
logger.debug(`Authentication: successful initiated at community:`, foreignFedCom.endPoint)
|
||||
methodLogger.debug('payload', payload)
|
||||
const jws = await encryptAndSign(payload, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
methodLogger.debug('jws', jws)
|
||||
// prepare the args for the client invocation
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComA!.publicKey.toString('hex')
|
||||
args.jwt = jws
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug('before client.openConnection() args:', args)
|
||||
const result = await client.openConnection(args)
|
||||
if (result) {
|
||||
methodLogger.debug(`successful initiated at community:`, fedComB.endPoint)
|
||||
} else {
|
||||
logger.error(`Authentication: can't initiate at community:`, foreignFedCom.endPoint)
|
||||
methodLogger.error(`can't initiate at community:`, fedComB.endPoint)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`Error:`, err)
|
||||
} else {
|
||||
methodLogger.debug(`comB.communityUuid is already a valid v4Uuid ${ comB.communityUuid || 'null' } and was authenticated at ${ comB.authenticatedAt || 'null'}`)
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error(`Error:`, err)
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
@ -5,7 +5,7 @@ import { ensureUrlEndsWithSlash } from '@/util/utilities'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLogger } from 'log4js'
|
||||
import { OpenConnectionArgs } from './model/OpenConnectionArgs'
|
||||
import { EncryptedTransferArgs } from 'core/src/graphql/model/EncryptedTransferArgs'
|
||||
import { openConnection } from './query/openConnection'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.client.1_0.AuthenticationClient`)
|
||||
@ -27,7 +27,7 @@ export class AuthenticationClient {
|
||||
})
|
||||
}
|
||||
|
||||
async openConnection(args: OpenConnectionArgs): Promise<boolean | undefined> {
|
||||
async openConnection(args: EncryptedTransferArgs): Promise<boolean | undefined> {
|
||||
logger.debug(`openConnection at ${this.endpoint} for args:`, args)
|
||||
try {
|
||||
const { data } = await this.client.rawRequest<{ openConnection: boolean }>(openConnection, {
|
||||
|
||||
@ -13,6 +13,7 @@ export class PublicCommunityInfoLoggingView extends AbstractLoggingView {
|
||||
description: this.self.description,
|
||||
creationDate: this.dateToString(this.self.creationDate),
|
||||
publicKey: this.self.publicKey,
|
||||
publicJwtKey: this.self.publicJwtKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,5 +6,5 @@ export class OpenConnectionArgs {
|
||||
publicKey: string
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
jwt: string
|
||||
}
|
||||
|
||||
@ -3,4 +3,5 @@ export interface PublicCommunityInfo {
|
||||
description: string
|
||||
creationDate: Date
|
||||
publicKey: string
|
||||
publicJwtKey: string
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ export const getPublicCommunityInfo = gql`
|
||||
description
|
||||
creationDate
|
||||
publicKey
|
||||
publicJwtKey
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const openConnection = gql`
|
||||
mutation ($args: OpenConnectionArgs!) {
|
||||
mutation ($args: EncryptedTransferArgs!) {
|
||||
openConnection(data: $args)
|
||||
}
|
||||
`
|
||||
|
||||
@ -343,7 +343,7 @@ describe('validate Communities', () => {
|
||||
})
|
||||
it('logs unsupported api for community with api 2_0 ', () => {
|
||||
expect(logger.debug).toBeCalledWith(
|
||||
'dbCom with unsupported apiVersion',
|
||||
'dbFedComB with unsupported apiVersion',
|
||||
dbCom.endPoint,
|
||||
'2_0',
|
||||
)
|
||||
|
||||
@ -2,18 +2,20 @@ import {
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFederatedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
} from 'database'
|
||||
import { IsNull } from 'typeorm'
|
||||
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { FederationClient as V1_0_FederationClient } from '@/federation/client/1_0/FederationClient'
|
||||
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 { getLogger } from 'log4js'
|
||||
import { startCommunityAuthentication } from './authenticateCommunities'
|
||||
import { PublicCommunityInfoLoggingView } from './client/1_0/logging/PublicCommunityInfoLogging.view'
|
||||
import { ApiVersionType } from './enum/apiVersionType'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.federation.validateCommunities`)
|
||||
|
||||
@ -34,6 +36,7 @@ export async function startValidateCommunities(timerInterval: number): Promise<v
|
||||
}
|
||||
|
||||
export async function validateCommunities(): Promise<void> {
|
||||
// search all foreign federated communities which are still not verified or have not been verified since last dht-announcement
|
||||
const dbFederatedCommunities: DbFederatedCommunity[] =
|
||||
await DbFederatedCommunity.createQueryBuilder()
|
||||
.where({ foreign: true, verifiedAt: IsNull() })
|
||||
@ -41,32 +44,36 @@ export async function validateCommunities(): Promise<void> {
|
||||
.getMany()
|
||||
|
||||
logger.debug(`found ${dbFederatedCommunities.length} dbCommunities`)
|
||||
for (const dbCom of dbFederatedCommunities) {
|
||||
logger.debug('dbCom', new FederatedCommunityLoggingView(dbCom))
|
||||
for (const dbFedComB of dbFederatedCommunities) {
|
||||
logger.debug('dbFedComB', new FederatedCommunityLoggingView(dbFedComB))
|
||||
const apiValueStrings: string[] = Object.values(ApiVersionType)
|
||||
logger.debug(`suppported ApiVersions=`, apiValueStrings)
|
||||
if (!apiValueStrings.includes(dbCom.apiVersion)) {
|
||||
logger.debug('dbCom with unsupported apiVersion', dbCom.endPoint, dbCom.apiVersion)
|
||||
if (!apiValueStrings.includes(dbFedComB.apiVersion)) {
|
||||
logger.debug('dbFedComB with unsupported apiVersion', dbFedComB.endPoint, dbFedComB.apiVersion)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
const client = FederationClientFactory.getInstance(dbCom)
|
||||
const client = FederationClientFactory.getInstance(dbFedComB)
|
||||
|
||||
if (client instanceof V1_0_FederationClient) {
|
||||
const pubKey = await client.getPublicKey()
|
||||
if (pubKey && pubKey === dbCom.publicKey.toString('hex')) {
|
||||
await DbFederatedCommunity.update({ id: dbCom.id }, { verifiedAt: new Date() })
|
||||
logger.debug(`verified community with:`, dbCom.endPoint)
|
||||
if (pubKey && pubKey === dbFedComB.publicKey.toString('hex')) {
|
||||
await DbFederatedCommunity.update({ id: dbFedComB.id }, { verifiedAt: new Date() })
|
||||
logger.debug(`verified dbFedComB with:`, dbFedComB.endPoint)
|
||||
const pubComInfo = await client.getPublicCommunityInfo()
|
||||
if (pubComInfo) {
|
||||
await writeForeignCommunity(dbCom, pubComInfo)
|
||||
await startCommunityAuthentication(dbCom)
|
||||
logger.debug(`write publicInfo of community: name=${pubComInfo.name}`)
|
||||
await writeForeignCommunity(dbFedComB, pubComInfo)
|
||||
logger.debug(`wrote response of getPublicCommunityInfo in dbFedComB ${dbFedComB.endPoint}`)
|
||||
try {
|
||||
await startCommunityAuthentication(dbFedComB)
|
||||
} catch (err) {
|
||||
logger.warn(`Warning: Authentication of community ${dbFedComB.endPoint} still ongoing:`, err)
|
||||
}
|
||||
} else {
|
||||
logger.debug('missing result of getPublicCommunityInfo')
|
||||
}
|
||||
} else {
|
||||
logger.debug('received not matching publicKey:', pubKey, dbCom.publicKey.toString('hex'))
|
||||
logger.debug('received not matching publicKey:', pubKey, dbFedComB.publicKey.toString('hex'))
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@ -75,6 +82,36 @@ export async function validateCommunities(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJwtKeyPairInHomeCommunity(): Promise<DbCommunity> {
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity`)
|
||||
try {
|
||||
// check for existing homeCommunity entry
|
||||
let homeCom = await getHomeCommunity()
|
||||
if (homeCom) {
|
||||
if (!homeCom.publicJwtKey && !homeCom.privateJwtKey) {
|
||||
// Generate key pair using jose library
|
||||
const { publicKey, privateKey } = await createKeyPair();
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicKey=`, publicKey);
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateKey=`, privateKey.slice(0, 20));
|
||||
|
||||
homeCom.publicJwtKey = publicKey;
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity publicJwtKey.length=`, homeCom.publicJwtKey.length);
|
||||
homeCom.privateJwtKey = privateKey;
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity privateJwtKey.length=`, homeCom.privateJwtKey.length);
|
||||
await DbCommunity.save(homeCom)
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity done`)
|
||||
} else {
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity: keypair already exists`)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Error! A HomeCommunity-Entry still not exist! Please start the DHT-Modul first.`)
|
||||
}
|
||||
return homeCom
|
||||
} catch (err) {
|
||||
throw new Error(`Error writing JwtKeyPair in HomeCommunity-Entry: ${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
async function writeForeignCommunity(
|
||||
dbCom: DbFederatedCommunity,
|
||||
pubInfo: PublicCommunityInfo,
|
||||
@ -96,6 +133,7 @@ async function writeForeignCommunity(
|
||||
com.foreign = true
|
||||
com.name = pubInfo.name
|
||||
com.publicKey = dbCom.publicKey
|
||||
com.publicJwtKey = pubInfo.publicJwtKey
|
||||
com.url = dbCom.endPoint
|
||||
await DbCommunity.save(com)
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Decimal } from 'decimal.js-light'
|
||||
import { Field, ObjectType } from 'type-graphql'
|
||||
|
||||
import { RedeemJwtPayloadType } from '@/auth/jwt/payloadtypes/RedeemJwtPayloadType'
|
||||
import { RedeemJwtPayloadType } from 'shared'
|
||||
|
||||
import { Community } from './Community'
|
||||
import { User } from './User'
|
||||
|
||||
@ -27,8 +27,6 @@ import { Decimal } from 'decimal.js-light'
|
||||
import { Arg, Args, Authorized, Ctx, Int, Mutation, Query, Resolver } from 'type-graphql'
|
||||
|
||||
import { RIGHTS } from '@/auth/RIGHTS'
|
||||
import { decode, encode, verify } from '@/auth/jwt/JWT'
|
||||
import { RedeemJwtPayloadType } from '@/auth/jwt/payloadtypes/RedeemJwtPayloadType'
|
||||
import {
|
||||
EVENT_CONTRIBUTION_LINK_REDEEM,
|
||||
EVENT_TRANSACTION_LINK_CREATE,
|
||||
@ -39,13 +37,12 @@ import { LogError } from '@/server/LogError'
|
||||
import { Context, getClientTimezoneOffset, getUser } from '@/server/context'
|
||||
import { TRANSACTIONS_LOCK } from '@/util/TRANSACTIONS_LOCK'
|
||||
import { TRANSACTION_LINK_LOCK } from '@/util/TRANSACTION_LINK_LOCK'
|
||||
import { calculateDecay } from 'shared'
|
||||
import { fullName } from '@/util/utilities'
|
||||
import { calculateBalance } from '@/util/validate'
|
||||
import { calculateDecay, decode, DisburseJwtPayloadType, encode, RedeemJwtPayloadType, verify } from 'shared'
|
||||
|
||||
import { DisburseJwtPayloadType } from '@/auth/jwt/payloadtypes/DisburseJwtPayloadType'
|
||||
import { Logger, getLogger } from 'log4js'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { getLogger, Logger } from 'log4js'
|
||||
import { executeTransaction } from './TransactionResolver'
|
||||
import {
|
||||
getAuthenticatedCommunities,
|
||||
@ -573,7 +570,7 @@ export class TransactionLinkResolver {
|
||||
throw new LogError('Sender community UUID is not set')
|
||||
}
|
||||
// now with the sender community UUID the jwt token can be verified
|
||||
const verifiedJwtPayload = await verify(code, senderCom.communityUuid)
|
||||
const verifiedJwtPayload = await verify('handshakeID', code, senderCom.communityUuid)
|
||||
logger.debug(
|
||||
'TransactionLinkResolver.queryRedeemJwtLink... nach verify verifiedJwtPayload=',
|
||||
verifiedJwtPayload,
|
||||
|
||||
@ -4,12 +4,14 @@ import { getLogger } from 'log4js'
|
||||
import { CONFIG } from './config'
|
||||
import { startValidateCommunities } from './federation/validateCommunities'
|
||||
import { createServer } from './server/createServer'
|
||||
import { writeJwtKeyPairInHomeCommunity } from './federation/validateCommunities'
|
||||
import { initLogging } from './server/logger'
|
||||
|
||||
async function main() {
|
||||
initLogging()
|
||||
const { app } = await createServer(getLogger('apollo'))
|
||||
|
||||
await writeJwtKeyPairInHomeCommunity()
|
||||
app.listen(CONFIG.PORT, () => {
|
||||
// biome-ignore lint/suspicious/noConsole: no need for logging the start message
|
||||
console.log(`Server is running at http://localhost:${CONFIG.PORT}`)
|
||||
|
||||
1
bun.lock
1
bun.lock
@ -6,6 +6,7 @@
|
||||
"dependencies": {
|
||||
"auto-changelog": "^2.4.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jose": "^4.14.4",
|
||||
"turbo": "^2.5.0",
|
||||
"uuid": "^8.3.2",
|
||||
},
|
||||
|
||||
@ -27,12 +27,15 @@
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
"@types/node": "^17.0.21",
|
||||
"type-graphql": "^1.1.1",
|
||||
"typescript": "^4.9.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"database": "*",
|
||||
"esbuild": "^0.25.2",
|
||||
"jose": "^4.14.4",
|
||||
"log4js": "^6.9.1",
|
||||
"shared": "*",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@ -1 +1 @@
|
||||
export const LOG4JS_BASE_CATEGORY_NAME = 'core'
|
||||
export const LOG4JS_BASE_CATEGORY_NAME = 'core'
|
||||
|
||||
42
core/src/graphql/logic/interpretEncryptedTransferArgs.ts
Normal file
42
core/src/graphql/logic/interpretEncryptedTransferArgs.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { EncryptedTransferArgs } from '../model/EncryptedTransferArgs'
|
||||
import { JwtPayloadType } from 'shared'
|
||||
import { Community as DbCommunity } from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { CommunityLoggingView, getHomeCommunity } from 'database'
|
||||
import { verifyAndDecrypt } from 'shared'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../../config/const'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.interpretEncryptedTransferArgs`)
|
||||
|
||||
export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise<JwtPayloadType | null> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.resolver.util.interpretEncryptedTransferArgs-method`)
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug('interpretEncryptedTransferArgs()... args:', args)
|
||||
// first find with args.publicKey the community 'requestingCom', which starts the request
|
||||
const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
if (!requestingCom) {
|
||||
const errmsg = `unknown requesting community with publicKey ${args.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
if (!requestingCom.publicJwtKey) {
|
||||
const errmsg = `missing publicJwtKey of requesting community with publicKey ${args.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
methodLogger.debug(`found requestingCom:`, new CommunityLoggingView(requestingCom))
|
||||
// verify the signing of args.jwt with homeCom.privateJwtKey and decrypt args.jwt with requestingCom.publicJwtKey
|
||||
const homeCom = await getHomeCommunity()
|
||||
const jwtPayload = await verifyAndDecrypt(args.handshakeID, args.jwt, homeCom!.privateJwtKey!, requestingCom.publicJwtKey) as JwtPayloadType
|
||||
if (!jwtPayload) {
|
||||
const errmsg = `invalid payload of community with publicKey ${args.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
methodLogger.debug('jwtPayload', jwtPayload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jwtPayload
|
||||
}
|
||||
@ -1,10 +1,13 @@
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class OpenConnectionArgs {
|
||||
export class EncryptedTransferArgs {
|
||||
@Field(() => String)
|
||||
handshakeID: string
|
||||
|
||||
@Field(() => String)
|
||||
publicKey: string
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
jwt: string
|
||||
}
|
||||
@ -1 +1,3 @@
|
||||
export * from './validation/user'
|
||||
export * from './validation/user'
|
||||
export * from './graphql/logic/interpretEncryptedTransferArgs'
|
||||
export * from './graphql/model/EncryptedTransferArgs'
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
/* MIGRATION TO ADD JWT-KEYPAIR IN COMMUNITY TABLE
|
||||
*
|
||||
* This migration adds fields for the jwt-keypair in the community.table
|
||||
*/
|
||||
|
||||
export async function upgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn(
|
||||
'ALTER TABLE `communities` ADD COLUMN `public_jwt_key` varchar(512) DEFAULT NULL AFTER `gms_api_key`;',
|
||||
)
|
||||
await queryFn(
|
||||
'ALTER TABLE `communities` ADD COLUMN `private_jwt_key` varchar(2048) DEFAULT NULL AFTER `public_jwt_key`;',
|
||||
)
|
||||
}
|
||||
|
||||
export async function downgrade(queryFn: (query: string, values?: any[]) => Promise<Array<any>>) {
|
||||
await queryFn('ALTER TABLE `communities` DROP COLUMN `public_jwt_key`;')
|
||||
await queryFn('ALTER TABLE `communities` DROP COLUMN `private_jwt_key`;')
|
||||
}
|
||||
@ -54,6 +54,12 @@ export class Community extends BaseEntity {
|
||||
@Column({ name: 'gms_api_key', type: 'varchar', length: 512, nullable: true, default: null })
|
||||
gmsApiKey: string | null
|
||||
|
||||
@Column({ name: 'public_jwt_key', type: 'varchar', length: 512, nullable: true })
|
||||
publicJwtKey: string | null
|
||||
|
||||
@Column({ name: 'private_jwt_key', type: 'varchar', length: 2048, nullable: true })
|
||||
privateJwtKey: string | null
|
||||
|
||||
@Column({
|
||||
name: 'location',
|
||||
type: 'geometry',
|
||||
|
||||
@ -13,6 +13,7 @@ export class CommunityLoggingView extends AbstractLoggingView {
|
||||
foreign: this.self.foreign,
|
||||
url: this.self.url,
|
||||
publicKey: this.self.publicKey.toString(this.bufferStringFormat),
|
||||
publicJwtKey: this.self.publicJwtKey,
|
||||
communityUuid: this.self.communityUuid,
|
||||
authenticatedAt: this.dateToString(this.self.authenticatedAt),
|
||||
name: this.self.name,
|
||||
|
||||
@ -42,6 +42,7 @@
|
||||
"await-semaphore": "0.1.3",
|
||||
"class-validator": "^0.13.2",
|
||||
"config-schema": "*",
|
||||
"core": "*",
|
||||
"cors": "2.8.5",
|
||||
"database": "*",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
import { FederatedCommunity as DbFederatedCommunity } from 'database'
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import { getLogger } from 'log4js'
|
||||
import { getLogger, Logger } from 'log4js'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
|
||||
import { AuthenticationArgs } from '@/graphql/api/1_0/model/AuthenticationArgs'
|
||||
import { OpenConnectionCallbackArgs } from '@/graphql/api/1_0/model/OpenConnectionCallbackArgs'
|
||||
import { EncryptedTransferArgs } from 'core/src/graphql/model/EncryptedTransferArgs'
|
||||
import { authenticate } from './query/authenticate'
|
||||
import { openConnectionCallback } from './query/openConnectionCallback'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.client.1_0.AuthenticationClient`)
|
||||
|
||||
export class AuthenticationClient {
|
||||
dbCom: DbFederatedCommunity
|
||||
endpoint: string
|
||||
@ -29,36 +26,41 @@ export class AuthenticationClient {
|
||||
})
|
||||
}
|
||||
|
||||
async openConnectionCallback(args: OpenConnectionCallbackArgs): Promise<boolean> {
|
||||
logger.debug('openConnectionCallback with endpoint', this.endpoint, args)
|
||||
async openConnectionCallback(args: EncryptedTransferArgs): Promise<boolean> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.client.1_0.AuthenticationClient.openConnectionCallback`)
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug('openConnectionCallback with endpoint', this.endpoint, args)
|
||||
try {
|
||||
const { data } = await this.client.rawRequest<any>(openConnectionCallback, { args })
|
||||
methodLogger.debug('after openConnectionCallback: data:', data)
|
||||
|
||||
if (data && data.openConnectionCallback) {
|
||||
logger.warn('openConnectionCallback without response data from endpoint', this.endpoint)
|
||||
if (!data || !data.openConnectionCallback) {
|
||||
methodLogger.warn('openConnectionCallback without response data from endpoint', this.endpoint)
|
||||
return false
|
||||
}
|
||||
logger.debug('openConnectionCallback successfully started with endpoint', this.endpoint)
|
||||
methodLogger.debug('openConnectionCallback successfully started with endpoint', this.endpoint)
|
||||
return true
|
||||
} catch (err) {
|
||||
logger.error('error on openConnectionCallback', err)
|
||||
methodLogger.error('error on openConnectionCallback', err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
async authenticate(args: AuthenticationArgs): Promise<string | null> {
|
||||
logger.debug('authenticate with endpoint=', this.endpoint)
|
||||
async authenticate(args: EncryptedTransferArgs): Promise<string | null> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.client.1_0.AuthenticationClient.authenticate`)
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug('authenticate with endpoint=', this.endpoint)
|
||||
try {
|
||||
const { data } = await this.client.rawRequest<any>(authenticate, { args })
|
||||
logger.debug('after authenticate: data:', data)
|
||||
methodLogger.debug('after authenticate: data:', data)
|
||||
|
||||
const authUuid: string = data?.authenticate
|
||||
if (authUuid) {
|
||||
logger.debug('received authenticated uuid', authUuid)
|
||||
methodLogger.debug('received authenticated uuid', authUuid)
|
||||
return authUuid
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('authenticate failed', {
|
||||
methodLogger.error('authenticate failed', {
|
||||
endpoint: this.endpoint,
|
||||
err,
|
||||
})
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const authenticate = gql`
|
||||
mutation ($args: AuthenticationArgs!) {
|
||||
mutation ($args: EncryptedTransferArgs!) {
|
||||
authenticate(data: $args)
|
||||
}
|
||||
`
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { gql } from 'graphql-request'
|
||||
|
||||
export const openConnectionCallback = gql`
|
||||
mutation ($args: OpenConnectionCallbackArgs!) {
|
||||
mutation ($args: EncryptedTransferArgs!) {
|
||||
openConnectionCallback(data: $args)
|
||||
}
|
||||
`
|
||||
|
||||
@ -12,6 +12,7 @@ export class GetPublicCommunityInfoResultLoggingView extends AbstractLoggingView
|
||||
description: this.self.description,
|
||||
creationDate: this.dateToString(this.self.creationDate),
|
||||
publicKey: this.self.publicKey,
|
||||
publicJwtKey: this.self.publicJwtKey,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,9 @@ import { Field, ObjectType } from 'type-graphql'
|
||||
export class GetPublicCommunityInfoResult {
|
||||
constructor(dbCom: DbCommunity) {
|
||||
this.publicKey = dbCom.publicKey.toString('hex')
|
||||
if (dbCom.publicJwtKey) {
|
||||
this.publicJwtKey = dbCom.publicJwtKey
|
||||
}
|
||||
this.name = dbCom.name
|
||||
this.description = dbCom.description
|
||||
this.creationDate = dbCom.creationDate
|
||||
@ -22,4 +25,7 @@ export class GetPublicCommunityInfoResult {
|
||||
|
||||
@Field(() => String)
|
||||
publicKey: string
|
||||
|
||||
@Field(() => String)
|
||||
publicJwtKey: string
|
||||
}
|
||||
|
||||
@ -1,10 +0,0 @@
|
||||
import { Field, InputType } from 'type-graphql'
|
||||
|
||||
@InputType()
|
||||
export class OpenConnectionCallbackArgs {
|
||||
@Field(() => String)
|
||||
oneTimeCode: string
|
||||
|
||||
@Field(() => String)
|
||||
url: string
|
||||
}
|
||||
@ -1,17 +1,16 @@
|
||||
import { CONFIG } from '@/config'
|
||||
import { LogError } from '@/server/LogError'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { EncryptedTransferArgs, interpretEncryptedTransferArgs } from 'core'
|
||||
import {
|
||||
CommunityLoggingView,
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, OpenConnectionJwtPayloadType } from 'shared'
|
||||
import { Arg, Mutation, Resolver } from 'type-graphql'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { AuthenticationArgs } from '../model/AuthenticationArgs'
|
||||
import { OpenConnectionArgs } from '../model/OpenConnectionArgs'
|
||||
import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs'
|
||||
import { startAuthentication, startOpenConnectionCallback } from '../util/authenticateCommunity'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver`)
|
||||
@ -21,67 +20,118 @@ export class AuthenticationResolver {
|
||||
@Mutation(() => Boolean)
|
||||
async openConnection(
|
||||
@Arg('data')
|
||||
args: OpenConnectionArgs,
|
||||
args: EncryptedTransferArgs,
|
||||
): Promise<boolean> {
|
||||
const pubKeyBuf = Buffer.from(args.publicKey, 'hex')
|
||||
logger.debug(`openConnection() via apiVersion=1_0:`, args)
|
||||
|
||||
// first find with args.publicKey the community 'comA', which starts openConnection request
|
||||
const comA = await DbCommunity.findOneBy({
|
||||
publicKey: pubKeyBuf, // Buffer.from(args.publicKey),
|
||||
})
|
||||
if (!comA) {
|
||||
throw new LogError(`unknown requesting community with publicKey`, pubKeyBuf.toString('hex'))
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.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)
|
||||
}
|
||||
logger.debug(`found requestedCom:`, new CommunityLoggingView(comA))
|
||||
// biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke callback-request asynchronously
|
||||
void startOpenConnectionCallback(args, comA, CONFIG.FEDERATION_API)
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async openConnectionCallback(
|
||||
@Arg('data')
|
||||
args: OpenConnectionCallbackArgs,
|
||||
args: EncryptedTransferArgs,
|
||||
): Promise<boolean> {
|
||||
logger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args)
|
||||
// TODO decrypt args.url with homeCom.privateKey and verify signing with callbackFedCom.publicKey
|
||||
const endPoint = args.url.slice(0, args.url.lastIndexOf('/') + 1)
|
||||
const apiVersion = args.url.slice(args.url.lastIndexOf('/') + 1, args.url.length)
|
||||
logger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.openConnectionCallback`)
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`openConnectionCallback() via apiVersion=1_0 ...`, args)
|
||||
// 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 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) {
|
||||
throw new LogError(`unknown callback community with url`, args.url)
|
||||
const errmsg = `unknown callback community with url` + openConnectionCallbackJwtPayload.url
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
logger.debug(
|
||||
methodLogger.debug(
|
||||
`found fedComB and start authentication:`,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
)
|
||||
// biome-ignore lint/complexity/noVoid: no await to respond immediately and invoke authenticate-request asynchronously
|
||||
void startAuthentication(args.oneTimeCode, 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)
|
||||
async authenticate(
|
||||
@Arg('data')
|
||||
args: AuthenticationArgs,
|
||||
args: EncryptedTransferArgs,
|
||||
): Promise<string | null> {
|
||||
logger.debug(`authenticate() via apiVersion=1_0 ...`, args)
|
||||
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: args.oneTimeCode })
|
||||
logger.debug('found authCom:', new CommunityLoggingView(authCom))
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.resolver.AuthenticationResolver.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) {
|
||||
// TODO decrypt args.uuid with authCom.publicKey
|
||||
authCom.communityUuid = args.uuid
|
||||
authCom.communityUuid = authArgs.uuid
|
||||
authCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(authCom)
|
||||
logger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
|
||||
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
// TODO encrypt homeCom.uuid with homeCom.privateKey
|
||||
if (homeCom.communityUuid) {
|
||||
return homeCom.communityUuid
|
||||
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
|
||||
}
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,46 +1,50 @@
|
||||
import { EncryptedTransferArgs } from 'core'
|
||||
import {
|
||||
CommunityLoggingView,
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import { OpenConnectionArgs } from '../model/OpenConnectionArgs'
|
||||
import { OpenConnectionCallbackArgs } from '../model/OpenConnectionCallbackArgs'
|
||||
import { validate as validateUUID, version as versionUUID } from 'uuid'
|
||||
|
||||
import { AuthenticationClientFactory } from '@/client/AuthenticationClientFactory'
|
||||
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 { AuthenticationArgs } from '../model/AuthenticationArgs'
|
||||
import { AuthenticationJwtPayloadType, AuthenticationResponseJwtPayloadType, encryptAndSign, OpenConnectionCallbackJwtPayloadType, verifyAndDecrypt } from 'shared'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity`)
|
||||
|
||||
export async function startOpenConnectionCallback(
|
||||
args: OpenConnectionArgs,
|
||||
comA: DbCommunity,
|
||||
handshakeID: string,
|
||||
publicKey: string,
|
||||
api: string,
|
||||
): Promise<void> {
|
||||
logger.debug(`startOpenConnectionCallback() with:`, {
|
||||
args,
|
||||
comA: new CommunityLoggingView(comA),
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startOpenConnectionCallback`)
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`Authentication: startOpenConnectionCallback() with:`, {
|
||||
publicKey,
|
||||
})
|
||||
try {
|
||||
const homeFedCom = await DbFedCommunity.findOneByOrFail({
|
||||
const homeComB = await getHomeCommunity()
|
||||
const homeFedComB = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: false,
|
||||
apiVersion: api,
|
||||
})
|
||||
const comA = await DbCommunity.findOneByOrFail({ publicKey: Buffer.from(publicKey, 'hex') })
|
||||
const fedComA = await DbFedCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
apiVersion: api,
|
||||
publicKey: comA.publicKey,
|
||||
})
|
||||
const oneTimeCode = randombytes_random()
|
||||
const oneTimeCode = randombytes_random().toString()
|
||||
// store oneTimeCode in requestedCom.community_uuid as authenticate-request-identifier
|
||||
comA.communityUuid = oneTimeCode.toString()
|
||||
comA.communityUuid = oneTimeCode
|
||||
await DbCommunity.save(comA)
|
||||
logger.debug(
|
||||
methodLogger.debug(
|
||||
`Authentication: stored oneTimeCode in requestedCom:`,
|
||||
new CommunityLoggingView(comA),
|
||||
)
|
||||
@ -48,68 +52,94 @@ export async function startOpenConnectionCallback(
|
||||
const client = AuthenticationClientFactory.getInstance(fedComA)
|
||||
|
||||
if (client instanceof V1_0_AuthenticationClient) {
|
||||
const callbackArgs = new OpenConnectionCallbackArgs()
|
||||
callbackArgs.oneTimeCode = oneTimeCode.toString()
|
||||
// TODO encrypt callbackArgs.url with requestedCom.publicKey and sign it with homeCom.privateKey
|
||||
callbackArgs.url = homeFedCom.endPoint.endsWith('/')
|
||||
? homeFedCom.endPoint + homeFedCom.apiVersion
|
||||
: homeFedCom.endPoint + '/' + homeFedCom.apiVersion
|
||||
logger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs)
|
||||
if (await client.openConnectionCallback(callbackArgs)) {
|
||||
logger.debug('startOpenConnectionCallback() successful:', callbackArgs)
|
||||
const url = homeFedComB.endPoint.endsWith('/')
|
||||
? homeFedComB.endPoint + homeFedComB.apiVersion
|
||||
: homeFedComB.endPoint + '/' + homeFedComB.apiVersion
|
||||
|
||||
const callbackArgs = new OpenConnectionCallbackJwtPayloadType(handshakeID, oneTimeCode, url)
|
||||
methodLogger.debug(`Authentication: start openConnectionCallback with args:`, callbackArgs)
|
||||
// encrypt callbackArgs with requestedCom.publicJwtKey and sign it with homeCom.privateJwtKey
|
||||
const jwt = await encryptAndSign(callbackArgs, homeComB!.privateJwtKey!, comA.publicJwtKey!)
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComB!.publicKey.toString('hex')
|
||||
args.jwt = jwt
|
||||
args.handshakeID = handshakeID
|
||||
const result = await client.openConnectionCallback(args)
|
||||
if (result) {
|
||||
methodLogger.debug('startOpenConnectionCallback() successful:', jwt)
|
||||
} else {
|
||||
logger.error('startOpenConnectionCallback() failed:', callbackArgs)
|
||||
methodLogger.error('startOpenConnectionCallback() failed:', jwt)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('error in startOpenConnectionCallback:', err)
|
||||
methodLogger.error('error in startOpenConnectionCallback:', err)
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
export async function startAuthentication(
|
||||
handshakeID: string,
|
||||
oneTimeCode: string,
|
||||
fedComB: DbFedCommunity,
|
||||
): Promise<void> {
|
||||
logger.debug(`startAuthentication()...`, {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startAuthentication`)
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`startAuthentication()...`, {
|
||||
oneTimeCode,
|
||||
fedComB: new FederatedCommunityLoggingView(fedComB),
|
||||
})
|
||||
try {
|
||||
const homeCom = await DbCommunity.findOneByOrFail({ foreign: false })
|
||||
const homeComA = await getHomeCommunity()
|
||||
const comB = await DbCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
publicKey: fedComB.publicKey,
|
||||
})
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error('Public JWT key still not exist for foreign community')
|
||||
}
|
||||
|
||||
// TODO encrypt homeCom.uuid with homeCom.privateKey and sign it with callbackFedCom.publicKey
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
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(`invoke authenticate() with:`, authenticationArgs)
|
||||
const fedComUuid = await client.authenticate(authenticationArgs)
|
||||
logger.debug(`response of authenticate():`, fedComUuid)
|
||||
if (fedComUuid !== null) {
|
||||
logger.debug(
|
||||
`received communityUUid for callbackFedCom:`,
|
||||
fedComUuid,
|
||||
const authenticationArgs = new AuthenticationJwtPayloadType(handshakeID, oneTimeCode, homeComA!.communityUuid!)
|
||||
// encrypt authenticationArgs.uuid with fedComB.publicJwtKey and sign it with homeCom.privateJwtKey
|
||||
const jwt = await encryptAndSign(authenticationArgs, homeComA!.privateJwtKey!, comB.publicJwtKey!)
|
||||
const args = new EncryptedTransferArgs()
|
||||
args.publicKey = homeComA!.publicKey.toString('hex')
|
||||
args.jwt = jwt
|
||||
args.handshakeID = handshakeID
|
||||
methodLogger.debug(`invoke authenticate() with:`, args)
|
||||
const responseJwt = await client.authenticate(args)
|
||||
methodLogger.debug(`response of authenticate():`, responseJwt)
|
||||
if (responseJwt !== null) {
|
||||
const payload = await verifyAndDecrypt(handshakeID, responseJwt, homeComA!.privateJwtKey!, comB.publicJwtKey!) as AuthenticationResponseJwtPayloadType
|
||||
methodLogger.debug(
|
||||
`received payload from authenticate ComB:`,
|
||||
payload,
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
)
|
||||
const callbackCom = await DbCommunity.findOneByOrFail({
|
||||
foreign: true,
|
||||
publicKey: fedComB.publicKey,
|
||||
})
|
||||
// TODO decrypt fedComUuid with callbackFedCom.publicKey
|
||||
callbackCom.communityUuid = fedComUuid
|
||||
callbackCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(callbackCom)
|
||||
logger.debug('Community Authentication successful:', new CommunityLoggingView(callbackCom))
|
||||
if (payload.tokentype !== AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE) {
|
||||
const errmsg = `Invalid tokentype in authenticate-response of community with publicKey` + comB.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
if (!payload.uuid || !validateUUID(payload.uuid) || versionUUID(payload.uuid) !== 4) {
|
||||
const errmsg = `Invalid uuid in authenticate-response of community with publicKey` + comB.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw new Error(errmsg)
|
||||
}
|
||||
comB.communityUuid = payload.uuid
|
||||
comB.authenticatedAt = new Date()
|
||||
await DbCommunity.save(comB)
|
||||
methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB))
|
||||
} else {
|
||||
logger.error('Community Authentication failed:', authenticationArgs)
|
||||
methodLogger.error('Community Authentication failed:', authenticationArgs)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error('error in startAuthentication:', err)
|
||||
methodLogger.error('error in startAuthentication:', err)
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
"dependencies": {
|
||||
"auto-changelog": "^2.4.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jose": "^4.14.4",
|
||||
"turbo": "^2.5.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
"dependencies": {
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"esbuild": "^0.25.2",
|
||||
"jose": "^4.14.4",
|
||||
"log4js": "^6.9.1",
|
||||
"zod": "^3.25.61"
|
||||
},
|
||||
|
||||
@ -1,2 +1,3 @@
|
||||
export const DECAY_START_TIME = new Date('2021-05-13T17:46:31Z')
|
||||
export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
|
||||
export const LOG4JS_BASE_CATEGORY_NAME = 'shared'
|
||||
export const REDEEM_JWT_TOKEN_EXPIRATION = '10m'
|
||||
@ -1,3 +1,12 @@
|
||||
export * from './schema'
|
||||
export * from './enum'
|
||||
export * from './logic/decay'
|
||||
export * from './jwt/JWT'
|
||||
export * from './jwt/payloadtypes/AuthenticationJwtPayloadType'
|
||||
export * from './jwt/payloadtypes/AuthenticationResponseJwtPayloadType'
|
||||
export * from './jwt/payloadtypes/DisburseJwtPayloadType'
|
||||
export * from './jwt/payloadtypes/EncryptedJWEJwtPayloadType'
|
||||
export * from './jwt/payloadtypes/JwtPayloadType'
|
||||
export * from './jwt/payloadtypes/OpenConnectionJwtPayloadType'
|
||||
export * from './jwt/payloadtypes/OpenConnectionCallbackJwtPayloadType'
|
||||
export * from './jwt/payloadtypes/RedeemJwtPayloadType'
|
||||
|
||||
167
shared/src/jwt/JWT.test.ts
Normal file
167
shared/src/jwt/JWT.test.ts
Normal file
@ -0,0 +1,167 @@
|
||||
|
||||
// import { testEnvironment } from '@test/helpers'
|
||||
// import { logger } from '@test/testSetup'
|
||||
|
||||
import { createKeyPair, decode, decrypt, encode, encrypt, encryptAndSign, verify, verifyAndDecrypt } from './JWT'
|
||||
import { EncryptedJWEJwtPayloadType } from './payloadtypes/EncryptedJWEJwtPayloadType'
|
||||
import { OpenConnectionJwtPayloadType } from './payloadtypes/OpenConnectionJwtPayloadType'
|
||||
|
||||
// let con: DataSource
|
||||
// let testEnv: {
|
||||
// mutate: ApolloServerTestClient['mutate']
|
||||
// query: ApolloServerTestClient['query']
|
||||
// con: DataSource
|
||||
// }
|
||||
let keypairComA: { publicKey: string; privateKey: string }
|
||||
let keypairComB: { publicKey: string; privateKey: string }
|
||||
|
||||
beforeAll(async () => {
|
||||
// testEnv = await testEnvironment(logger)
|
||||
// con = testEnv.con
|
||||
// await cleanDB()
|
||||
|
||||
keypairComA = await createKeyPair()
|
||||
keypairComB = await createKeyPair()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
// await cleanDB()
|
||||
// await con.destroy()
|
||||
})
|
||||
|
||||
describe('test JWS creation and verification', () => {
|
||||
let jwsComA: string
|
||||
let jwsComB: string
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
jwsComA = await encode(new OpenConnectionJwtPayloadType('handshakeID', 'http://localhost:5001/api/'), keypairComA.privateKey)
|
||||
jwsComB = await encode(new OpenConnectionJwtPayloadType('handshakeID', 'http://localhost:5002/api/'), keypairComB.privateKey)
|
||||
})
|
||||
it('decode jwsComA', async () => {
|
||||
const decodedJwsComA = await decode(jwsComA)
|
||||
expect(decodedJwsComA).toEqual({
|
||||
expiration: '10m',
|
||||
handshakeID: 'handshakeID',
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5001/api/',
|
||||
})
|
||||
})
|
||||
it('decode jwsComB', async () => {
|
||||
const decodedJwsComB = await decode(jwsComB)
|
||||
expect(decodedJwsComB).toEqual({
|
||||
expiration: '10m',
|
||||
handshakeID: 'handshakeID',
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5002/api/',
|
||||
})
|
||||
})
|
||||
it('verify jwsComA', async () => {
|
||||
const verifiedJwsComA = await verify('handshakeID', jwsComA, keypairComA.publicKey)
|
||||
expect(verifiedJwsComA).toEqual(expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5001/api/',
|
||||
})
|
||||
}))
|
||||
})
|
||||
it('verify jwsComB', async () => {
|
||||
const verifiedJwsComB = await verify('handshakeID', jwsComB, keypairComB.publicKey)
|
||||
expect(verifiedJwsComB).toEqual(expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5002/api/',
|
||||
})
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('test JWE encryption and decryption', () => {
|
||||
let jweComA: string
|
||||
let jweComB: string
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
jweComA = await encrypt(new OpenConnectionJwtPayloadType('handshakeID', 'http://localhost:5001/api/'), keypairComB.publicKey)
|
||||
jweComB = await encrypt(new OpenConnectionJwtPayloadType('handshakeID', 'http://localhost:5002/api/'), keypairComA.publicKey)
|
||||
})
|
||||
it('decrypt jweComA', async () => {
|
||||
const decryptedAJwT = await decrypt('handshakeID', jweComA, keypairComB.privateKey)
|
||||
expect(JSON.parse(decryptedAJwT)).toEqual(expect.objectContaining({
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5001/api/',
|
||||
handshakeID: 'handshakeID',
|
||||
}))
|
||||
})
|
||||
it('decrypt jweComB', async () => {
|
||||
const decryptedBJwT = await decrypt('handshakeID', jweComB, keypairComA.privateKey)
|
||||
expect(JSON.parse(decryptedBJwT)).toEqual(expect.objectContaining({
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5002/api/',
|
||||
handshakeID: 'handshakeID',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
describe('test encrypted and signed JWT', () => {
|
||||
let jweComA: string
|
||||
let jwsComA: string
|
||||
let jweComB: string
|
||||
let jwsComB: string
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
jweComA = await encrypt(new OpenConnectionJwtPayloadType('handshakeID', 'http://localhost:5001/api/'), keypairComB.publicKey)
|
||||
jwsComA = await encode(new EncryptedJWEJwtPayloadType('handshakeID', jweComA), keypairComA.privateKey)
|
||||
jweComB = await encrypt(new OpenConnectionJwtPayloadType('handshakeID', 'http://localhost:5002/api/'), keypairComA.publicKey)
|
||||
jwsComB = await encode(new EncryptedJWEJwtPayloadType('handshakeID', jweComB), keypairComB.privateKey)
|
||||
})
|
||||
it('verify jwsComA', async () => {
|
||||
const verifiedJwsComA = await verify('handshakeID', jwsComA, keypairComA.publicKey)
|
||||
expect(verifiedJwsComA).toEqual(expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
jwe: jweComA,
|
||||
tokentype: EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE,
|
||||
handshakeID: 'handshakeID',
|
||||
})
|
||||
}))
|
||||
})
|
||||
it('verify jwsComB', async () => {
|
||||
const verifiedJwsComB = await verify('handshakeID', jwsComB, keypairComB.publicKey)
|
||||
expect(verifiedJwsComB).toEqual(expect.objectContaining({
|
||||
payload: expect.objectContaining({
|
||||
jwe: jweComB,
|
||||
tokentype: EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE,
|
||||
handshakeID: 'handshakeID',
|
||||
})
|
||||
}))
|
||||
})
|
||||
it('decrypt jweComA', async () => {
|
||||
expect(JSON.parse(await decrypt('handshakeID', jweComA, keypairComB.privateKey))).toEqual(expect.objectContaining({
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5001/api/',
|
||||
handshakeID: 'handshakeID',
|
||||
}))
|
||||
})
|
||||
it('decrypt jweComB', async () => {
|
||||
expect(JSON.parse(await decrypt('handshakeID', jweComB, keypairComA.privateKey))).toEqual(expect.objectContaining({
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5002/api/',
|
||||
handshakeID: 'handshakeID',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
describe('test encryptAndSign and verifyAndDecrypt', () => {
|
||||
let jwtComA: string
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks()
|
||||
jwtComA = await encryptAndSign(new OpenConnectionJwtPayloadType('handshakeID', 'http://localhost:5001/api/'), keypairComA.privateKey, keypairComB.publicKey)
|
||||
})
|
||||
it('verifyAndDecrypt jwtComA', async () => {
|
||||
const verifiedAndDecryptedPayload = await verifyAndDecrypt('handshakeID', jwtComA, keypairComB.privateKey, keypairComA.publicKey)
|
||||
expect(verifiedAndDecryptedPayload).toEqual(expect.objectContaining({
|
||||
tokentype: OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE,
|
||||
url: 'http://localhost:5001/api/',
|
||||
handshakeID: 'handshakeID',
|
||||
}))
|
||||
})
|
||||
})
|
||||
176
shared/src/jwt/JWT.ts
Normal file
176
shared/src/jwt/JWT.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import { generateKeyPair, exportSPKI, exportPKCS8, SignJWT, decodeJwt, importPKCS8, importSPKI, jwtVerify, CompactEncrypt, compactDecrypt } from 'jose'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '../const'
|
||||
import { getLogger } from 'log4js'
|
||||
|
||||
const logger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT`)
|
||||
|
||||
import { JwtPayloadType } from './payloadtypes/JwtPayloadType'
|
||||
import { EncryptedJWEJwtPayloadType } from './payloadtypes/EncryptedJWEJwtPayloadType'
|
||||
|
||||
export const createKeyPair = async (): Promise<{ publicKey: string; privateKey: string }> => {
|
||||
// Generate key pair using jose library
|
||||
const keyPair = await generateKeyPair('RS256', {
|
||||
modulusLength: 2048, // recommended key size
|
||||
extractable: true,
|
||||
});
|
||||
logger.debug(`Federation: writeJwtKeyPairInHomeCommunity generated keypair...`);
|
||||
|
||||
// Convert keys to PEM format for storage in database
|
||||
const publicKeyPem = await exportSPKI(keyPair.publicKey);
|
||||
const privateKeyPem = await exportPKCS8(keyPair.privateKey);
|
||||
return { publicKey: publicKeyPem, privateKey: privateKeyPem };
|
||||
}
|
||||
|
||||
export const verify = async (handshakeID: string, token: string, publicKey: string): Promise<JwtPayloadType | null> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT.verify`)
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
if (!token) {
|
||||
methodLogger.error('verify... token is empty')
|
||||
throw new Error('401 Unauthorized')
|
||||
}
|
||||
methodLogger.debug('verify... token, publicKey=', token, publicKey)
|
||||
|
||||
try {
|
||||
const importedKey = await importSPKI(publicKey, 'RS256')
|
||||
// Convert the key to JWK format if needed
|
||||
const secret = typeof importedKey === 'string'
|
||||
? JSON.parse(importedKey)
|
||||
: importedKey;
|
||||
// const secret = new TextEncoder().encode(publicKey)
|
||||
const { payload } = await jwtVerify(token, secret, {
|
||||
issuer: JwtPayloadType.ISSUER,
|
||||
audience: JwtPayloadType.AUDIENCE,
|
||||
})
|
||||
payload.handshakeID = handshakeID
|
||||
methodLogger.debug('verify after jwtVerify... payload=', payload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return payload as JwtPayloadType
|
||||
} catch (err) {
|
||||
methodLogger.error('verify after jwtVerify... error=', err)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const encode = async (payload: JwtPayloadType, privatekey: string): Promise<string> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT.encode`)
|
||||
methodLogger.addContext('handshakeID', payload.handshakeID)
|
||||
methodLogger.debug('encode... payload=', payload)
|
||||
methodLogger.debug('encode... privatekey=', privatekey.substring(0, 20))
|
||||
try {
|
||||
const importedKey = await importPKCS8(privatekey, 'RS256')
|
||||
const secret = typeof importedKey === 'string'
|
||||
? JSON.parse(importedKey)
|
||||
: importedKey;
|
||||
|
||||
// const secret = new TextEncoder().encode(privatekey)
|
||||
const token = await new SignJWT({ payload, 'urn:gradido:claim': true })
|
||||
.setProtectedHeader({
|
||||
alg: 'RS256',
|
||||
})
|
||||
.setIssuedAt()
|
||||
.setIssuer(JwtPayloadType.ISSUER)
|
||||
.setAudience(JwtPayloadType.AUDIENCE)
|
||||
.setExpirationTime(payload.expiration)
|
||||
.sign(secret)
|
||||
methodLogger.debug('encode... token=', token)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return token
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to sign JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export const verifyJwtType = async (handshakeID: string, token: string, publicKey: string): Promise<string> => {
|
||||
const payload = await verify(handshakeID, token, publicKey)
|
||||
return payload ? payload.tokentype : 'unknown token type'
|
||||
}
|
||||
|
||||
export const decode = (token: string): JwtPayloadType => {
|
||||
const { payload } = decodeJwt(token)
|
||||
return payload as JwtPayloadType
|
||||
}
|
||||
|
||||
export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promise<string> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT.encrypt`)
|
||||
methodLogger.addContext('handshakeID', payload.handshakeID)
|
||||
methodLogger.debug('encrypt... payload=', payload)
|
||||
methodLogger.debug('encrypt... publicKey=', publicKey)
|
||||
try {
|
||||
const encryptKey = await importSPKI(publicKey, 'RSA-OAEP-256')
|
||||
// Convert the key to JWK format if needed
|
||||
const recipientKey = typeof encryptKey === 'string'
|
||||
? JSON.parse(encryptKey)
|
||||
: encryptKey;
|
||||
|
||||
const jwe = await new CompactEncrypt(
|
||||
new TextEncoder().encode(JSON.stringify(payload)),
|
||||
)
|
||||
.setProtectedHeader({ alg: 'RSA-OAEP-256', enc: 'A256GCM' })
|
||||
.encrypt(recipientKey)
|
||||
methodLogger.debug('encrypt... jwe=', jwe)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jwe.toString()
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to encrypt JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export const decrypt = async(handshakeID: string, jwe: string, privateKey: string): Promise<string> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT.decrypt`)
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug('decrypt... jwe=', jwe)
|
||||
methodLogger.debug('decrypt... privateKey=', privateKey.substring(0, 10))
|
||||
try {
|
||||
const decryptKey = await importPKCS8(privateKey, 'RSA-OAEP-256')
|
||||
const { plaintext, protectedHeader } =
|
||||
await compactDecrypt(jwe, decryptKey)
|
||||
methodLogger.debug('decrypt... plaintext=', plaintext)
|
||||
methodLogger.debug('decrypt... protectedHeader=', protectedHeader)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return new TextDecoder().decode(plaintext)
|
||||
} catch (e) {
|
||||
methodLogger.error('Failed to decrypt JWT:', e)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string, publicKey: string): Promise<string> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT.encryptAndSign`)
|
||||
methodLogger.addContext('handshakeID', payload.handshakeID)
|
||||
const jwe = await encrypt(payload, publicKey)
|
||||
methodLogger.debug('encryptAndSign... jwe=', jwe)
|
||||
const jws = await encode(new EncryptedJWEJwtPayloadType(payload.handshakeID, jwe), privateKey)
|
||||
methodLogger.debug('encryptAndSign... jws=', jws)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return jws
|
||||
}
|
||||
|
||||
export const verifyAndDecrypt = async (handshakeID: string, token: string, privateKey: string, publicKey: string): Promise<JwtPayloadType | null> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.auth.jwt.JWT.verifyAndDecrypt`)
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
const jweVerifyResult = await verify(handshakeID, token, publicKey)
|
||||
if (!jweVerifyResult) {
|
||||
return null
|
||||
}
|
||||
const jwePayload = jweVerifyResult.payload as EncryptedJWEJwtPayloadType
|
||||
methodLogger.debug('verifyAndDecrypt... jwePayload=', jwePayload)
|
||||
if (!jwePayload) {
|
||||
return null
|
||||
}
|
||||
const jwePayloadType = jwePayload.tokentype
|
||||
if (jwePayloadType !== EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE) {
|
||||
return null
|
||||
}
|
||||
const jwe = jwePayload.jwe
|
||||
methodLogger.debug('verifyAndDecrypt... jwe=', jwe)
|
||||
const payload = await decrypt(handshakeID, jwe as string, privateKey)
|
||||
methodLogger.debug('verifyAndDecrypt... payload=', payload)
|
||||
methodLogger.removeContext('handshakeID')
|
||||
return JSON.parse(payload) as JwtPayloadType
|
||||
}
|
||||
19
shared/src/jwt/payloadtypes/AuthenticationJwtPayloadType.ts
Normal file
19
shared/src/jwt/payloadtypes/AuthenticationJwtPayloadType.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { JwtPayloadType } from './JwtPayloadType'
|
||||
|
||||
export class AuthenticationJwtPayloadType extends JwtPayloadType {
|
||||
static AUTHENTICATION_TYPE = 'authentication'
|
||||
|
||||
oneTimeCode: string
|
||||
uuid: string
|
||||
|
||||
constructor(
|
||||
handshakeID: string,
|
||||
oneTimeCode: string,
|
||||
uuid: string,
|
||||
) {
|
||||
super(handshakeID)
|
||||
this.tokentype = AuthenticationJwtPayloadType.AUTHENTICATION_TYPE
|
||||
this.oneTimeCode = oneTimeCode
|
||||
this.uuid = uuid
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { JwtPayloadType } from './JwtPayloadType'
|
||||
|
||||
export class AuthenticationResponseJwtPayloadType extends JwtPayloadType {
|
||||
static AUTHENTICATION_RESPONSE_TYPE = 'authenticationResponse'
|
||||
|
||||
uuid: string
|
||||
|
||||
constructor(
|
||||
handshakeID: string,
|
||||
uuid: string,
|
||||
) {
|
||||
super(handshakeID)
|
||||
this.tokentype = AuthenticationResponseJwtPayloadType.AUTHENTICATION_RESPONSE_TYPE
|
||||
this.uuid = uuid
|
||||
}
|
||||
}
|
||||
@ -30,7 +30,7 @@ export class DisburseJwtPayloadType extends JwtPayloadType {
|
||||
recipientAlias: string,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
super()
|
||||
super('handshakeID')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.tokentype = DisburseJwtPayloadType.DISBURSE_ACTIVATION_TYPE
|
||||
this.sendercommunityuuid = senderCommunityUuid
|
||||
18
shared/src/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts
Normal file
18
shared/src/jwt/payloadtypes/EncryptedJWEJwtPayloadType.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { JwtPayloadType } from './JwtPayloadType'
|
||||
|
||||
export class EncryptedJWEJwtPayloadType extends JwtPayloadType {
|
||||
static ENCRYPTED_JWE_TYPE = 'encrypted-jwe'
|
||||
|
||||
jwe: string
|
||||
|
||||
constructor(
|
||||
handshakeID: string,
|
||||
jwe: string,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
super(handshakeID)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.tokentype = EncryptedJWEJwtPayloadType.ENCRYPTED_JWE_TYPE
|
||||
this.jwe = jwe
|
||||
}
|
||||
}
|
||||
@ -1,8 +1,11 @@
|
||||
import { JWTPayload } from 'jose'
|
||||
|
||||
import { CONFIG } from '@/config'
|
||||
import { REDEEM_JWT_TOKEN_EXPIRATION } from '../../const'
|
||||
|
||||
export class JwtPayloadType implements JWTPayload {
|
||||
static ISSUER = 'urn:gradido:issuer'
|
||||
static AUDIENCE = 'urn:gradido:audience'
|
||||
|
||||
iat?: number | undefined
|
||||
exp?: number | undefined
|
||||
nbf?: number | undefined
|
||||
@ -12,10 +15,12 @@ export class JwtPayloadType implements JWTPayload {
|
||||
iss?: string | undefined;
|
||||
[propName: string]: unknown
|
||||
|
||||
handshakeID: string // used as logger context during authentication handshake between comA and comB
|
||||
tokentype: string
|
||||
expiration: string // in minutes (format: 10m for ten minutes)
|
||||
constructor() {
|
||||
constructor(handshakeID: string) {
|
||||
this.tokentype = 'unknown jwt type'
|
||||
this.expiration = CONFIG.REDEEM_JWT_TOKEN_EXPIRATION || '10m'
|
||||
this.expiration = REDEEM_JWT_TOKEN_EXPIRATION || '10m'
|
||||
this.handshakeID = handshakeID
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { JwtPayloadType } from './JwtPayloadType'
|
||||
|
||||
export class OpenConnectionCallbackJwtPayloadType extends JwtPayloadType {
|
||||
static OPEN_CONNECTION_CALLBACK_TYPE = 'open-connection-callback'
|
||||
|
||||
oneTimeCode: string
|
||||
url: string
|
||||
|
||||
constructor(
|
||||
handshakeID: string,
|
||||
oneTimeCode: string,
|
||||
url: string,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
super(handshakeID)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.tokentype = OpenConnectionCallbackJwtPayloadType.OPEN_CONNECTION_CALLBACK_TYPE
|
||||
this.oneTimeCode = oneTimeCode
|
||||
this.url = url
|
||||
}
|
||||
}
|
||||
18
shared/src/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts
Normal file
18
shared/src/jwt/payloadtypes/OpenConnectionJwtPayloadType.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { JwtPayloadType } from './JwtPayloadType'
|
||||
|
||||
export class OpenConnectionJwtPayloadType extends JwtPayloadType {
|
||||
static OPEN_CONNECTION_TYPE = 'open-connection'
|
||||
|
||||
url: string
|
||||
|
||||
constructor(
|
||||
handshakeID: string,
|
||||
url: string,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
super(handshakeID)
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.tokentype = OpenConnectionJwtPayloadType.OPEN_CONNECTION_TYPE
|
||||
this.url = url
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,3 @@
|
||||
// import { JWTPayload } from 'jose'
|
||||
import { JwtPayloadType } from './JwtPayloadType'
|
||||
|
||||
export class RedeemJwtPayloadType extends JwtPayloadType {
|
||||
@ -22,7 +21,7 @@ export class RedeemJwtPayloadType extends JwtPayloadType {
|
||||
validUntil: string,
|
||||
) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
||||
super()
|
||||
super('handshakeID')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
this.tokentype = RedeemJwtPayloadType.REDEEM_ACTIVATION_TYPE
|
||||
this.sendercommunityuuid = senderCom
|
||||
Loading…
x
Reference in New Issue
Block a user