mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
finish refactor authentication with states
This commit is contained in:
parent
ced6f42fa0
commit
6e13f5d8ab
@ -100,7 +100,7 @@ export async function startCommunityAuthentication(
|
||||
methodLogger.error(errorMsg)
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = errorMsg
|
||||
await state.save()
|
||||
}
|
||||
await state.save()
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,37 +6,35 @@ 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.logic.interpretEncryptedTransferArgs`)
|
||||
const createLogger = (functionName: string) => getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs.${functionName}`)
|
||||
|
||||
export const interpretEncryptedTransferArgs = async (args: EncryptedTransferArgs): Promise<JwtPayloadType | null> => {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.logic.interpretEncryptedTransferArgs-method`)
|
||||
const methodLogger = createLogger('interpretEncryptedTransferArgs')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug('interpretEncryptedTransferArgs()... args:', args)
|
||||
// first find with args.publicKey the community 'requestingCom', which starts the request
|
||||
// TODO: maybe use community from caller instead of loading it separately
|
||||
const requestingCom = await DbCommunity.findOneBy({ publicKey: Buffer.from(args.publicKey, 'hex') })
|
||||
if (!requestingCom) {
|
||||
const errmsg = `unknown requesting community with publicKey ${Buffer.from(args.publicKey, 'hex')}`
|
||||
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 ${Buffer.from(args.publicKey, 'hex')}`
|
||||
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
|
||||
// TODO: maybe use community from caller instead of loading it separately
|
||||
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 ${Buffer.from(args.publicKey, 'hex')}`
|
||||
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
|
||||
}
|
||||
|
||||
@ -36,6 +36,11 @@ export const delay = promisify(setTimeout)
|
||||
export const ensureUrlEndsWithSlash = (url: string): string => {
|
||||
return url.endsWith('/') ? url : url.concat('/')
|
||||
}
|
||||
export function splitUrlInEndPointAndApiVersion(url: string): { endPoint: string, apiVersion: string } {
|
||||
const endPoint = url.slice(0, url.lastIndexOf('/') + 1)
|
||||
const apiVersion = url.slice(url.lastIndexOf('/') + 1, url.length)
|
||||
return { endPoint, apiVersion }
|
||||
}
|
||||
/**
|
||||
* Calculates the date representing the first day of the month, a specified number of months prior to a given date.
|
||||
*
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
export enum CommunityHandshakeStateType {
|
||||
START_COMMUNITY_AUTHENTICATION = 'START_COMMUNITY_AUTHENTICATION',
|
||||
START_OPEN_CONNECTION_CALLBACK = 'START_OPEN_CONNECTION_CALLBACK',
|
||||
START_AUTHENTICATION = 'START_AUTHENTICATION',
|
||||
OPEN_CONNECTION_CALLBACK = 'OPEN_CONNECTION_CALLBACK',
|
||||
|
||||
SUCCESS = 'SUCCESS',
|
||||
|
||||
@ -22,4 +22,14 @@ export function findPendingCommunityHandshake(
|
||||
},
|
||||
relations: withRelations ? { federatedCommunity: { community: true } } : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export function findPendingCommunityHandshakeOrFailByOneTimeCode(
|
||||
oneTimeCode: number
|
||||
): Promise<CommunityHandshakeState> {
|
||||
return CommunityHandshakeState.findOneOrFail({
|
||||
where: { oneTimeCode },
|
||||
relations: { federatedCommunity: { community: true } },
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,12 +1,17 @@
|
||||
import { CONFIG } from '@/config'
|
||||
import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const'
|
||||
import { EncryptedTransferArgs, interpretEncryptedTransferArgs } from 'core'
|
||||
import { CommunityHandshakeStateLogic, EncryptedTransferArgs, interpretEncryptedTransferArgs, splitUrlInEndPointAndApiVersion } from 'core'
|
||||
import {
|
||||
CommunityLoggingView,
|
||||
CommunityHandshakeStateLoggingView,
|
||||
CommunityHandshakeState as DbCommunityHandshakeState,
|
||||
CommunityHandshakeStateType,
|
||||
Community as DbCommunity,
|
||||
FederatedCommunity as DbFedCommunity,
|
||||
FederatedCommunityLoggingView,
|
||||
getHomeCommunity,
|
||||
findPendingCommunityHandshake,
|
||||
findPendingCommunityHandshakeOrFailByOneTimeCode,
|
||||
} from 'database'
|
||||
import { getLogger } from 'log4js'
|
||||
import {
|
||||
@ -93,9 +98,7 @@ export class AuthenticationResolver {
|
||||
// no infos to the caller
|
||||
return true
|
||||
}
|
||||
|
||||
const endPoint = openConnectionCallbackJwtPayload.url.slice(0, openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1)
|
||||
const apiVersion = openConnectionCallbackJwtPayload.url.slice(openConnectionCallbackJwtPayload.url.lastIndexOf('/') + 1, openConnectionCallbackJwtPayload.url.length)
|
||||
const { endPoint, apiVersion } = splitUrlInEndPointAndApiVersion(openConnectionCallbackJwtPayload.url)
|
||||
methodLogger.debug(`search fedComB per:`, endPoint, apiVersion)
|
||||
const fedComB = await DbFedCommunity.findOneBy({ endPoint, apiVersion })
|
||||
if (!fedComB) {
|
||||
@ -126,45 +129,47 @@ export class AuthenticationResolver {
|
||||
const methodLogger = createLogger('authenticate')
|
||||
methodLogger.addContext('handshakeID', args.handshakeID)
|
||||
methodLogger.debug(`authenticate() via apiVersion=1_0 ...`, args)
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
let stateSaveResolver: Promise<DbCommunityHandshakeState> | undefined = undefined
|
||||
try {
|
||||
const authArgs = await interpretEncryptedTransferArgs(args) as AuthenticationJwtPayloadType
|
||||
methodLogger.debug(`interpreted authentication payload...authArgs:`, authArgs)
|
||||
if (!authArgs) {
|
||||
const errmsg = `invalid authentication payload of requesting community with publicKey` + args.publicKey
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
throw new Error(`invalid authentication payload of requesting community with publicKey ${args.publicKey}`)
|
||||
}
|
||||
|
||||
if (!uint32Schema.safeParse(Number(authArgs.oneTimeCode)).success) {
|
||||
const errmsg = `invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
throw new Error(
|
||||
`invalid oneTimeCode: ${authArgs.oneTimeCode} for community with publicKey ${authArgs.publicKey}, expect uint32`
|
||||
)
|
||||
}
|
||||
|
||||
state = await findPendingCommunityHandshakeOrFailByOneTimeCode(Number(authArgs.oneTimeCode))
|
||||
const stateLogic = new CommunityHandshakeStateLogic(state)
|
||||
if (await stateLogic.isTimeoutUpdate() || state.status !== CommunityHandshakeStateType.START_OPEN_CONNECTION_CALLBACK) {
|
||||
throw new Error('No valid pending community handshake found')
|
||||
}
|
||||
state.status = CommunityHandshakeStateType.SUCCESS
|
||||
stateSaveResolver = state.save()
|
||||
|
||||
methodLogger.debug(`search community per oneTimeCode:`, authArgs.oneTimeCode)
|
||||
const authCom = await DbCommunity.findOneByOrFail({ communityUuid: authArgs.oneTimeCode })
|
||||
const authCom = state.federatedCommunity.community
|
||||
if (authCom) {
|
||||
methodLogger.debug('found authCom:', new CommunityLoggingView(authCom))
|
||||
methodLogger.debug('authCom.publicKey', authCom.publicKey.toString('hex'))
|
||||
methodLogger.debug('args.publicKey', args.publicKey)
|
||||
if (authCom.publicKey.compare(Buffer.from(args.publicKey, 'hex')) !== 0) {
|
||||
const errmsg = `corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
throw new Error(
|
||||
`corrupt authentication call detected, oneTimeCode: ${authArgs.oneTimeCode} doesn't belong to caller: ${args.publicKey}`
|
||||
)
|
||||
}
|
||||
const communityUuid = uuidv4Schema.safeParse(authArgs.uuid)
|
||||
if (!communityUuid.success) {
|
||||
const errmsg = `invalid uuid: ${authArgs.uuid} for community with publicKey ${authCom.publicKey}`
|
||||
methodLogger.error(errmsg)
|
||||
// no infos to the caller
|
||||
return null
|
||||
throw new Error(`invalid uuid: ${authArgs.uuid} for community with publicKey ${authCom.publicKey}`)
|
||||
}
|
||||
authCom.communityUuid = communityUuid.data
|
||||
authCom.authenticatedAt = new Date()
|
||||
await DbCommunity.save(authCom)
|
||||
await authCom.save()
|
||||
methodLogger.debug('store authCom.uuid successfully:', new CommunityLoggingView(authCom))
|
||||
const homeComB = await getHomeCommunity()
|
||||
if (homeComB?.communityUuid) {
|
||||
@ -175,8 +180,26 @@ export class AuthenticationResolver {
|
||||
}
|
||||
return null
|
||||
} catch (err) {
|
||||
methodLogger.error('invalid jwt token:', err)
|
||||
let errorString = ''
|
||||
if (err instanceof Error) {
|
||||
errorString = err.message
|
||||
} else {
|
||||
errorString = String(err)
|
||||
}
|
||||
if (state) {
|
||||
methodLogger.info(`state: ${new CommunityHandshakeStateLoggingView(state)}`)
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = errorString
|
||||
stateSaveResolver = state.save()
|
||||
}
|
||||
methodLogger.error(`failed: ${errorString}`)
|
||||
// no infos to the caller
|
||||
return null
|
||||
} finally {
|
||||
if (stateSaveResolver) {
|
||||
await stateSaveResolver
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,12 +132,14 @@ export async function startAuthentication(
|
||||
oneTimeCode: string,
|
||||
fedComB: DbFedCommunity,
|
||||
): Promise<void> {
|
||||
const methodLogger = getLogger(`${LOG4JS_BASE_CATEGORY_NAME}.graphql.api.1_0.util.authenticateCommunity.startAuthentication`)
|
||||
const methodLogger = createLogger('startAuthentication')
|
||||
methodLogger.addContext('handshakeID', handshakeID)
|
||||
methodLogger.debug(`startAuthentication()...`, {
|
||||
oneTimeCode,
|
||||
fedComB: new FederatedCommunityLoggingView(fedComB),
|
||||
})
|
||||
let state: DbCommunityHandshakeState | null = null
|
||||
let stateSaveResolver: Promise<DbCommunityHandshakeState> | undefined = undefined
|
||||
try {
|
||||
const homeComA = await getHomeCommunity()
|
||||
const comB = await DbCommunity.findOneByOrFail({
|
||||
@ -147,6 +149,17 @@ export async function startAuthentication(
|
||||
if (!comB.publicJwtKey) {
|
||||
throw new Error('Public JWT key still not exist for foreign community')
|
||||
}
|
||||
state = await findPendingCommunityHandshake(fedComB.publicKey, fedComB.apiVersion, false)
|
||||
if (!state) {
|
||||
throw new Error('No pending community handshake found')
|
||||
}
|
||||
const stateLogic = new CommunityHandshakeStateLogic(state)
|
||||
if (await stateLogic.isTimeoutUpdate() || state.status !== CommunityHandshakeStateType.START_COMMUNITY_AUTHENTICATION) {
|
||||
methodLogger.debug('invalid state', new CommunityHandshakeStateLoggingView(state))
|
||||
throw new Error('No valid pending community handshake found')
|
||||
}
|
||||
state.status = CommunityHandshakeStateType.START_AUTHENTICATION
|
||||
stateSaveResolver = state.save()
|
||||
|
||||
const client = AuthenticationClientFactory.getInstance(fedComB)
|
||||
|
||||
@ -161,6 +174,7 @@ export async function startAuthentication(
|
||||
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(
|
||||
@ -169,27 +183,40 @@ export async function startAuthentication(
|
||||
new FederatedCommunityLoggingView(fedComB),
|
||||
)
|
||||
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)
|
||||
throw new Error(`Invalid tokentype in authenticate-response of community with publicKey ${comB.publicKey}`)
|
||||
}
|
||||
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)
|
||||
throw new Error(`Invalid uuid in authenticate-response of community with publicKey ${comB.publicKey}`)
|
||||
}
|
||||
comB.communityUuid = payload.uuid
|
||||
comB.authenticatedAt = new Date()
|
||||
await DbCommunity.save(comB)
|
||||
await DbCommunity.save(comB)
|
||||
state.status = CommunityHandshakeStateType.SUCCESS
|
||||
stateSaveResolver = state.save()
|
||||
methodLogger.debug('Community Authentication successful:', new CommunityLoggingView(comB))
|
||||
} else {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = 'Community Authentication failed, empty response'
|
||||
stateSaveResolver = state.save()
|
||||
methodLogger.error('Community Authentication failed:', authenticationArgs)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
methodLogger.error('error in startAuthentication:', err)
|
||||
let errorString: string = ''
|
||||
if (err instanceof Error) {
|
||||
errorString = err.message
|
||||
} else {
|
||||
errorString = String(err)
|
||||
}
|
||||
if (state) {
|
||||
state.status = CommunityHandshakeStateType.FAILED
|
||||
state.lastError = errorString
|
||||
stateSaveResolver = state.save()
|
||||
}
|
||||
methodLogger.error('error in startAuthentication:', errorString)
|
||||
} finally {
|
||||
if (stateSaveResolver) {
|
||||
await stateSaveResolver
|
||||
}
|
||||
}
|
||||
methodLogger.removeContext('handshakeID')
|
||||
}
|
||||
|
||||
@ -43,11 +43,9 @@ export const verify = async (handshakeID: string, token: string, publicKey: stri
|
||||
})
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -74,11 +72,9 @@ export const encode = async (payload: JwtPayloadType, privatekey: string): Promi
|
||||
.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
|
||||
}
|
||||
}
|
||||
@ -111,11 +107,9 @@ export const encrypt = async (payload: JwtPayloadType, publicKey: string): Promi
|
||||
.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
|
||||
}
|
||||
}
|
||||
@ -131,11 +125,9 @@ export const decrypt = async(handshakeID: string, jwe: string, privateKey: strin
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -147,7 +139,6 @@ export const encryptAndSign = async (payload: JwtPayloadType, privateKey: string
|
||||
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
|
||||
}
|
||||
|
||||
@ -171,6 +162,5 @@ export const verifyAndDecrypt = async (handshakeID: string, token: string, priva
|
||||
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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user