Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions samples/cliConfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"label": "AFJ Rest Agent 1",
"walletId": "sample",
"label": "Credo Rest Agent",
"walletId": "sharedAgent",
"walletKey": "sample",
"walletType": "postgres",
"walletUrl": "localhost:5432",
Expand Down Expand Up @@ -43,5 +43,6 @@
"schemaManagerContractAddress": "0x552992e9f14b15bBd76488cD4c38c89B80259f37",
"rpcUrl": "https://polygon-mumbai.infura.io/v3/0579d305568d404e996e49695e9272a3",
"fileServerUrl": "https://schema.credebl.id/",
"fileServerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBeWFuV29ya3MiLCJpZCI6ImNhZDI3ZjhjLTMyNWYtNDRmZC04ZmZkLWExNGNhZTY3NTMyMSJ9.I3IR7abjWbfStnxzn1BhxhV0OEzt1x3mULjDdUcgWHk"
"fileServerToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJBeWFuV29ya3MiLCJpZCI6ImNhZDI3ZjhjLTMyNWYtNDRmZC04ZmZkLWExNGNhZTY3NTMyMSJ9.I3IR7abjWbfStnxzn1BhxhV0OEzt1x3mULjDdUcgWHk",
"api-key": "supersecret"
}
156 changes: 144 additions & 12 deletions src/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,165 @@
import type * as express from 'express'
import type { RestAgentModules, RestMultiTenantAgentModules } from './cliAgent'
import type { TenantAgent } from '@aries-framework/tenants/build/TenantAgent'
import type { Request } from 'express'

import { LogLevel } from '@aries-framework/core'
import { Agent, LogLevel } from '@aries-framework/core'
import jwt, { decode } from 'jsonwebtoken'
import { container } from 'tsyringe'

import { AgentRole, ErrorMessages } from './enums/enum'
import { StatusException } from './error'
import { TsLogger } from './utils/logger'

// export type AgentType = Agent<RestAgentModules> | Agent<RestMultiTenantAgentModules> | TenantAgent<RestAgentModules>

let dynamicApiKey: string = 'api_key' // Initialize with a default value

