sign jwt with senderCommunityUuid, search senderCom per decoded payload

and verify jwt with senderComUUid selected from db ensured against
referrer
This commit is contained in:
clauspeterhuebner 2025-04-15 22:06:43 +02:00
parent 41032e5fc6
commit a6e2efef19
7 changed files with 96 additions and 43 deletions

View File

@ -1,20 +1,15 @@
import { createPrivateKey, sign } from 'node:crypto'
import { SignJWT, jwtVerify } from 'jose'
import { SignJWT, jwtVerify, decodeJwt, JWTPayload } from 'jose'
import { LogError } from '@/server/LogError'
import { backendLogger as logger } from '@/server/logger'
import { JwtPayloadType } from './payloadtypes/JwtPayloadType'
export const verify = async (token: string, signkey: Buffer): Promise<JwtPayloadType | null> => {
export const verify = async (token: string, signkey: string): Promise<JwtPayloadType | null> => {
if (!token) throw new LogError('401 Unauthorized')
logger.info(
'JWT.verify... token, signkey, signkey.toString(hex)',
token,
signkey,
signkey.toString('hex'),
)
logger.info('JWT.verify... token, signkey=', token, signkey)
try {
/*
@ -28,7 +23,8 @@ export const verify = async (token: string, signkey: Buffer): Promise<JwtPayload
logger.info('JWT.verify... keyObject.asymmetricKeyType=', keyObject.asymmetricKeyType)
logger.info('JWT.verify... keyObject.asymmetricKeySize=', keyObject.asymmetricKeySize)
*/
const { payload } = await jwtVerify(token, signkey, {
const secret = new TextEncoder().encode(signkey)
const { payload } = await jwtVerify(token, secret, {
issuer: 'urn:gradido:issuer',
audience: 'urn:gradido:audience',
})
@ -40,12 +36,11 @@ export const verify = async (token: string, signkey: Buffer): Promise<JwtPayload
}
}
export const encode = async (payload: JwtPayloadType, signkey: Buffer): Promise<string> => {
export const encode = async (payload: JwtPayloadType, signkey: string): Promise<string> => {
logger.info('JWT.encode... payload=', payload)
logger.info('JWT.encode... signkey=', signkey)
logger.info('JWT.encode... signkey length=', signkey.length)
logger.info('JWT.encode... signkey.toString(hex)=', signkey.toString('hex'))
try {
const secret = new TextEncoder().encode(signkey)
const token = await new SignJWT({ payload, 'urn:gradido:claim': true })
.setProtectedHeader({
alg: 'HS256',
@ -54,7 +49,7 @@ export const encode = async (payload: JwtPayloadType, signkey: Buffer): Promise<
.setIssuer('urn:gradido:issuer')
.setAudience('urn:gradido:audience')
.setExpirationTime(payload.expiration)
.sign(signkey)
.sign(secret)
return token
} catch (e) {
logger.error('Failed to sign JWT:', e)
@ -62,7 +57,12 @@ export const encode = async (payload: JwtPayloadType, signkey: Buffer): Promise<
}
}
export const verifyJwtType = async (token: string, signkey: Buffer): Promise<string> => {
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 unknown as JwtPayloadType
}

View File

@ -23,7 +23,7 @@ import { TransactionLink, TransactionLinkResult } from '@model/TransactionLink'
import { User } from '@model/User'
import { QueryLinkResult } from '@union/QueryLinkResult'
import { verify, encode } from '@/auth/jwt/JWT'
import { verify, encode, decode } from '@/auth/jwt/JWT'
import { DisbursementJwtPayloadType } from '@/auth/jwt/payloadtypes/DisbursementJwtPayloadType'
import { RIGHTS } from '@/auth/RIGHTS'
import {
@ -42,7 +42,11 @@ import { fullName } from '@/util/utilities'
import { calculateBalance } from '@/util/validate'
import { executeTransaction } from './TransactionResolver'
import { getAuthenticatedCommunities, getHomeCommunity } from './util/communities'
import {
getAuthenticatedCommunities,
getCommunityByUuid,
getHomeCommunity,
} from './util/communities'
import { getUserCreation, validateContribution } from './util/creations'
import { getLastTransaction } from './util/getLastTransaction'
import { sendTransactionsToDltConnector } from './util/sendTransactionsToDltConnector'
@ -139,7 +143,10 @@ export class TransactionLinkResolver {
@Authorized([RIGHTS.QUERY_TRANSACTION_LINK])
@Query(() => QueryLinkResult)
async queryTransactionLink(@Arg('code') code: string): Promise<typeof QueryLinkResult> {
async queryTransactionLink(
@Arg('code') code: string,
@Arg('referrer') referrer: string,
): Promise<typeof QueryLinkResult> {
logger.debug('TransactionLinkResolver.queryTransactionLink... code=', code)
const transactionLink = new TransactionLink()
if (code.match(/^CL-/)) {
@ -178,29 +185,54 @@ export class TransactionLinkResolver {
} else {
// disbursement jwt-token
logger.debug('TransactionLinkResolver.queryTransactionLink... disbursement jwt-token found')
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-assignment
const homeCom = await getHomeCommunity()
const jwtPayload = await verify(code, homeCom.publicKey)
logger.debug('TransactionLinkResolver.queryTransactionLink... jwtPayload=', jwtPayload)
if (jwtPayload !== null && jwtPayload instanceof DisbursementJwtPayloadType) {
const disburseJwtPayload: DisbursementJwtPayloadType = jwtPayload
// first check sender of payload against referrer
const payload = decode(code)
if (!payload) {
throw new LogError('Invalid JWT payload', payload)
}
if (payload instanceof DisbursementJwtPayloadType) {
const disburseJwtPayload: DisbursementJwtPayloadType = payload
logger.debug(
'TransactionLinkResolver.queryTransactionLink... disburseJwtPayload=',
jwtPayload,
disburseJwtPayload,
)
transactionLink.communityName = homeCom.name !== null ? homeCom.name : 'unknown'
// transactionLink.user = new User()
transactionLink.user.alias = disburseJwtPayload.sendername
transactionLink.amount = new Decimal(disburseJwtPayload.amount)
transactionLink.memo = disburseJwtPayload.memo
transactionLink.code = disburseJwtPayload.redeemcode
logger.debug(
'TransactionLinkResolver.queryTransactionLink... transactionLink=',
transactionLink,
)
return transactionLink
const senderCom = await getCommunityByUuid(disburseJwtPayload.sendercommunityuuid)
if (!senderCom) {
throw new LogError(
'Sender community not found:',
disburseJwtPayload.sendercommunityuuid,
)
}
const senderUrl = senderCom.url.replace(/\/api\/?$/, '')
if (!senderUrl.startsWith(referrer)) {
throw new LogError('Sender community does not match referrer', senderCom.name, referrer)
}
if (!senderCom.communityUuid) {
throw new LogError('Sender community UUID is not set')
}
// now with the sender community UUID the jwt token can be verified
const jwtPayload = await verify(code, senderCom.communityUuid)
logger.debug('TransactionLinkResolver.queryTransactionLink... jwtPayload=', jwtPayload)
if (jwtPayload !== null && jwtPayload instanceof DisbursementJwtPayloadType) {
const disburseJwtPayload: DisbursementJwtPayloadType = jwtPayload
logger.debug(
'TransactionLinkResolver.queryTransactionLink... disburseJwtPayload=',
disburseJwtPayload,
)
transactionLink.communityName = senderCom.name !== null ? senderCom.name : 'unknown'
transactionLink.user = new User(null)
transactionLink.user.alias = disburseJwtPayload.sendername
transactionLink.amount = new Decimal(disburseJwtPayload.amount)
transactionLink.memo = disburseJwtPayload.memo
transactionLink.code = disburseJwtPayload.redeemcode
logger.debug(
'TransactionLinkResolver.queryTransactionLink... transactionLink=',
transactionLink,
)
return transactionLink
}
} else {
throw new LogError('Redeem with wrong type of JWT-Token! jwtType=', jwtPayload)
throw new LogError('Redeem with wrong type of JWT-Token! payload=', payload)
}
}
}
@ -446,12 +478,12 @@ export class TransactionLinkResolver {
amount,
memo,
)
// encode/sign the jwt with the private key of the sender/home community
// TODO:encode/sign the jwt normally with the private key of the sender/home community, but interims with uuid
const homeCom = await getHomeCommunity()
if (!homeCom.privateKey) {
throw new LogError('Home community private key is not set')
if (!homeCom.communityUuid) {
throw new LogError('Home community UUID is not set')
}
const redeemJwt = await encode(disbursementJwtPayloadType, homeCom.privateKey)
const redeemJwt = await encode(disbursementJwtPayloadType, homeCom.communityUuid)
// TODO: encrypt the payload with the public key of the target community
return redeemJwt
}

View File

@ -2,6 +2,7 @@
<div
:link-data="linkData"
:redeem-code="redeemCode"
:referrer="referrer"
:is-contribution-link="isContributionLink"
class="redeem-community-selection"
>
@ -48,6 +49,7 @@ import { useMutation } from '@vue/apollo-composable'
const props = defineProps({
linkData: { type: Object, required: true },
redeemCode: { type: String, required: true },
referrer: { type: String, required: true },
isContributionLink: { type: Boolean, default: false },
receiverCommunity: {
type: Object,

View File

@ -4,6 +4,7 @@
v-model:receiver-community="receiverCommunity"
:link-data="props.linkData"
:redeem-code="props.redeemCode"
:referrer="props.referrer"
:is-contribution-link="props.isContributionLink"
/>
@ -39,6 +40,7 @@ const { login, register } = useAuthLinks()
const props = defineProps({
linkData: { type: Object, required: true },
redeemCode: { type: String, required: true },
referrer: { type: String, required: true },
isContributionLink: { type: Boolean, default: false },
})

View File

@ -128,8 +128,8 @@ export const checkUsername = gql`
`
export const queryTransactionLink = gql`
query ($code: String!) {
queryTransactionLink(code: $code) {
query ($code: String!, $referrer: String!) {
queryTransactionLink(code: $code, referrer: $referrer) {
... on TransactionLink {
id
amount

View File

@ -6,6 +6,7 @@
<redeem-select-community
:link-data="linkData"
:redeem-code="redeemCode"
:referrer="referrer"
:is-contribution-link="isContributionLink"
/>
</template>
@ -48,7 +49,7 @@ import { useI18n } from 'vue-i18n'
const { toastError, toastSuccess } = useAppToast()
const router = useRouter()
const { params } = useRoute()
const { params, meta } = useRoute()
const store = useStore()
const { d, t } = useI18n()
@ -73,6 +74,7 @@ const redeemedBoxText = ref('')
const { result, onResult, loading, error, onError } = useQuery(queryTransactionLink, {
code: params.code,
referrer: meta.referrer,
})
const {
@ -86,6 +88,7 @@ const isContributionLink = computed(() => {
})
const redeemCode = computed(() => params.code)
const referrer = computed(() => meta.referrer)
const tokenExpiresInSeconds = computed(() => {
const remainingSecs = Math.floor(
@ -170,6 +173,7 @@ const emit = defineEmits(['set-mobile-start'])
onMounted(() => {
console.log('TransactionLink.onMounted... params=', params)
console.log('TransactionLink.onMounted... meta=', meta)
emit('set-mobile-start', false)
})

View File

@ -1,5 +1,11 @@
import NotFound from '@/pages/NotFoundPage'
function setReferrerToMeta(to, from) {
if (Object.keys(from.query).length) {
to.meta.referrer = from.path
}
}
const routes = [
{
path: '/authenticate',
@ -156,6 +162,13 @@ const routes = [
{
path: '/redeem/:code',
component: () => import('@/pages/TransactionLink'),
beforeEnter: (to, from) => {
setReferrerToMeta(to, from)
return true
},
meta: {
referrer: 'unknown',
},
},
{
path: '/:catchAll(.*)',