mirror of
https://github.com/IT4Change/gradido.git
synced 2026-02-06 09:56:05 +00:00
Merge branch 'master' into refactor_dlt_connector_modern_stack
This commit is contained in:
commit
491a13894f
158
.github/workflows/test_e2e.yml
vendored
158
.github/workflows/test_e2e.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
sudo chown runner:docker -R *
|
||||
bun install
|
||||
bun install --frozen-lockfile
|
||||
sudo cp ./nginx/e2e-test.conf /etc/nginx/sites-available/default
|
||||
|
||||
- name: Boot up test system | seed backend
|
||||
@ -40,9 +40,8 @@ jobs:
|
||||
cd backend
|
||||
cp .env.test_e2e .env
|
||||
cd ..
|
||||
bun turbo backend#build
|
||||
bun turbo frontend#build
|
||||
bun turbo backend#start frontend#start --env-mode=loose &
|
||||
bun turbo backend#build frontend#build --env-mode=loose
|
||||
bun turbo backend#start frontend#start --env-mode=loose &
|
||||
|
||||
- name: End-to-end tests | prepare
|
||||
run: |
|
||||
@ -106,3 +105,154 @@ jobs:
|
||||
with:
|
||||
name: backend-logs-pr-#${{ steps.pr.outputs.number }}
|
||||
path: /home/runner/work/gradido/gradido/logs/backend
|
||||
|
||||
end-to-end-tests-playwright:
|
||||
name: End-to-End Tests Playwright
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Node.js version
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.20.7'
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
sudo chown runner:docker -R *
|
||||
bun install --frozen-lockfile
|
||||
sudo cp ./nginx/e2e-test.conf /etc/nginx/sites-available/default
|
||||
|
||||
- name: Boot up test system | seed backend
|
||||
run: bun turbo seed
|
||||
|
||||
- name: copy test config
|
||||
run: |
|
||||
cd frontend
|
||||
cp .env.test_e2e .env
|
||||
cd ../backend
|
||||
cp .env.test_e2e .env
|
||||
|
||||
- name: Moving logs after seeding
|
||||
run: |
|
||||
mkdir -p /home/runner/work/gradido/gradido/logs/backend/seed
|
||||
mv /home/runner/work/gradido/gradido/logs/backend/*.log /home/runner/work/gradido/gradido/logs/backend/seed/
|
||||
|
||||
- name: Boot up test system | docker-compose backend, frontend
|
||||
run: |
|
||||
bun turbo backend#build frontend#build --env-mode=loose
|
||||
bun turbo backend#start frontend#start --env-mode=loose &
|
||||
|
||||
- name: End-to-end tests | prepare
|
||||
run: |
|
||||
cd e2e-tests/playwright/typescript
|
||||
bun install
|
||||
|
||||
- name: wait for frontend and backend to be ready
|
||||
run: |
|
||||
until nc -z 127.0.0.1 3000; do echo waiting for frontend; sleep 1; done;
|
||||
until nc -z 127.0.0.1 4000; do echo waiting for backend; sleep 1; done;
|
||||
|
||||
- name: Start local nginx webserver
|
||||
run: |
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
run: |
|
||||
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
|
||||
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
run: |
|
||||
cd e2e-tests/playwright/typescript
|
||||
bun run test --project="Google Chrome"
|
||||
|
||||
- name: End-to-end tests | if tests failed, upload video
|
||||
id: e2e-video
|
||||
if: ${{ failure() && steps.e2e-tests.conclusion == 'failure' }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: cypress-videos-pr-#${{ steps.pr.outputs.number }}
|
||||
path: /home/runner/work/gradido/gradido/e2e-tests/playwright/typescript/test-results
|
||||
|
||||
end-to-end-tests-playwright-java:
|
||||
name: End-to-End Tests Playwright java
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set Node.js version
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18.20.7'
|
||||
|
||||
- uses: actions/setup-java@v3
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '17'
|
||||
|
||||
- name: install bun
|
||||
uses: oven-sh/setup-bun@v2
|
||||
|
||||
- name: Boot up test system | docker-compose mariadb mailserver
|
||||
run: docker compose -f docker-compose.yml -f docker-compose.test.yml up --detach mariadb mailserver
|
||||
|
||||
- name: Prepare test system
|
||||
run: |
|
||||
sudo chown runner:docker -R *
|
||||
bun install
|
||||
sudo cp ./nginx/e2e-test.conf /etc/nginx/sites-available/default
|
||||
|
||||
- name: Boot up test system | seed backend
|
||||
run: bun turbo seed
|
||||
|
||||
- name: Moving logs after seeding
|
||||
run: |
|
||||
mkdir -p /home/runner/work/gradido/gradido/logs/backend/seed
|
||||
mv /home/runner/work/gradido/gradido/logs/backend/*.log /home/runner/work/gradido/gradido/logs/backend/seed/
|
||||
|
||||
- name: Boot up test system | docker-compose backend, frontend
|
||||
run: |
|
||||
cd backend
|
||||
cp .env.test_e2e .env
|
||||
cd ..
|
||||
bun turbo backend#build frontend#build --env-mode=loose
|
||||
bun turbo backend#start frontend#start --env-mode=loose &
|
||||
|
||||
- name: End-to-end tests | prepare
|
||||
run: |
|
||||
cd e2e-tests/playwright/typescript
|
||||
bun install
|
||||
|
||||
- name: wait for frontend and backend to be ready
|
||||
run: |
|
||||
until nc -z 127.0.0.1 3000; do echo waiting for frontend; sleep 1; done;
|
||||
until nc -z 127.0.0.1 4000; do echo waiting for backend; sleep 1; done;
|
||||
|
||||
- name: Start local nginx webserver
|
||||
run: |
|
||||
sudo nginx -t
|
||||
sudo systemctl start nginx
|
||||
|
||||
- name: wait for nginx and mailserver to be ready
|
||||
run: |
|
||||
until nc -z 127.0.0.1 80; do echo waiting for nginx; sleep 1; done;
|
||||
until nc -z 127.0.0.1 1025; do echo waiting for mailserver; sleep 1; done;
|
||||
|
||||
- name: End-to-end tests | run tests
|
||||
id: e2e-tests
|
||||
run: |
|
||||
cd e2e-tests/playwright/java
|
||||
mvn -B install -D skipTests --no-transfer-progress
|
||||
mvn exec:java -e -D exec.mainClass=com.microsoft.playwright.CLI -D exec.args="install --with-deps"
|
||||
mvn test
|
||||
@ -174,6 +174,13 @@ turbo frontend#dev backend#start admin#start --env-mode=loose
|
||||
|
||||
Tip: for local setup use a local nginx server with similar config like docker nginx [nginx.conf](./nginx/gradido.conf) but replace docker image name with localhost
|
||||
|
||||
### Clear
|
||||
In root folder calling `yarn clear` will clear all turbo caches, node_modules and build folders of all workspaces for a clean rebuild.
|
||||
|
||||
```bash
|
||||
yarn clear
|
||||
```
|
||||
|
||||
|
||||
## Services defined in this package
|
||||
|
||||
|
||||
@ -19,7 +19,8 @@
|
||||
"test:debug": "cross-env TZ=UTC node --inspect-brk ./node_modules/vitest/vitest.mjs",
|
||||
"test:watch": "cross-env TZ=UTC vitest",
|
||||
"locales": "scripts/sort.sh",
|
||||
"locales:fix": "scripts/sort.sh --fix"
|
||||
"locales:fix": "scripts/sort.sh --fix",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/json": "^2.2.228",
|
||||
|
||||
@ -4,6 +4,7 @@ JWT_EXPIRES_IN=2m
|
||||
GDT_ACTIVE=false
|
||||
HUMHUB_ACTIVE=false
|
||||
GMS_ACTIVE=false
|
||||
USE_CRYPTO_WORKER=true
|
||||
|
||||
# Email
|
||||
EMAIL=true
|
||||
|
||||
@ -22,7 +22,8 @@
|
||||
"locales": "scripts/sort.sh",
|
||||
"locales:fix": "scripts/sort.sh --fix",
|
||||
"start": "cross-env TZ=UTC node build/index.js",
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
|
||||
@ -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,
|
||||
@ -43,13 +41,12 @@ import {
|
||||
} from '@/util/InterruptiveSleepManager'
|
||||
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,
|
||||
@ -579,7 +576,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,
|
||||
|
||||
@ -5,12 +5,14 @@ import { sendTransactionsToDltConnector } from './apis/dltConnector/sendTransact
|
||||
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}`)
|
||||
|
||||
@ -58,6 +58,7 @@ const run = async () => {
|
||||
const { con } = server
|
||||
await cleanDB()
|
||||
logger.info('##seed## clean database successful...')
|
||||
logger.info(`crypto worker enabled: ${CONFIG.USE_CRYPTO_WORKER}`)
|
||||
|
||||
// seed home community
|
||||
await writeHomeCommunityEntry()
|
||||
|
||||
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",
|
||||
},
|
||||
|
||||
@ -21,7 +21,8 @@
|
||||
"test": "bun test",
|
||||
"test:debug": "bun test --inspect-brk",
|
||||
"lint": "biome check --error-on-warnings .",
|
||||
"lint:fix": "biome check --error-on-warnings . --write"
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
|
||||
@ -21,17 +21,21 @@
|
||||
"test:debug": "bun test --inspect-brk",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check --error-on-warnings .",
|
||||
"lint:fix": "biome check --error-on-warnings . --write"
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"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`;')
|
||||
}
|
||||
@ -19,7 +19,7 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check --error-on-warnings .",
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"clear": "cross-env TZ=UTC tsx migration/index.ts clear",
|
||||
"clearDB": "cross-env TZ=UTC tsx migration/index.ts clear",
|
||||
"test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test vitest --reporter verbose --no-file-parallelism run",
|
||||
"test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles",
|
||||
"up": "cross-env TZ=UTC tsx migration/index.ts up",
|
||||
@ -28,7 +28,8 @@
|
||||
"up:test": "cross-env TZ=UTC DB_DATABASE=gradido_test tsx migration/index.ts up",
|
||||
"up:backend_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_backend tsx migration/index.ts up",
|
||||
"up:federation_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_federation tsx migration/index.ts up",
|
||||
"up:dht_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_dht tsx migration/index.ts up"
|
||||
"up:dht_test": "cross-env TZ=UTC DB_DATABASE=gradido_test_dht tsx migration/index.ts up",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -282,7 +282,7 @@ bun install
|
||||
|
||||
# build all modules
|
||||
log_step 'build all modules'
|
||||
turbo build --env-mode=loose
|
||||
turbo build --env-mode=loose --concurrency=$(nproc)
|
||||
|
||||
# database
|
||||
log_step 'Updating database'
|
||||
|
||||
@ -17,7 +17,8 @@
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --verbose --runInBand --forceExit --detectOpenHandles",
|
||||
"test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles",
|
||||
"test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --coverage --runInBand --forceExit --detectOpenHandles"
|
||||
"test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_dht jest --coverage --runInBand --forceExit --detectOpenHandles",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
|
||||
2
e2e-tests/.gitignore
vendored
2
e2e-tests/.gitignore
vendored
@ -3,4 +3,4 @@ cypress/screenshots/
|
||||
cypress/videos/
|
||||
cypress/reports/
|
||||
cucumber-messages.ndjson
|
||||
|
||||
**/target
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
###############################################################################
|
||||
# Dockerfile to create a ready-to-use Playwright Docker image for end-to-end
|
||||
# testing.
|
||||
#
|
||||
# To avoid hardcoded versoning of Playwright, this Dockerfile is a custom
|
||||
# version of the ready-to-use Dockerfile privided by Playwright developement
|
||||
# (https://github.com/microsoft/playwright/blob/main/utils/docker/Dockerfile.focal)
|
||||
#
|
||||
# Here the latest stable versions of the browsers Chromium, Firefox, and Webkit
|
||||
# (Safari) are installed, icluding all dependencies based on Ubuntu specified by
|
||||
# Playwright developement.
|
||||
###############################################################################
|
||||
|
||||
FROM ubuntu:focal
|
||||
|
||||
# set a timezone for the Playwright browser dependency installation
|
||||
ARG TZ=Europe/Berlin
|
||||
|
||||
ARG DOCKER_WORKDIR=/tests/
|
||||
WORKDIR $DOCKER_WORKDIR
|
||||
|
||||
# package manager preparation
|
||||
RUN apt-get -qq update && apt-get install -qq -y curl gpg > /dev/null
|
||||
# for Node.js
|
||||
RUN curl -sL https://deb.nodesource.com/setup_16.x | bash -
|
||||
# for Yarn
|
||||
RUN curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
||||
|
||||
# install node v16 and Yarn
|
||||
RUN apt-get -qq update && apt-get install -qq -y nodejs yarn
|
||||
|
||||
COPY tests/package.json tests/yarn.lock $DOCKER_WORKDIR
|
||||
|
||||
# install Playwright with all dependencies
|
||||
# for the browsers chromium, firefox, and webkit
|
||||
RUN yarn install && yarn playwright install --with-deps
|
||||
|
||||
# clean up
|
||||
RUN rm -rf /var/lib/apt/lists/* && apt-get -qq clean
|
||||
|
||||
COPY tests/ $DOCKER_WORKDIR
|
||||
@ -1,24 +0,0 @@
|
||||
# Gradido End-to-End Testing with [Playwright](https://playwright.dev/) (CI-ready via Docker)
|
||||
|
||||
|
||||
A sample setup to show-case Playwright (using Typescript) as an end-to-end testing tool for Gradido runniing in a Docker container.
|
||||
Here we have a simple UI-based happy path login test running against the DEV system.
|
||||
|
||||
## Precondition
|
||||
Since dependencies and configurations for Github Actions integration is not set up yet, please run in root directory
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
to boot up the DEV system, before running the test.
|
||||
|
||||
## Execute the test
|
||||
|
||||
```bash
|
||||
# build a Docker image from the Dockerfile
|
||||
docker build -t gradido_e2e-tests-playwright .
|
||||
|
||||
# run the Docker container and execute the given tests
|
||||
docker run -it --network=host gradido_e2e-tests-playwright yarn playwright-e2e-tests
|
||||
```
|
||||
12
e2e-tests/playwright/java/Readme.md
Normal file
12
e2e-tests/playwright/java/Readme.md
Normal file
@ -0,0 +1,12 @@
|
||||
# gradido-e2e-tests-playwright with java
|
||||
|
||||
Experimental End-to-end tests with Playwright and Java
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Java 17
|
||||
- Maven
|
||||
- Playwright
|
||||
- Gradle
|
||||
- Git
|
||||
|
||||
52
e2e-tests/playwright/java/pom.xml
Normal file
52
e2e-tests/playwright/java/pom.xml
Normal file
@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>net.gradido</groupId>
|
||||
<artifactId>gradido-e2e-tests-playwright</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<name>Gradido Playwright End-to-End Tests in Java</name>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.microsoft.playwright</groupId>
|
||||
<artifactId>playwright</artifactId>
|
||||
<version>1.52.0</version>
|
||||
</dependency>
|
||||
<!-- JUnit Jupiter API & Engine -->
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.10.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.10.1</version>
|
||||
<!-- References to interface static methods are allowed only at source level 1.8 or above -->
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.0.0-M9</version>
|
||||
<configuration>
|
||||
<includes>
|
||||
<include>**/*Test*.java</include>
|
||||
</includes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@ -0,0 +1,69 @@
|
||||
package net.gradido.e2e.base;
|
||||
|
||||
import com.microsoft.playwright.Browser;
|
||||
import com.microsoft.playwright.BrowserContext;
|
||||
import com.microsoft.playwright.Playwright;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.Tracing;
|
||||
import org.junit.jupiter.api.*;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
// Subclasses will inherit PER_CLASS behavior.
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
public class BaseTest {
|
||||
|
||||
protected Playwright playwright;
|
||||
protected Browser browser;
|
||||
|
||||
@BeforeAll
|
||||
void setUpAll() {
|
||||
playwright = Playwright.create();
|
||||
browser = playwright.chromium().launch();
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
void tearDownAll() {
|
||||
playwright.close();
|
||||
}
|
||||
|
||||
// New instance for each test method.
|
||||
protected BrowserContext context;
|
||||
protected Page page;
|
||||
protected Path currentTracePath;
|
||||
|
||||
@BeforeEach
|
||||
void setUp(TestInfo testInfo) {
|
||||
context = browser.newContext(
|
||||
new Browser.NewContextOptions().setBaseURL("http://localhost:3000")
|
||||
);
|
||||
context.route("**/*", route -> {
|
||||
String url = route.request().url();
|
||||
|
||||
// we skip fontawesome and googleapis requests, we don't need them for functions test, but they cost time
|
||||
if (url.contains("use.fontawesome.com") || url.contains("fonts.googleapis.com")) {
|
||||
route.abort();
|
||||
return;
|
||||
}
|
||||
route.resume();
|
||||
});
|
||||
|
||||
// Start tracing before creating
|
||||
String testName = testInfo.getDisplayName().replaceAll("[^a-zA-Z0-9]", "");
|
||||
currentTracePath = Paths.get("target/traces/" + testName + ".zip");
|
||||
context.tracing().start(new Tracing.StartOptions()
|
||||
.setScreenshots(true)
|
||||
.setSnapshots(true)
|
||||
.setSources(true));
|
||||
page = context.newPage();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
// Stop tracing and export it into a zip archive.
|
||||
context.tracing().stop(new Tracing.StopOptions().setPath(currentTracePath));
|
||||
|
||||
page.close();
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package net.gradido.e2e.components;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class Toasts {
|
||||
public final Locator toastSlot;
|
||||
public final Locator toastTypeError;
|
||||
public final Locator toastTitle;
|
||||
public final Locator toastMessage;
|
||||
|
||||
public Toasts(Page page) {
|
||||
toastSlot = page.locator("#__BVID__toaster-container");
|
||||
toastTypeError = toastSlot.locator(".toast.text-bg-danger");
|
||||
toastTitle = toastTypeError.locator(".gdd-toaster-title");
|
||||
toastMessage = toastTypeError.locator(".gdd-toaster-body");
|
||||
}
|
||||
|
||||
public void assertErrorToastVisible() {
|
||||
toastTypeError.waitFor();
|
||||
assertTrue(toastTitle.isVisible());
|
||||
assertTrue(toastMessage.isVisible());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package net.gradido.e2e.pages;
|
||||
|
||||
import com.microsoft.playwright.Locator;
|
||||
import com.microsoft.playwright.Page;
|
||||
import com.microsoft.playwright.Response;
|
||||
|
||||
public class LoginPage {
|
||||
private final Page page;
|
||||
|
||||
private final Locator emailInput;
|
||||
private final Locator passwordInput;
|
||||
private final Locator submitButton;
|
||||
|
||||
public LoginPage(Page page) {
|
||||
this.page = page;
|
||||
emailInput = page.locator("input[name='email']");
|
||||
passwordInput = page.locator("input[name='password']");
|
||||
submitButton = page.locator("button[type='submit']");
|
||||
}
|
||||
|
||||
public void login(String email, String password) {
|
||||
emailInput.fill(email);
|
||||
passwordInput.fill(password);
|
||||
Response response = page.waitForResponse("**/graphql", () -> {
|
||||
submitButton.click();
|
||||
});
|
||||
}
|
||||
|
||||
public void navigate() {
|
||||
page.navigate("http://localhost:3000/login");
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package net.gradido.e2e.tests;
|
||||
|
||||
import net.gradido.e2e.base.BaseTest;
|
||||
import net.gradido.e2e.pages.LoginPage;
|
||||
import net.gradido.e2e.components.Toasts;
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class InvalidLoginTest extends BaseTest {
|
||||
private LoginPage loginPage;
|
||||
|
||||
@BeforeEach
|
||||
void initPageObjects() {
|
||||
loginPage = new LoginPage(page);
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidUserSeesError() {
|
||||
loginPage.navigate();
|
||||
loginPage.login("peter@lustig.de", "wrongpass");
|
||||
Toasts toast = new Toasts(page);
|
||||
toast.assertErrorToastVisible();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package net.gradido.e2e.tests;
|
||||
|
||||
import net.gradido.e2e.base.BaseTest;
|
||||
import net.gradido.e2e.pages.LoginPage;
|
||||
import net.gradido.e2e.components.Toasts;
|
||||
|
||||
import org.junit.jupiter.api.*;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
public class ValidLoginTest extends BaseTest {
|
||||
private LoginPage loginPage;
|
||||
|
||||
@BeforeEach
|
||||
void initPageObjects() {
|
||||
loginPage = new LoginPage(page);
|
||||
}
|
||||
|
||||
@Test
|
||||
void validUserCanLogin() {
|
||||
loginPage.navigate();
|
||||
loginPage.login("peter@lustig.de", "Aa12345_");
|
||||
page.waitForURL("http://localhost:3000/overview");
|
||||
assertTrue(page.url().contains("/overview"));
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
junit.jupiter.execution.parallel.enabled = true
|
||||
junit.jupiter.execution.parallel.mode.default = same_thread
|
||||
junit.jupiter.execution.parallel.mode.classes.default = concurrent
|
||||
junit.jupiter.execution.parallel.config.strategy=dynamic
|
||||
junit.jupiter.execution.parallel.config.dynamic.factor=0.5
|
||||
@ -1,10 +0,0 @@
|
||||
import { FullConfig } from '@playwright/test';
|
||||
|
||||
async function globalSetup(config: FullConfig) {
|
||||
process.env.EMAIL = 'bibi@bloxberg.de';
|
||||
process.env.PASSWORD = 'Aa12345_';
|
||||
process.env.GMS_ACTIVE = false;
|
||||
process.env.HUMHUB_ACTIVE = false;
|
||||
}
|
||||
|
||||
export default globalSetup;
|
||||
@ -1,15 +0,0 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { LoginPage } from './models/login_page';
|
||||
import { WelcomePage } from './models/welcome_page';
|
||||
|
||||
|
||||
test('Gradido login test (happy path)', async ({ page }) => {
|
||||
const { EMAIL, PASSWORD } = process.env;
|
||||
const loginPage = new LoginPage(page);
|
||||
await loginPage.goto();
|
||||
await loginPage.enterEmail(EMAIL);
|
||||
await loginPage.enterPassword(PASSWORD);
|
||||
await loginPage.submitLogin();
|
||||
// assertions
|
||||
await expect(page).toHaveURL('./overview');
|
||||
});
|
||||
@ -1,33 +0,0 @@
|
||||
import { expect, test, Locator, Page } from '@playwright/test';
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page;
|
||||
readonly url: string;
|
||||
readonly emailInput: Locator;
|
||||
readonly passwordInput: Locator;
|
||||
readonly submitBtn: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.url = './login';
|
||||
this.emailInput = page.locator('id=Email-input-field');
|
||||
this.passwordInput = page.locator('id=Password-input-field');
|
||||
this.submitBtn = page.locator('text=Login');
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto(this.url);
|
||||
}
|
||||
|
||||
async enterEmail(email: string) {
|
||||
await this.emailInput.fill(email);
|
||||
}
|
||||
|
||||
async enterPassword(password: string) {
|
||||
await this.passwordInput.fill(password);
|
||||
}
|
||||
|
||||
async submitLogin() {
|
||||
await this.submitBtn.click();
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
import { expect, Locator, Page } from '@playwright/test';
|
||||
|
||||
export class WelcomePage {
|
||||
readonly page: Page;
|
||||
readonly url: string;
|
||||
readonly profileLink: Locator;
|
||||
|
||||
constructor(page: Page){
|
||||
this.page = page;
|
||||
this.url = './overview';
|
||||
this.profileLink = page.locator('href=/profile');
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import type { PlaywrightTestConfig } from '@playwright/test';
|
||||
|
||||
const config: PlaywrightTestConfig = {
|
||||
globalSetup: require.resolve('./global-setup'),
|
||||
ignoreHTTPSErrors: true,
|
||||
locale: 'de-DE',
|
||||
reporter: process.env.CI ? 'github' : 'list',
|
||||
retries: 1,
|
||||
screenshot: 'only-on-failure',
|
||||
testDir: '.',
|
||||
timeout: 30000,
|
||||
trace: 'on-first-retry',
|
||||
video: 'never',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
use: {
|
||||
baseURL: process.env.URL || 'http://127.0.0.1:3000',
|
||||
browserName: 'webkit',
|
||||
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
7
e2e-tests/playwright/typescript/.gitignore
vendored
Normal file
7
e2e-tests/playwright/typescript/.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
|
||||
# Playwright
|
||||
node_modules/
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
22
e2e-tests/playwright/typescript/components/Toasts.ts
Normal file
22
e2e-tests/playwright/typescript/components/Toasts.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
export class Toasts {
|
||||
public readonly toastSlot: Locator
|
||||
public readonly toastTypeError: Locator
|
||||
public readonly toastTitle: Locator
|
||||
public readonly toastMessage: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.toastSlot = page.locator('#__BVID__toaster-container')
|
||||
this.toastTypeError = this.toastSlot.locator('.toast.text-bg-danger')
|
||||
this.toastTitle = this.toastTypeError.locator('.gdd-toaster-title')
|
||||
this.toastMessage = this.toastTypeError.locator('.gdd-toaster-body')
|
||||
}
|
||||
|
||||
async assertErrorToastVisible(): Promise<void> {
|
||||
await this.toastTypeError.waitFor({ state: 'visible' })
|
||||
expect(this.toastTitle).toBeVisible()
|
||||
expect(this.toastMessage).toBeVisible()
|
||||
}
|
||||
}
|
||||
1
e2e-tests/playwright/typescript/config/index.ts
Normal file
1
e2e-tests/playwright/typescript/config/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export const FRONTEND_URL = "http://localhost:3000"
|
||||
13
e2e-tests/playwright/typescript/package.json
Normal file
13
e2e-tests/playwright/typescript/package.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "typescript",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.53.1",
|
||||
"@types/node": "^24.0.7"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "playwright test --reporter=list"
|
||||
}
|
||||
}
|
||||
28
e2e-tests/playwright/typescript/pages/LoginPage.ts
Normal file
28
e2e-tests/playwright/typescript/pages/LoginPage.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
import { FRONTEND_URL } from '../config'
|
||||
|
||||
export class LoginPage {
|
||||
readonly page: Page
|
||||
readonly emailInput: Locator
|
||||
readonly passwordInput: Locator
|
||||
readonly submitButton: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
this.emailInput = page.locator('#email-input-field')
|
||||
this.passwordInput = page.locator('#password-input-field')
|
||||
this.submitButton = page.locator("button[type='submit']")
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto(`${FRONTEND_URL}/login`)
|
||||
}
|
||||
|
||||
async login(email: string, password: string) {
|
||||
await this.emailInput.fill(email)
|
||||
await this.passwordInput.fill(password)
|
||||
const responsePromise = this.page.waitForResponse('**/graphql')
|
||||
await this.submitButton.click()
|
||||
await responsePromise
|
||||
}
|
||||
}
|
||||
82
e2e-tests/playwright/typescript/playwright.config.ts
Normal file
82
e2e-tests/playwright/typescript/playwright.config.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// import dotenv from 'dotenv';
|
||||
// import path from 'path';
|
||||
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
// Glob patterns or regular expressions that match test files.
|
||||
testMatch: '*',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: 2,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? '90%' : '75%',
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://localhost:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
video: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
},
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://localhost:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
13
e2e-tests/playwright/typescript/tests/InvalidLoginTest.ts
Normal file
13
e2e-tests/playwright/typescript/tests/InvalidLoginTest.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { LoginPage } from '../pages/LoginPage'
|
||||
import { Toasts } from '../components/Toasts'
|
||||
|
||||
test('invalid login', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page)
|
||||
await loginPage.goto()
|
||||
await loginPage.login('peter@lustig.de', 'wrongpass')
|
||||
|
||||
const toast = new Toasts(page)
|
||||
await toast.assertErrorToastVisible()
|
||||
await page.waitForTimeout(50)
|
||||
})
|
||||
11
e2e-tests/playwright/typescript/tests/ValidLoginTest.ts
Normal file
11
e2e-tests/playwright/typescript/tests/ValidLoginTest.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { LoginPage } from '../pages/LoginPage'
|
||||
import { FRONTEND_URL } from '../config'
|
||||
|
||||
test('valid login', async ({ page }) => {
|
||||
const loginPage = new LoginPage(page)
|
||||
await loginPage.goto()
|
||||
await loginPage.login('peter@lustig.de', 'Aa12345_')
|
||||
await page.waitForURL(`${FRONTEND_URL}/overview`)
|
||||
expect(page.url()).toContain('/overview')
|
||||
})
|
||||
41
e2e-tests/playwright/typescript/yarn.lock
Normal file
41
e2e-tests/playwright/typescript/yarn.lock
Normal file
@ -0,0 +1,41 @@
|
||||
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@playwright/test@^1.53.1":
|
||||
version "1.53.1"
|
||||
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.53.1.tgz#3ad5a2ce334b4a78390fd91e0a9d8423c09bc808"
|
||||
integrity sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==
|
||||
dependencies:
|
||||
playwright "1.53.1"
|
||||
|
||||
"@types/node@^24.0.7":
|
||||
version "24.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.0.7.tgz#ee580f7850c7eabaeef61ef96b8d8c04fdf94f53"
|
||||
integrity sha512-YIEUUr4yf8q8oQoXPpSlnvKNVKDQlPMWrmOcgzoduo7kvA2UF0/BwJ/eMKFTiTtkNL17I0M6Xe2tvwFU7be6iw==
|
||||
dependencies:
|
||||
undici-types "~7.8.0"
|
||||
|
||||
fsevents@2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
|
||||
integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
|
||||
|
||||
playwright-core@1.53.1:
|
||||
version "1.53.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.53.1.tgz#0b6f7a2006ccb6126ffcc3e3b2fa9efda23b6638"
|
||||
integrity sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==
|
||||
|
||||
playwright@1.53.1:
|
||||
version "1.53.1"
|
||||
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.53.1.tgz#86fb041b237a6868d163c87c4b9737fd1cac145e"
|
||||
integrity sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==
|
||||
dependencies:
|
||||
playwright-core "1.53.1"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
undici-types@~7.8.0:
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.8.0.tgz#de00b85b710c54122e44fbfd911f8d70174cd294"
|
||||
integrity sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==
|
||||
@ -18,7 +18,8 @@
|
||||
"test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_federation node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles",
|
||||
"test:coverage": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test_federation jest --coverage --runInBand --forceExit --detectOpenHandles",
|
||||
"lint": "biome check --error-on-warnings .",
|
||||
"lint:fix": "biome check --error-on-warnings . --write"
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
@ -41,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')
|
||||
}
|
||||
|
||||
1
frontend/.env.test_e2e
Normal file
1
frontend/.env.test_e2e
Normal file
@ -0,0 +1 @@
|
||||
GRAPHQL_URI=http://127.0.0.1:4000/graphql
|
||||
@ -21,7 +21,8 @@
|
||||
"compile-scss": "node ./scripts/scss.mjs compile",
|
||||
"watch-scss": "node ./scripts/scss.mjs watch",
|
||||
"compile-scss-sass": "node ./scripts/scss.mjs compile sass",
|
||||
"watch-scss-sass": "node ./scripts/scss.mjs watch sass"
|
||||
"watch-scss-sass": "node ./scripts/scss.mjs watch sass",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@morev/vue-transitions": "^3.0.2",
|
||||
|
||||
@ -8,11 +8,11 @@
|
||||
<BImg class="sheet-img position-absolute d-block d-lg-none zindex1000" :src="sheet"></BImg>
|
||||
<BCollapse id="nav-collapse" is-nav>
|
||||
<BNavbarNav class="ms-auto me-4 d-none d-lg-flex" right>
|
||||
<NavItem :to="register()" class="auth-navbar ms-lg-5">
|
||||
<NavItem :to="routeWithParamsAndQuery('Register')" class="auth-navbar ms-lg-5">
|
||||
{{ $t('signup') }}
|
||||
</NavItem>
|
||||
<span class="d-none d-lg-block py-1">{{ $t('|') }}</span>
|
||||
<NavItem :to="login()" class="auth-navbar">
|
||||
<NavItem :to="routeWithParamsAndQuery('Login')" class="auth-navbar">
|
||||
{{ $t('signin') }}
|
||||
</NavItem>
|
||||
</BNavbarNav>
|
||||
@ -25,7 +25,7 @@
|
||||
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
import NavItem from '../Menu/NavItem.vue'
|
||||
|
||||
const { login, register } = useAuthLinks()
|
||||
const { routeWithParamsAndQuery } = useAuthLinks()
|
||||
|
||||
const backgroundHeader = '/img/template/gradido_background_header.png'
|
||||
const logo = '/img/brand/gradido-logo_200x59.png'
|
||||
|
||||
@ -2,9 +2,13 @@
|
||||
<div class="navbar-small">
|
||||
<BNavbar class="navi">
|
||||
<BNavbarNav>
|
||||
<NavItem :to="register()" class="auth-navbar">{{ $t('signup') }}</NavItem>
|
||||
<NavItem :to="routeWithParamsAndQuery('Register')" class="auth-navbar">
|
||||
{{ $t('signup') }}
|
||||
</NavItem>
|
||||
<span class="mt-1">{{ $t('|') }}</span>
|
||||
<NavItem :to="login()" class="auth-navbar">{{ $t('signin') }}</NavItem>
|
||||
<NavItem :to="routeWithParamsAndQuery('Login')" class="auth-navbar">
|
||||
{{ $t('signin') }}
|
||||
</NavItem>
|
||||
</BNavbarNav>
|
||||
</BNavbar>
|
||||
</div>
|
||||
@ -14,7 +18,7 @@
|
||||
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
import NavItem from '../Menu/NavItem.vue'
|
||||
|
||||
const { login, register } = useAuthLinks()
|
||||
const { routeWithParamsAndQuery } = useAuthLinks()
|
||||
</script>
|
||||
<style scoped>
|
||||
.navi {
|
||||
|
||||
@ -10,13 +10,15 @@
|
||||
<BRow>
|
||||
<BCol sm="12" md="6">
|
||||
<p>{{ $t('gdd_per_link.no-account') }}</p>
|
||||
<BButton variant="primary" :to="register()">
|
||||
<BButton variant="primary" :to="routeWithParamsAndQuery('Register')">
|
||||
{{ $t('gdd_per_link.to-register') }}
|
||||
</BButton>
|
||||
</BCol>
|
||||
<BCol sm="12" md="6" class="mt-4 mt-lg-0">
|
||||
<p>{{ $t('gdd_per_link.has-account') }}</p>
|
||||
<BButton variant="gradido" :to="login()">{{ $t('gdd_per_link.to-login') }}</BButton>
|
||||
<BButton variant="gradido" :to="routeWithParamsAndQuery('Login')">
|
||||
{{ $t('gdd_per_link.to-login') }}
|
||||
</BButton>
|
||||
</BCol>
|
||||
</BRow>
|
||||
</BCard>
|
||||
@ -26,7 +28,7 @@
|
||||
<script setup>
|
||||
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
|
||||
const { login, register } = useAuthLinks()
|
||||
const { routeWithParamsAndQuery } = useAuthLinks()
|
||||
defineProps({
|
||||
linkData: { type: Object, required: true },
|
||||
isContributionLink: { type: Boolean, default: false },
|
||||
|
||||
@ -17,13 +17,21 @@
|
||||
<BRow>
|
||||
<BCol sm="12" md="6">
|
||||
<p>{{ $t('gdd_per_link.no-account') }}</p>
|
||||
<BButton variant="primary" :disabled="isForeignCommunitySelected" :to="register()">
|
||||
<BButton
|
||||
variant="primary"
|
||||
:disabled="isForeignCommunitySelected"
|
||||
:to="routeWithParamsAndQuery('Register')"
|
||||
>
|
||||
{{ $t('gdd_per_link.to-register') }}
|
||||
</BButton>
|
||||
</BCol>
|
||||
<BCol sm="12" md="6" class="mt-4 mt-lg-0">
|
||||
<p>{{ $t('gdd_per_link.has-account') }}</p>
|
||||
<BButton variant="gradido" :disabled="isForeignCommunitySelected" :to="login()">
|
||||
<BButton
|
||||
variant="gradido"
|
||||
:disabled="isForeignCommunitySelected"
|
||||
:to="routeWithParamsAndQuery('Login')"
|
||||
>
|
||||
{{ $t('gdd_per_link.to-login') }}
|
||||
</BButton>
|
||||
</BCol>
|
||||
@ -37,7 +45,7 @@ import { ref, computed } from 'vue'
|
||||
import CONFIG from '@/config'
|
||||
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
|
||||
const { login, register } = useAuthLinks()
|
||||
const { routeWithParamsAndQuery } = useAuthLinks()
|
||||
const props = defineProps({
|
||||
linkData: { type: Object, required: true },
|
||||
redeemCode: { type: String, required: true },
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<span v-if="isActive" class="nav-link active" aria-current="page">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<BNavItem v-else :to="href" class="auth-navbar"><slot></slot></BNavItem>
|
||||
<BNavItem v-else v-bind="props" class="auth-navbar"><slot></slot></BNavItem>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,19 +1,20 @@
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
export function useAuthLinks() {
|
||||
const { params } = useRoute()
|
||||
|
||||
const login = () => {
|
||||
if (params.code) return '/login/' + params.code
|
||||
return '/login'
|
||||
}
|
||||
const register = () => {
|
||||
if (params.code) return '/register/' + params.code
|
||||
return '/register'
|
||||
const route = useRoute()
|
||||
/**
|
||||
* Combine current route params and query with given params and query
|
||||
* @param {string} name
|
||||
* @param {{ params: {}, query: {} }} options
|
||||
* @returns {{ name: string, params: {}, query: {} }} a vue3 routing object for :to
|
||||
*/
|
||||
const routeWithParamsAndQuery = (name, options = { params: {}, query: {} }) => {
|
||||
return {
|
||||
name,
|
||||
params: { ...route.params, ...options.params },
|
||||
query: { ...route.query, ...options.query },
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
login,
|
||||
register,
|
||||
}
|
||||
return { routeWithParamsAndQuery }
|
||||
}
|
||||
|
||||
@ -13,7 +13,10 @@
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="d-flex justify-content-end mb-4 mb-lg-0">
|
||||
<router-link to="/forgot-password" data-test="forgot-password-link">
|
||||
<router-link
|
||||
:to="routeWithParamsAndQuery('ForgotPassword')"
|
||||
data-test="forgot-password-link"
|
||||
>
|
||||
{{ $t('settings.password.forgot_pwd') }}
|
||||
</router-link>
|
||||
</BCol>
|
||||
@ -39,7 +42,7 @@
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="mt-1 auth-navbar">
|
||||
<BLink :to="register()">
|
||||
<BLink :to="routeWithParamsAndQuery('Register')">
|
||||
{{ $t('signup') }}
|
||||
</BLink>
|
||||
</BCol>
|
||||
@ -82,7 +85,7 @@ const { mutate } = useMutation(login)
|
||||
const { mutate: mutateHumhubAutoLogin } = useMutation(authenticateHumhubAutoLoginProject)
|
||||
// const $loading = useLoading() // TODO needs to be updated but there is some sort of an issue that breaks the app.
|
||||
const { toastError } = useAppToast()
|
||||
const { register } = useAuthLinks()
|
||||
const { routeWithParamsAndQuery } = useAuthLinks()
|
||||
|
||||
const form = ref({
|
||||
email: '',
|
||||
@ -123,7 +126,7 @@ const onSubmit = handleSubmit(async (values) => {
|
||||
}
|
||||
|
||||
if (route.params.code) {
|
||||
await router.push(`/redeem/${route.params.code}`)
|
||||
await router.push(routeWithParamsAndQuery('Redeem'))
|
||||
} else {
|
||||
await router.push(store.state.redirectPath)
|
||||
}
|
||||
@ -131,12 +134,12 @@ const onSubmit = handleSubmit(async (values) => {
|
||||
if (error.message.includes('User email not validated')) {
|
||||
showPageMessage.value = true
|
||||
errorSubtitle.value = t('message.activateEmail')
|
||||
errorLinkTo.value = '/forgot-password'
|
||||
errorLinkTo.value = routeWithParamsAndQuery('ForgotPassword')
|
||||
toastError(t('error.no-account'))
|
||||
} else if (error.message.includes('User has no password set yet')) {
|
||||
showPageMessage.value = true
|
||||
errorSubtitle.value = t('message.unsetPassword')
|
||||
errorLinkTo.value = '/reset-password/login'
|
||||
errorLinkTo.value = routeWithParamsAndQuery('ResetPassword')
|
||||
toastError(t('error.no-account'))
|
||||
} else if (error.message.includes('No user with this credentials')) {
|
||||
toastError(t('error.no-user'))
|
||||
|
||||
@ -77,7 +77,7 @@
|
||||
</BRow>
|
||||
<BRow>
|
||||
<BCol class="mt-1 auth-navbar">
|
||||
<BLink :to="login()">
|
||||
<BLink :to="routeWithParamsAndQuery('Login')">
|
||||
{{ $t('signin') }}
|
||||
</BLink>
|
||||
</BCol>
|
||||
@ -105,7 +105,7 @@ import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
import CONFIG from '@/config'
|
||||
|
||||
const { toastError } = useAppToast()
|
||||
const { login } = useAuthLinks()
|
||||
const { routeWithParamsAndQuery } = useAuthLinks()
|
||||
|
||||
const { mutate } = useMutation(createUser)
|
||||
|
||||
|
||||
@ -417,7 +417,10 @@ describe('ResetPassword', () => {
|
||||
'...email was sent more than 23 hours and 10 minutes ago',
|
||||
)
|
||||
expect(message.props('buttonText')).toBe('settings.password.reset')
|
||||
expect(message.props('linkTo')).toBe('/forgot-password/resetPassword')
|
||||
expect(message.props('linkTo')).toMatchObject({
|
||||
name: 'ForgotPassword',
|
||||
params: { comingFrom: 'reset-password' },
|
||||
})
|
||||
expect(mockToastError).toHaveBeenCalledWith(
|
||||
'...email was sent more than 23 hours and 10 minutes ago',
|
||||
)
|
||||
@ -435,7 +438,7 @@ describe('ResetPassword', () => {
|
||||
expect(message.props('headline')).toBe('message.title')
|
||||
expect(message.props('subtitle')).toBe('message.checkEmail')
|
||||
expect(message.props('buttonText')).toBe('login')
|
||||
expect(message.props('linkTo')).toBe('/login')
|
||||
expect(message.props('linkTo')).toMatchObject({ name: 'Login' })
|
||||
})
|
||||
|
||||
it('handles success response on /reset-password', async () => {
|
||||
@ -454,7 +457,7 @@ describe('ResetPassword', () => {
|
||||
expect(message.props('headline')).toBe('message.title')
|
||||
expect(message.props('subtitle')).toBe('message.checkEmail')
|
||||
expect(message.props('buttonText')).toBe('login')
|
||||
expect(message.props('linkTo')).toBe('/login')
|
||||
expect(message.props('linkTo')).toMatchObject({ name: 'Login' })
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -41,19 +41,22 @@ import InputPasswordConfirmation from '@/components/Inputs/InputPasswordConfirma
|
||||
import Message from '@/components/Message/Message.vue'
|
||||
import { useAppToast } from '@/composables/useToast'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { useAuthLinks } from '@/composables/useAuthLinks'
|
||||
|
||||
const { routeWithParamsAndQuery } = useAuthLinks()
|
||||
|
||||
const textFields = {
|
||||
reset: {
|
||||
title: 'settings.password.change-password',
|
||||
text: 'settings.password.reset-password.text',
|
||||
button: 'settings.password.change-password',
|
||||
linkTo: '/login',
|
||||
linkTo: routeWithParamsAndQuery('Login'),
|
||||
},
|
||||
checkEmail: {
|
||||
title: 'settings.password.set',
|
||||
text: 'settings.password.set-password.text',
|
||||
button: 'settings.password.set',
|
||||
linkTo: '/login',
|
||||
linkTo: routeWithParamsAndQuery('Login'),
|
||||
},
|
||||
}
|
||||
|
||||
@ -100,7 +103,7 @@ const onSubmit = async () => {
|
||||
? t('message.checkEmail')
|
||||
: t('message.reset')
|
||||
messageButtonText.value = t('login')
|
||||
messageButtonLinkTo.value = route.params.code ? `/login/${route.params.code}` : '/login'
|
||||
messageButtonLinkTo.value = routeWithParamsAndQuery('Login')
|
||||
} catch (error) {
|
||||
const errorMessage = error.message.match(
|
||||
/email was sent more than ([0-9]+ hours)?( and )?([0-9]+ minutes)? ago/,
|
||||
@ -112,7 +115,9 @@ const onSubmit = async () => {
|
||||
messageHeadline.value = t('message.errorTitle')
|
||||
messageSubtitle.value = errorMessage
|
||||
messageButtonText.value = t('settings.password.reset')
|
||||
messageButtonLinkTo.value = '/forgot-password/resetPassword'
|
||||
messageButtonLinkTo.value = routeWithParamsAndQuery('ForgotPassword', {
|
||||
params: { comingFrom: 'reset-password' },
|
||||
})
|
||||
toastError(errorMessage)
|
||||
}
|
||||
}
|
||||
@ -124,7 +129,9 @@ const checkOptInCode = async () => {
|
||||
})
|
||||
} catch (error) {
|
||||
toastError(error.message)
|
||||
await router.push('/forgot-password/resetPassword')
|
||||
await router.push(
|
||||
routeWithParamsAndQuery('ForgotPassword', { params: { comingFrom: 'reset-password' } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -114,18 +114,22 @@ const routes = [
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Login',
|
||||
path: '/login/:code?',
|
||||
component: () => import('@/pages/Login'),
|
||||
},
|
||||
{
|
||||
name: 'Register',
|
||||
path: '/register/:code?',
|
||||
component: () => import('@/pages/Register'),
|
||||
},
|
||||
{
|
||||
name: 'ForgotPassword',
|
||||
path: '/forgot-password',
|
||||
component: () => import('@/pages/ForgotPassword'),
|
||||
},
|
||||
{
|
||||
name: 'ForgotPasswordComingFrom',
|
||||
path: '/forgot-password/:comingFrom',
|
||||
component: () => import('@/pages/ForgotPassword'),
|
||||
},
|
||||
@ -138,14 +142,17 @@ const routes = [
|
||||
// component: () => import('@/pages/SelectCommunity'),
|
||||
// },
|
||||
{
|
||||
name: 'ResetPassword',
|
||||
path: '/reset-password/:optin',
|
||||
component: () => import('@/pages/ResetPassword'),
|
||||
},
|
||||
{
|
||||
name: 'CheckEmail',
|
||||
path: '/checkEmail/:optin/:code?',
|
||||
component: () => import('@/pages/ResetPassword'),
|
||||
},
|
||||
{
|
||||
name: 'Redeem',
|
||||
path: '/redeem/:code',
|
||||
component: () => import('@/pages/TransactionLink'),
|
||||
},
|
||||
|
||||
@ -25,11 +25,13 @@
|
||||
"docker": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml up",
|
||||
"docker:rebuild": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose -f docker-compose.yml build",
|
||||
"docker_dev": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose up",
|
||||
"docker_dev:rebuild": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose build"
|
||||
"docker_dev:rebuild": "cross-env BUILD_COMMIT=$(git rev-parse HEAD) docker compose build",
|
||||
"clear": "rm -rf .turbo && turbo clear"
|
||||
},
|
||||
"dependencies": {
|
||||
"auto-changelog": "^2.4.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"jose": "^4.14.4",
|
||||
"turbo": "^2.5.0",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
|
||||
@ -21,7 +21,8 @@
|
||||
"test:debug": "bun test --inspect-brk",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check --error-on-warnings .",
|
||||
"lint:fix": "biome check --error-on-warnings . --write"
|
||||
"lint:fix": "biome check --error-on-warnings . --write",
|
||||
"clear": "rm -rf node_modules && rm -rf build && rm -rf .turbo"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.0",
|
||||
@ -33,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
|
||||
@ -24,6 +24,9 @@
|
||||
"dependsOn": ["build"],
|
||||
"persistent": true,
|
||||
"cache": false
|
||||
},
|
||||
"clear": {
|
||||
"cache": false
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user