export async function expressAuthentication(
request: express.Request,
securityName: string,
secMethod?: { [key: string]: any },
scopes?: string
) {
export async function expressAuthentication(request: Request, securityName: string, scopes?: string[]) {
const logger = new TsLogger(LogLevel.info)
const agent = container.resolve(Agent<RestMultiTenantAgentModules>)

logger.info(`securityName::: ${securityName}`)
logger.info(`scopes::: ${scopes}`)

logger.info(`secMethod::: ${JSON.stringify(secMethod)}`)
logger.info(`scopes::: ${JSON.stringify(scopes)}`)
if (scopes && scopes?.includes('skip')) {
// Skip authentication for this route or controller
request['agent'] = agent
return true
}

const apiKeyHeader = request.headers['authorization']

if (!apiKeyHeader) {
// return false
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
}

if (securityName === 'apiKey') {
// Auth: For BW/Dedicated agent to GET their token
if (apiKeyHeader) {
const providedApiKey = apiKeyHeader as string

if (providedApiKey === dynamicApiKey) {
return 'success'
request['agent'] = agent
return true
}
}
}

if (securityName === 'jwt') {
const tenancy = agent!.modules.tenants ? true : false
const tokenWithHeader = apiKeyHeader
const token = tokenWithHeader!.replace('Bearer ', '')
const reqPath = request.path
const decodedToken: jwt.JwtPayload = decode(token) as jwt.JwtPayload
const role: AgentRole = decodedToken.role

if (tenancy) {
// it should be a shared agent
if (role !== AgentRole.RestRootAgentWithTenants && role !== AgentRole.RestTenantAgent) {
// return false //'The agent is a multi-tenant agent'
logger.debug('Unknown role. The agent is a multi-tenant agent')
return Promise.reject(new StatusException('Unknown role', 401))
}
if (role === AgentRole.RestTenantAgent) {
// Logic if the token is of tenant agent
if (reqPath.includes('/multi-tenancy/')) {
// Note: Include the below logic for path detection instead of url
// if (scopes && scopes?.includes('multi-tenant')) {
logger.debug('Tenants cannot manage tenants')
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
} else {
// Auth: tenant agent
const tenantId: string = decodedToken.tenantId
if (!tenantId) {
// return false
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
}
const tenantAgent = await agent.modules.tenants.getTenantAgent({ tenantId })
if (!tenantAgent) {
// return false
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
}

const verified = await verifyToken(tenantAgent, token)
// Note: logic to store generate token for tenant using BW's secertKey
// const verified = await verifyToken(agent, token)

// Failed to verify token
if (!verified) {
// return false
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
}

// Only need to registerInstance for TenantAgent.
// return tenantAgent
request['agent'] = tenantAgent
return true
}
} else if (role === AgentRole.RestRootAgentWithTenants) {
// Auth: base wallet
const verified = await verifyToken(agent!, token)

// Base wallet cant access any endpoints apart from multi-tenant endpoint
// if (!reqPath.includes('/multi-tenancy/')) {
// logger.error('Basewallet can only manage tenants and can`t perform other operations')
// return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
// }

// Note: Implement the authorization part using scopes(below), instead of url(above)
// if (!scopes?.includes('multi-tenant')) {
// logger.error('Basewallet can only manage tenants')
// return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
// }

if (!verified) return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))

request['agent'] = agent
return true
} else {
// return false //'Invalid Token'
logger.debug('Invalid Token')
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
}
} else {
if (role !== AgentRole.RestRootAgent) {
logger.debug('This is a dedicated agent')
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
// return false //'This is a dedicated agent'
} else {
// Auth: dedicated agent

if (reqPath.includes('/multi-tenancy/'))
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))

const verified = await verifyToken(agent!, token)
if (!verified) return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
//return false

request['agent'] = agent
return true
}
}
}
// return false
return Promise.reject(new StatusException(ErrorMessages.Unauthorized, 401))
}

async function verifyToken(agent: Agent | TenantAgent<RestAgentModules>, token: string): Promise<boolean> {
const secretKey = await getSecretKey(agent)
const verified = jwt.verify(token, secretKey)

return verified ? true : false
}

// Common function to pass agent object and get secretKey
async function getSecretKey(
agent: Agent<RestMultiTenantAgentModules | RestAgentModules> | TenantAgent<RestAgentModules>
): Promise<string> {
const genericRecord = await agent.genericRecords.getAll()
const recordWithToken = genericRecord.find((record) => record?.content?.secretKey !== undefined)
const secretKey = recordWithToken?.content.secretKey as string

return secretKey
}

export function setDynamicApiKey(newApiKey: string) {
Expand Down
4 changes: 4 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ const parsed = yargs
boolean: true,
default: false,
})
.option('apiKey', {
string: true,
})
// .option('storage-config', {
// array: true,
// default: [],
Expand Down Expand Up @@ -198,5 +201,6 @@ export async function runCliServer() {
rpcUrl: parsed['rpcUrl'],
fileServerUrl: parsed['fileServerUrl'],
fileServerToken: parsed['fileServerToken'],
apiKey: parsed['apiKey'],
} as unknown as AriesRestConfig)
}
73 changes: 29 additions & 44 deletions src/cliAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ import { anoncreds } from '@hyperledger/anoncreds-nodejs'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
import { indyVdr } from '@hyperledger/indy-vdr-nodejs'
import axios from 'axios'
import { randomBytes } from 'crypto'
import { readFile } from 'fs/promises'
import jwt from 'jsonwebtoken'

// eslint-disable-next-line import/no-cycle
import { setupServer } from './server'
import { generateSecretKey } from './utils/common.service'
import { TsLogger } from './utils/logger'
import { BCOVRIN_TEST_GENESIS } from './utils/util'

Expand Down Expand Up @@ -100,6 +100,7 @@ export interface AriesRestConfig {
rpcUrl: string
fileServerUrl: string
fileServerToken: string
apiKey: string
}

export async function readRestConfig(path: string) {
Expand Down Expand Up @@ -202,23 +203,24 @@ const getWithTenantModules = (networkConfig: [IndyVdrPoolConfig, ...IndyVdrPoolC
}
}

async function generateSecretKey(length: number = 32): Promise<string> {
// Asynchronously generate a buffer containing random values
const buffer: Buffer = await new Promise((resolve, reject) => {
randomBytes(length, (error, buf) => {
if (error) {
reject(error)
} else {
resolve(buf)
}
})
})

// Convert the buffer to a hexadecimal string
const secretKey: string = buffer.toString('hex')

return secretKey
}
// Add this function in common service
// async function generateSecretKey(length: number = 32): Promise<string> {
// // Asynchronously generate a buffer containing random values
// const buffer: Buffer = await new Promise((resolve, reject) => {
// randomBytes(length, (error, buf) => {
// if (error) {
// reject(error)
// } else {
// resolve(buf)
// }
// })
// })

// // Convert the buffer to a hexadecimal string
// const secretKey: string = buffer.toString('hex')

// return secretKey
// }

export async function runRestAgent(restConfig: AriesRestConfig) {
const {
Expand All @@ -228,6 +230,7 @@ export async function runRestAgent(restConfig: AriesRestConfig) {
webhookUrl,
adminPort,
walletConfig,
apiKey,
...afjConfig
} = restConfig

Expand Down Expand Up @@ -327,36 +330,18 @@ export async function runRestAgent(restConfig: AriesRestConfig) {

await agent.initialize()

let token: string = ''
const genericRecord = await agent.genericRecords.getAll()
const recordsWithSecretKey = genericRecord.some((record) => record?.content?.secretKey)

const recordsWithToken = genericRecord.some((record) => record?.content?.token)
if (!genericRecord.length || !recordsWithToken) {
// Call the async function
const secretKeyInfo: string = await generateSecretKey()
// Check if the secretKey already exist in the genericRecords

// if already exist - then don't generate the secret key again
// Check if the JWT token already available in genericRecords - if yes, and also don't generate the JWT token
// instead use the existin JWT token
// if JWT token is not found, create/generate a new token and save in genericRecords
// next time, the same token should be used - instead of creating a new token on every restart event of the agent

// if already exist - then don't generate the secret key again
// Check if the JWT token already available in genericRecords - if yes, and also don't generate the JWT token
// instead use the existin JWT token
// if JWT token is not found, create/generate a new token and save in genericRecords
// next time, the same token should be used - instead of creating a new token on every restart event of the agent
token = jwt.sign({ agentInfo: 'agentInfo' }, secretKeyInfo)
if (!genericRecord.length || !recordsWithSecretKey) {
// If secretKey doesn't exist in genericRecord: i.e. Agent initialized for the first time or secretKey not found
// Generate and store secret key for agent while initialization
const secretKeyInfo = await generateSecretKey()
await agent.genericRecords.save({
content: {
secretKey: secretKeyInfo,
token,
},
})
} else {
const recordWithToken = genericRecord.find((record) => record?.content?.token !== undefined)
token = recordWithToken?.content.token as string
}

const app = await setupServer(
Expand All @@ -365,10 +350,10 @@ export async function runRestAgent(restConfig: AriesRestConfig) {
webhookUrl,
port: adminPort,
},
token
apiKey
)

logger.info(`*** API Token: ${token}`)
logger.info(`*** API Key: ${apiKey}`)

app.listen(adminPort, () => {
logger.info(`Successfully started server on port ${adminPort}`)
Expand Down
Loading