From 942556680c182152efa01ec9618928834c73904a Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Tue, 3 Jun 2025 13:54:32 +0300 Subject: [PATCH 01/33] routes and unit tests --- src/components/Auth/index.ts | 90 +++++++++++++++ src/components/database/AuthTokenDatabase.ts | 57 ++++++++++ src/components/database/DatabaseFactory.ts | 5 + src/components/database/TypesenseSchemas.ts | 12 +- src/components/database/index.ts | 3 + src/components/database/sqliteAuthToken.ts | 105 ++++++++++++++++++ src/components/httpRoutes/auth.ts | 48 ++++++++ src/components/httpRoutes/index.ts | 5 + .../httpRoutes/middleware/authMiddleware.ts | 42 +++++++ src/index.ts | 5 + src/test/unit/auth/token.test.ts | 92 +++++++++++++++ src/utils/blockchain.ts | 9 ++ 12 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 src/components/Auth/index.ts create mode 100644 src/components/database/AuthTokenDatabase.ts create mode 100644 src/components/database/sqliteAuthToken.ts create mode 100644 src/components/httpRoutes/auth.ts create mode 100644 src/components/httpRoutes/middleware/authMiddleware.ts create mode 100644 src/test/unit/auth/token.test.ts diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts new file mode 100644 index 000000000..4537f9664 --- /dev/null +++ b/src/components/Auth/index.ts @@ -0,0 +1,90 @@ +import { getMessageHash, verifyMessage } from "../../utils/index.js"; +import { AuthToken } from "../database/AuthTokenDatabase.js"; +import jwt from 'jsonwebtoken'; +import { Database } from "../database/index.js"; + +export interface CommonValidation { + valid: boolean; + error: string; +} + +export class Auth { + private db: Database + private jwtSecret: string + private signatureMessage: string + + public constructor(db: Database) { + this.db = db + this.jwtSecret = process.env.JWT_SECRET || 'ocean-node-secret' + this.signatureMessage = process.env.SIGNATURE_MESSAGE || 'token-auth' + } + + public getJwtSecret(): string { + return this.jwtSecret + } + + public getSignatureMessage(): string { + return this.signatureMessage + } + + async createToken(signature: string, address: string, validUntil: number | null = null): Promise { + const createdAt = Date.now() + const messageHashBytes = await getMessageHash(this.signatureMessage) + const isValid = await verifyMessage(messageHashBytes, address, signature) + if (!isValid) { + throw new Error('Invalid signature') + } + + const jwtToken = jwt.sign( + { + address, + createdAt + }, + this.jwtSecret + ) + + const token = await this.db.authToken.createToken(jwtToken, address, validUntil, createdAt) + return token + } + + async validateToken(token: string): Promise { + const tokenEntry = await this.db.authToken.validateToken(token) + if (!tokenEntry) { + return null + } + return tokenEntry + } + + async deleteToken(token: string): Promise { + await this.db.authToken.deleteToken(token) + } + + async validateAuthenticationOrToken( + address: string, + signature?: string, + token?: string, + message?: string + ): Promise { + try { + if (token) { + const authToken = await this.validateToken(token) + if (authToken && authToken.address.toLowerCase() === address.toLowerCase()) { + return { valid: true, error: '' } + } + } + + if (signature && message) { + const messageHashBytes = await getMessageHash(message) + const isValid = await verifyMessage(messageHashBytes, address, signature) + + if (isValid) { + return { valid: true, error: '' } + } + } + + return { valid: false, error: 'Invalid authentication' } + } catch (e) { + return { valid: false, error: `Error during authentication validation: ${e}` } + } + } +} \ No newline at end of file diff --git a/src/components/database/AuthTokenDatabase.ts b/src/components/database/AuthTokenDatabase.ts new file mode 100644 index 000000000..3c39d9974 --- /dev/null +++ b/src/components/database/AuthTokenDatabase.ts @@ -0,0 +1,57 @@ +import { DATABASE_LOGGER } from '../../utils/logging/common.js' +import { AbstractDatabase } from './BaseDatabase.js' +import { OceanNodeDBConfig } from '../../@types/OceanNode.js' +import { TypesenseSchema } from './TypesenseSchemas.js' +import path from 'path' +import * as fs from 'fs' +import { SQLiteAuthToken } from './sqliteAuthToken.js' + +export interface AuthToken { + token: string + address: string + created: Date + validUntil: Date | null + isValid: boolean +} + +export class AuthTokenDatabase extends AbstractDatabase { + private provider: SQLiteAuthToken + + constructor(config: OceanNodeDBConfig, schema: TypesenseSchema) { + super(config, schema) + return (async (): Promise => { + DATABASE_LOGGER.info('Creating AuthTokenDatabase with SQLite') + + const dbDir = path.dirname('databases/authTokenDatabase.sqlite') + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }) + } + this.provider = new SQLiteAuthToken('databases/authTokenDatabase.sqlite') + await this.provider.createTable() + return this + })() as unknown as AuthTokenDatabase + } + + async createToken( + token: string, + address: string, + validUntil: number | null = null, + createdAt: number + ): Promise { + await this.provider.createToken(token, address, createdAt, validUntil) + return token + } + + async validateToken(token: string): Promise { + const tokenEntry = await this.provider.validateTokenEntry(token) + if (!tokenEntry) { + return null + } + + return tokenEntry + } + + async deleteToken(token: string): Promise { + await this.provider.deleteTokenEntry(token) + } +} diff --git a/src/components/database/DatabaseFactory.ts b/src/components/database/DatabaseFactory.ts index 628aeb604..989b76598 100644 --- a/src/components/database/DatabaseFactory.ts +++ b/src/components/database/DatabaseFactory.ts @@ -32,6 +32,7 @@ import { DB_TYPES } from '../../utils/index.js' import { C2DDatabase } from './C2DDatabase.js' import { SQLLiteNonceDatabase } from './SQLLiteNonceDatabase.js' import { SQLLiteConfigDatabase } from './SQLLiteConfigDatabase.js' +import { AuthTokenDatabase } from './AuthTokenDatabase.js' export class DatabaseFactory { private static databaseMap = { @@ -91,6 +92,10 @@ export class DatabaseFactory { return await new C2DDatabase(config, typesenseSchemas.c2dSchemas) } + static async createAuthTokenDatabase(config: OceanNodeDBConfig): Promise { + return await new AuthTokenDatabase(config, typesenseSchemas.authTokenSchemas) + } + static createIndexerDatabase( config: OceanNodeDBConfig ): Promise { diff --git a/src/components/database/TypesenseSchemas.ts b/src/components/database/TypesenseSchemas.ts index 0cdf7ea5e..56cfbc38f 100644 --- a/src/components/database/TypesenseSchemas.ts +++ b/src/components/database/TypesenseSchemas.ts @@ -52,7 +52,8 @@ export type TypesenseSchemas = { indexerSchemas: TypesenseSchema logSchemas: TypesenseSchema orderSchema: TypesenseSchema - ddoStateSchema: TypesenseSchema + ddoStateSchema: TypesenseSchema, + authTokenSchemas: TypesenseSchema } const ddoSchemas = readJsonSchemas() export const typesenseSchemas: TypesenseSchemas = { @@ -126,5 +127,14 @@ export const typesenseSchemas: TypesenseSchemas = { { name: 'valid', type: 'bool' }, { name: 'error', type: 'string' } ] + }, + authTokenSchemas: { + name: 'authTokens', + enable_nested_fields: true, + fields: [ + { name: 'token', type: 'string' }, + { name: 'address', type: 'string' }, + { name: 'createdAt', type: 'int64' }, + ] } } diff --git a/src/components/database/index.ts b/src/components/database/index.ts index 863d3cad3..25156004e 100644 --- a/src/components/database/index.ts +++ b/src/components/database/index.ts @@ -18,6 +18,7 @@ import { ElasticsearchSchema } from './ElasticSchemas.js' import { SQLLiteConfigDatabase } from './SQLLiteConfigDatabase.js' import { SQLLiteNonceDatabase } from './SQLLiteNonceDatabase.js' import { TypesenseSchema } from './TypesenseSchemas.js' +import { AuthTokenDatabase } from './AuthTokenDatabase.js' export type Schema = ElasticsearchSchema | TypesenseSchema @@ -30,6 +31,7 @@ export class Database { ddoState: AbstractDdoStateDatabase sqliteConfig: SQLLiteConfigDatabase c2d: C2DDatabase + authToken: AuthTokenDatabase constructor(private config: OceanNodeDBConfig) { return (async (): Promise => { @@ -55,6 +57,7 @@ export class Database { this.logs = await DatabaseFactory.createLogDatabase(this.config) this.order = await DatabaseFactory.createOrderDatabase(this.config) this.ddoState = await DatabaseFactory.createDdoStateDatabase(this.config) + this.authToken = await DatabaseFactory.createAuthTokenDatabase(this.config) } else { DATABASE_LOGGER.info( 'Invalid DB URL. Only Nonce and C2D Databases are initialized. Other databases are not available.' diff --git a/src/components/database/sqliteAuthToken.ts b/src/components/database/sqliteAuthToken.ts new file mode 100644 index 000000000..afae57e68 --- /dev/null +++ b/src/components/database/sqliteAuthToken.ts @@ -0,0 +1,105 @@ +import { AuthToken } from './AuthTokenDatabase.js' +import sqlite3 from 'sqlite3' +import { DATABASE_LOGGER } from '../../utils/logging/common.js' + +interface AuthTokenDatabaseProvider { + createToken( + token: string, + address: string, + createdAt: number, + validUntil: number | null + ): Promise + validateTokenEntry(token: string): Promise + deleteTokenEntry(token: string): Promise +} + +export class SQLiteAuthToken implements AuthTokenDatabaseProvider { + private db: sqlite3.Database + + constructor(dbFilePath: string) { + this.db = new sqlite3.Database(dbFilePath) + } + + async createTable(): Promise { + await this.db.exec(` + CREATE TABLE IF NOT EXISTS authTokens ( + token TEXT PRIMARY KEY, + address TEXT NOT NULL, + createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, + validUntil DATETIME + ) + `) + } + + async createToken( + token: string, + address: string, + createdAt: number, + validUntil: number | null = null + ): Promise { + const insertSQL = ` + INSERT INTO authTokens (token, address, createdAt, validUntil) VALUES (?, ?, ?, ?) + ` + return new Promise((resolve, reject) => { + this.db.run(insertSQL, [token, address, createdAt, validUntil], (err) => { + if (err) { + DATABASE_LOGGER.error(`Error creating auth token: ${err}`) + reject(err) + } else { + resolve() + } + }) + }) + } + + async validateTokenEntry(token: string): Promise { + const selectSQL = ` + SELECT * FROM authTokens WHERE token = ? + ` + return new Promise((resolve, reject) => { + this.db.get(selectSQL, [token], (err, row: AuthToken) => { + if (err) { + DATABASE_LOGGER.error(`Error validating auth token: ${err}`) + reject(err) + return + } + + if (!row) { + resolve(null) + return + } + + if (row.validUntil === null) { + resolve(row) + return + } + + const validUntilDate = new Date(row.validUntil).getTime() + const now = Date.now() + + if (validUntilDate < now) { + resolve(null) + return + } + + resolve(row) + }) + }) + } + + async deleteTokenEntry(token: string): Promise { + const deleteSQL = ` + DELETE FROM authTokens WHERE token = ? + ` + return new Promise((resolve, reject) => { + this.db.run(deleteSQL, [token], (err) => { + if (err) { + DATABASE_LOGGER.error(`Error deleting auth token: ${err}`) + reject(err) + } else { + resolve() + } + }) + }) + } +} diff --git a/src/components/httpRoutes/auth.ts b/src/components/httpRoutes/auth.ts new file mode 100644 index 000000000..a57ec0ca0 --- /dev/null +++ b/src/components/httpRoutes/auth.ts @@ -0,0 +1,48 @@ +import express from 'express' +import { HTTP_LOGGER } from '../../utils/logging/common.js' +import { OceanNode } from '../../OceanNode.js' +import { Auth } from '../Auth/index.js' + +export const authRoutes = express.Router() +const oceanNode = OceanNode.getInstance() +const auth = new Auth(oceanNode.getDatabase()) + +authRoutes.post('/api/v1/auth/token', async (req, res) => { + try { + const { signature, address, validUntil } = req.body + + if (!signature || !address) { + return res.status(400).json({ error: 'Missing required parameters' }) + } + + const token = await auth.createToken(signature, address, validUntil) + + res.json({ token }) + } catch (error) { + HTTP_LOGGER.error(`Error creating auth token: ${error}`) + res.status(500).json({ error: 'Internal server error' }) + } +}) + +authRoutes.delete('/api/v1/auth/token', async (req, res) => { + try { + const { signature, address, token } = req.body + + if (!signature || !address || !token) { + return res.status(400).json({ error: 'Missing required parameters' }) + } + + const tokenEntry = await auth.validateToken(token) + if (!tokenEntry) { + return res.status(401).json({ error: 'Invalid token' }) + } + + await auth.deleteToken(token) + + res.json({ success: true }) + } catch (error) { + HTTP_LOGGER.error(`Error deleting auth token: ${error}`) + res.status(500).json({ success: false, error: 'Internal server error' }) + } +}) + diff --git a/src/components/httpRoutes/index.ts b/src/components/httpRoutes/index.ts index d93e460f2..a40da0466 100644 --- a/src/components/httpRoutes/index.ts +++ b/src/components/httpRoutes/index.ts @@ -13,8 +13,10 @@ import { queueRoutes } from './queue.js' import { jobsRoutes } from './jobs.js' import { addMapping, allRoutesMapping, findPathName } from './routeUtils.js' import { PolicyServerPassthroughRoute } from './policyServer.js' +import { authRoutes } from './auth.js' export * from './getOceanPeers.js' +export * from './auth.js' export const httpRoutes = express.Router() @@ -56,6 +58,9 @@ httpRoutes.use(queueRoutes) httpRoutes.use(jobsRoutes) // policy server passthrough httpRoutes.use(PolicyServerPassthroughRoute) +// auth routes +httpRoutes.use(authRoutes) + export function getAllServiceEndpoints() { httpRoutes.stack.forEach(addMapping.bind(null, [])) const data: any = {} diff --git a/src/components/httpRoutes/middleware/authMiddleware.ts b/src/components/httpRoutes/middleware/authMiddleware.ts new file mode 100644 index 000000000..28e60eaf6 --- /dev/null +++ b/src/components/httpRoutes/middleware/authMiddleware.ts @@ -0,0 +1,42 @@ +import { Request, Response, NextFunction } from 'express' +import { HTTP_LOGGER } from '../../../utils/logging/common.js' +import { OceanNode } from '../../../OceanNode.js' + +const oceanNode = OceanNode.getInstance() + +export interface AuthenticatedRequest extends Request { + authenticatedAddress?: string +} + +export async function validateAuthToken( + req: AuthenticatedRequest, + res: Response, + next: NextFunction +) { + const authHeader = req.headers.authorization + + if (!authHeader) { + // If no auth header is present, check for signature in the request + return next() + } + + const [scheme, token] = authHeader.split(' ') + + if (scheme !== 'Bearer' || !token) { + return res.status(401).json({ error: 'Invalid authorization header format' }) + } + + try { + const tokenEntry = await oceanNode.getDatabase().authToken.validateToken(token) + + if (!tokenEntry) { + return res.status(401).json({ error: 'Invalid or expired token' }) + } + + req.authenticatedAddress = tokenEntry.address + next() + } catch (error) { + HTTP_LOGGER.error(`Error validating auth token: ${error}`) + res.status(500).json({ error: 'Internal server error' }) + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 603abedbd..a42641b62 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,7 @@ import cors from 'cors' import { scheduleCronJobs } from './utils/cronjobs/scheduleCronJobs.js' import { requestValidator } from './components/httpRoutes/requestValidator.js' import { hasValidDBConfiguration } from './utils/database.js' +import { authRoutes } from './components/httpRoutes/auth.js' const app: Express = express() @@ -169,6 +170,10 @@ if (config.hasHttp) { next() }) + + // Add auth routes before the main routes + app.use(authRoutes) + // Integrate static file serving middleware app.use(removeExtraSlashes) app.use('/', httpRoutes) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts new file mode 100644 index 000000000..39690bed6 --- /dev/null +++ b/src/test/unit/auth/token.test.ts @@ -0,0 +1,92 @@ +import { OceanNodeConfig } from '../../../@types/OceanNode.js' +import { getConfiguration, getMessageHash } from '../../../utils/index.js' +import { expect } from 'chai' +import { Database } from '../../../components/database/index.js' +import { Wallet } from 'ethers' +import { Auth } from '../../../components/Auth/index.js' + +describe('Auth Token Tests', () => { + let wallet: Wallet + let mockDatabase: Database + let config: OceanNodeConfig + + before(async () => { + config = await getConfiguration(true) + mockDatabase = await new Database(config.dbConfig) + wallet = new Wallet(process.env.PRIVATE_KEY) + }) + + const createToken = async (auth: Auth, address: string, validUntil: number) => { + const msg = auth.getSignatureMessage() + const messageHash = await getMessageHash(msg); + const signature = await wallet.signMessage(messageHash); + const token = await auth.createToken(signature, address, validUntil); + return token; + } + + it('should create and validate a token', async () => { + const auth = new Auth(mockDatabase) + const token = await createToken(auth, wallet.address, null) + expect(token).to.be.a('string') + + const validationResult = await auth.validateToken(token) + expect(validationResult).to.not.be.null + expect(validationResult?.address).to.equal(wallet.address) + }) + + + it('should validate authentication with token', async () => { + const auth = new Auth(mockDatabase) + const token = await createToken(auth, wallet.address, null) + const result = await auth.validateAuthenticationOrToken(wallet.address, undefined, token) + expect(result.valid).to.be.true + }) + + it('should validate authentication with signature', async () => { + const auth = new Auth(mockDatabase) + const message = auth.getSignatureMessage() + const messageHash = await getMessageHash(message) + const signature = await wallet.signMessage(messageHash) + + const result = await auth.validateAuthenticationOrToken( + wallet.address, + signature, + undefined, + message + ) + expect(result.valid).to.be.true + }) + + it('should fail validation with invalid token', async () => { + const auth = new Auth(mockDatabase) + const result = await auth.validateAuthenticationOrToken( + wallet.address, + undefined, + 'invalid-token' + ) + expect(result.valid).to.be.false + }) + + it('should fail validation with invalid signature', async () => { + const auth = new Auth(mockDatabase) + const message = 'Test message' + const invalidSignature = '0x' + '0'.repeat(130) + + const result = await auth.validateAuthenticationOrToken( + wallet.address, + invalidSignature, + undefined, + message + ) + expect(result.valid).to.be.false + }) + + it('should respect token expiry', async () => { + const auth = new Auth(mockDatabase) + const validUntil = new Date(Date.now() - 1000) // 1 second ago + const token = await createToken(auth, wallet.address, validUntil.getTime()) + + const validationResult = await auth.validateToken(token) + expect(validationResult).to.be.null + }) +}) \ No newline at end of file diff --git a/src/utils/blockchain.ts b/src/utils/blockchain.ts index 3a175d04e..593c52dee 100644 --- a/src/utils/blockchain.ts +++ b/src/utils/blockchain.ts @@ -270,6 +270,15 @@ export async function verifyMessage( } } +export async function getMessageHash(message: string): Promise { + const messageHash = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(messageHash) + return messageHashBytes +} + export async function checkSupportedChainId(chainId: number): Promise { const config = await getConfiguration() if (!chainId || !(`${chainId.toString()}` in config.supportedNetworks)) { From fec199644b64777d20a4b9f1f62e969780e55984 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 09:51:36 +0300 Subject: [PATCH 02/33] unit and integration tests --- src/OceanNode.ts | 7 + src/components/Auth/index.ts | 148 +++++------ src/components/database/AuthTokenDatabase.ts | 32 +-- src/components/database/DatabaseFactory.ts | 6 +- src/components/database/TypesenseSchemas.ts | 4 +- src/components/database/index.ts | 7 +- src/components/database/sqliteAuthToken.ts | 24 +- src/components/httpRoutes/aquarius.ts | 73 +++--- src/components/httpRoutes/auth.ts | 69 +++--- .../httpRoutes/middleware/authMiddleware.ts | 53 ++-- src/test/integration/auth.test.ts | 233 ++++++++++++++++++ src/test/unit/auth/token.test.ts | 141 +++++------ src/utils/blockchain.ts | 2 +- 13 files changed, 536 insertions(+), 263 deletions(-) create mode 100644 src/test/integration/auth.test.ts diff --git a/src/OceanNode.ts b/src/OceanNode.ts index 762bf5d44..21727a0f1 100644 --- a/src/OceanNode.ts +++ b/src/OceanNode.ts @@ -12,6 +12,7 @@ import { pipe } from 'it-pipe' import { GENERIC_EMOJIS, LOG_LEVELS_STR } from './utils/logging/Logger.js' import { BaseHandler } from './components/core/handler/handler.js' import { C2DEngines } from './components/c2d/compute_engines.js' +import { Auth } from './components/Auth/index.js' export interface RequestLimiter { requester: string | string[] // IP address or peer ID @@ -35,6 +36,7 @@ export class OceanNode { // requester private remoteCaller: string | string[] private requestMap: Map + private auth: Auth // eslint-disable-next-line no-useless-constructor private constructor( @@ -47,6 +49,7 @@ export class OceanNode { this.coreHandlers = CoreHandlersRegistry.getInstance(this) this.requestMap = new Map() this.config = config + this.auth = new Auth(this.db.authToken) if (node) { node.setCoreHandlers(this.coreHandlers) } @@ -136,6 +139,10 @@ export class OceanNode { return this.requestMap } + public getAuth(): Auth { + return this.auth + } + /** * Use this method to direct calls to the node as node cannot dial into itself * @param message command message diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index 4537f9664..c2f55f2e3 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -1,90 +1,98 @@ -import { getMessageHash, verifyMessage } from "../../utils/index.js"; -import { AuthToken } from "../database/AuthTokenDatabase.js"; -import jwt from 'jsonwebtoken'; -import { Database } from "../database/index.js"; +import { getMessageHash, verifyMessage } from '../../utils/index.js' +import { AuthToken, AuthTokenDatabase } from '../database/AuthTokenDatabase.js' +import jwt from 'jsonwebtoken' export interface CommonValidation { - valid: boolean; - error: string; + valid: boolean + error: string } export class Auth { - private db: Database - private jwtSecret: string - private signatureMessage: string + private authTokenDatabase: AuthTokenDatabase + private jwtSecret: string + private signatureMessage: string - public constructor(db: Database) { - this.db = db - this.jwtSecret = process.env.JWT_SECRET || 'ocean-node-secret' - this.signatureMessage = process.env.SIGNATURE_MESSAGE || 'token-auth' - } + public constructor(authTokenDatabase: AuthTokenDatabase) { + this.authTokenDatabase = authTokenDatabase + this.jwtSecret = process.env.JWT_SECRET || 'ocean-node-secret' + this.signatureMessage = process.env.SIGNATURE_MESSAGE || 'token-auth' + } - public getJwtSecret(): string { - return this.jwtSecret - } + public getJwtSecret(): string { + return this.jwtSecret + } - public getSignatureMessage(): string { - return this.signatureMessage - } + public getSignatureMessage(): string { + return this.signatureMessage + } - async createToken(signature: string, address: string, validUntil: number | null = null): Promise { - const createdAt = Date.now() - const messageHashBytes = await getMessageHash(this.signatureMessage) - const isValid = await verifyMessage(messageHashBytes, address, signature) - if (!isValid) { - throw new Error('Invalid signature') - } + async validateSignature(signature: string, address: string): Promise { + const messageHashBytes = getMessageHash(this.signatureMessage) + const isValid = await verifyMessage(messageHashBytes, address, signature) + return isValid + } - const jwtToken = jwt.sign( - { - address, - createdAt - }, - this.jwtSecret - ) + async createToken( + address: string, + validUntil: number | null = null + ): Promise { + const createdAt = Date.now() - const token = await this.db.authToken.createToken(jwtToken, address, validUntil, createdAt) - return token - } + const jwtToken = jwt.sign( + { + address, + createdAt + }, + this.jwtSecret + ) - async validateToken(token: string): Promise { - const tokenEntry = await this.db.authToken.validateToken(token) - if (!tokenEntry) { - return null - } - return tokenEntry - } + const token = await this.authTokenDatabase.createToken( + jwtToken, + address, + validUntil, + createdAt + ) + return token + } - async deleteToken(token: string): Promise { - await this.db.authToken.deleteToken(token) + async validateToken(token: string): Promise { + const tokenEntry = await this.authTokenDatabase.validateToken(token) + if (!tokenEntry) { + return null } + return tokenEntry + } - async validateAuthenticationOrToken( - address: string, - signature?: string, - token?: string, - message?: string - ): Promise { - try { - if (token) { - const authToken = await this.validateToken(token) - if (authToken && authToken.address.toLowerCase() === address.toLowerCase()) { - return { valid: true, error: '' } - } - } + async invalidateToken(token: string): Promise { + await this.authTokenDatabase.invalidateToken(token) + } - if (signature && message) { - const messageHashBytes = await getMessageHash(message) - const isValid = await verifyMessage(messageHashBytes, address, signature) + async validateAuthenticationOrToken( + address: string, + signature?: string, + token?: string, + message?: string + ): Promise { + try { + if (token) { + const authToken = await this.validateToken(token) + if (authToken && authToken.address.toLowerCase() === address.toLowerCase()) { + return { valid: true, error: '' } + } + } - if (isValid) { - return { valid: true, error: '' } - } - } + if (signature && message) { + const messageHashBytes = getMessageHash(message) + const isValid = await verifyMessage(messageHashBytes, address, signature) - return { valid: false, error: 'Invalid authentication' } - } catch (e) { - return { valid: false, error: `Error during authentication validation: ${e}` } + if (isValid) { + return { valid: true, error: '' } } + } + + return { valid: false, error: 'Invalid authentication' } + } catch (e) { + return { valid: false, error: `Error during authentication validation: ${e}` } } -} \ No newline at end of file + } +} diff --git a/src/components/database/AuthTokenDatabase.ts b/src/components/database/AuthTokenDatabase.ts index 3c39d9974..159506124 100644 --- a/src/components/database/AuthTokenDatabase.ts +++ b/src/components/database/AuthTokenDatabase.ts @@ -1,7 +1,6 @@ import { DATABASE_LOGGER } from '../../utils/logging/common.js' import { AbstractDatabase } from './BaseDatabase.js' import { OceanNodeDBConfig } from '../../@types/OceanNode.js' -import { TypesenseSchema } from './TypesenseSchemas.js' import path from 'path' import * as fs from 'fs' import { SQLiteAuthToken } from './sqliteAuthToken.js' @@ -17,19 +16,20 @@ export interface AuthToken { export class AuthTokenDatabase extends AbstractDatabase { private provider: SQLiteAuthToken - constructor(config: OceanNodeDBConfig, schema: TypesenseSchema) { - super(config, schema) - return (async (): Promise => { - DATABASE_LOGGER.info('Creating AuthTokenDatabase with SQLite') - - const dbDir = path.dirname('databases/authTokenDatabase.sqlite') - if (!fs.existsSync(dbDir)) { - fs.mkdirSync(dbDir, { recursive: true }) - } - this.provider = new SQLiteAuthToken('databases/authTokenDatabase.sqlite') - await this.provider.createTable() - return this - })() as unknown as AuthTokenDatabase + private constructor(config: OceanNodeDBConfig, provider?: SQLiteAuthToken) { + super(config) + this.provider = provider + } + + static async create(config: OceanNodeDBConfig): Promise { + DATABASE_LOGGER.info('Creating AuthTokenDatabase with SQLite') + const dbDir = path.dirname('databases/authTokenDatabase.sqlite') + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }) + } + const provider = new SQLiteAuthToken('databases/authTokenDatabase.sqlite') + await provider.createTable() + return new AuthTokenDatabase(config, provider) } async createToken( @@ -51,7 +51,7 @@ export class AuthTokenDatabase extends AbstractDatabase { return tokenEntry } - async deleteToken(token: string): Promise { - await this.provider.deleteTokenEntry(token) + async invalidateToken(token: string): Promise { + await this.provider.invalidateTokenEntry(token) } } diff --git a/src/components/database/DatabaseFactory.ts b/src/components/database/DatabaseFactory.ts index 989b76598..e83549f9d 100644 --- a/src/components/database/DatabaseFactory.ts +++ b/src/components/database/DatabaseFactory.ts @@ -92,8 +92,10 @@ export class DatabaseFactory { return await new C2DDatabase(config, typesenseSchemas.c2dSchemas) } - static async createAuthTokenDatabase(config: OceanNodeDBConfig): Promise { - return await new AuthTokenDatabase(config, typesenseSchemas.authTokenSchemas) + static async createAuthTokenDatabase( + config: OceanNodeDBConfig + ): Promise { + return await AuthTokenDatabase.create(config) } static createIndexerDatabase( diff --git a/src/components/database/TypesenseSchemas.ts b/src/components/database/TypesenseSchemas.ts index 56cfbc38f..a5b25c48a 100644 --- a/src/components/database/TypesenseSchemas.ts +++ b/src/components/database/TypesenseSchemas.ts @@ -52,7 +52,7 @@ export type TypesenseSchemas = { indexerSchemas: TypesenseSchema logSchemas: TypesenseSchema orderSchema: TypesenseSchema - ddoStateSchema: TypesenseSchema, + ddoStateSchema: TypesenseSchema authTokenSchemas: TypesenseSchema } const ddoSchemas = readJsonSchemas() @@ -134,7 +134,7 @@ export const typesenseSchemas: TypesenseSchemas = { fields: [ { name: 'token', type: 'string' }, { name: 'address', type: 'string' }, - { name: 'createdAt', type: 'int64' }, + { name: 'createdAt', type: 'int64' } ] } } diff --git a/src/components/database/index.ts b/src/components/database/index.ts index 25156004e..9a01f22ef 100644 --- a/src/components/database/index.ts +++ b/src/components/database/index.ts @@ -36,10 +36,12 @@ export class Database { constructor(private config: OceanNodeDBConfig) { return (async (): Promise => { try { - // these 2 are using SQL Lite provider + // these databases use SQLite provider this.nonce = await DatabaseFactory.createNonceDatabase(this.config) this.sqliteConfig = await DatabaseFactory.createConfigDatabase() this.c2d = await DatabaseFactory.createC2DDatabase(this.config) + this.authToken = await DatabaseFactory.createAuthTokenDatabase(this.config) + // only for Typesense or Elasticsearch if (hasValidDBConfiguration(this.config)) { // add this DB transport too @@ -57,10 +59,9 @@ export class Database { this.logs = await DatabaseFactory.createLogDatabase(this.config) this.order = await DatabaseFactory.createOrderDatabase(this.config) this.ddoState = await DatabaseFactory.createDdoStateDatabase(this.config) - this.authToken = await DatabaseFactory.createAuthTokenDatabase(this.config) } else { DATABASE_LOGGER.info( - 'Invalid DB URL. Only Nonce and C2D Databases are initialized. Other databases are not available.' + 'Invalid DB URL. Only Nonce, C2D, Auth Token and Config Databases are initialized. Other databases are not available.' ) } return this diff --git a/src/components/database/sqliteAuthToken.ts b/src/components/database/sqliteAuthToken.ts index afae57e68..ac526dbe3 100644 --- a/src/components/database/sqliteAuthToken.ts +++ b/src/components/database/sqliteAuthToken.ts @@ -10,7 +10,7 @@ interface AuthTokenDatabaseProvider { validUntil: number | null ): Promise validateTokenEntry(token: string): Promise - deleteTokenEntry(token: string): Promise + invalidateTokenEntry(token: string): Promise } export class SQLiteAuthToken implements AuthTokenDatabaseProvider { @@ -26,12 +26,13 @@ export class SQLiteAuthToken implements AuthTokenDatabaseProvider { token TEXT PRIMARY KEY, address TEXT NOT NULL, createdAt DATETIME DEFAULT CURRENT_TIMESTAMP, - validUntil DATETIME + validUntil DATETIME, + isValid BOOLEAN DEFAULT TRUE ) `) } - async createToken( + createToken( token: string, address: string, createdAt: number, @@ -52,12 +53,12 @@ export class SQLiteAuthToken implements AuthTokenDatabaseProvider { }) } - async validateTokenEntry(token: string): Promise { + validateTokenEntry(token: string): Promise { const selectSQL = ` SELECT * FROM authTokens WHERE token = ? ` return new Promise((resolve, reject) => { - this.db.get(selectSQL, [token], (err, row: AuthToken) => { + this.db.get(selectSQL, [token], async (err, row: AuthToken) => { if (err) { DATABASE_LOGGER.error(`Error validating auth token: ${err}`) reject(err) @@ -69,6 +70,11 @@ export class SQLiteAuthToken implements AuthTokenDatabaseProvider { return } + if (!row.isValid) { + resolve(null) + return + } + if (row.validUntil === null) { resolve(row) return @@ -79,6 +85,8 @@ export class SQLiteAuthToken implements AuthTokenDatabaseProvider { if (validUntilDate < now) { resolve(null) + DATABASE_LOGGER.info(`Auth token ${token} is invalid`) + await this.invalidateTokenEntry(token) return } @@ -87,14 +95,14 @@ export class SQLiteAuthToken implements AuthTokenDatabaseProvider { }) } - async deleteTokenEntry(token: string): Promise { + invalidateTokenEntry(token: string): Promise { const deleteSQL = ` - DELETE FROM authTokens WHERE token = ? + UPDATE authTokens SET isValid = FALSE WHERE token = ? ` return new Promise((resolve, reject) => { this.db.run(deleteSQL, [token], (err) => { if (err) { - DATABASE_LOGGER.error(`Error deleting auth token: ${err}`) + DATABASE_LOGGER.error(`Error invalidating auth token: ${err}`) reject(err) } else { resolve() diff --git a/src/components/httpRoutes/aquarius.ts b/src/components/httpRoutes/aquarius.ts index e95171373..37873bf9a 100644 --- a/src/components/httpRoutes/aquarius.ts +++ b/src/components/httpRoutes/aquarius.ts @@ -10,6 +10,7 @@ import { QueryCommand } from '../../@types/commands.js' import { DatabaseFactory } from '../database/DatabaseFactory.js' import { SearchQuery } from '../../@types/DDO/SearchQuery.js' import { getConfiguration } from '../../utils/index.js' +import { validateAuthToken } from './middleware/authMiddleware.js' export const aquariusRoutes = express.Router() @@ -132,41 +133,51 @@ aquariusRoutes.get(`${AQUARIUS_API_BASE_PATH}/state/ddo`, async (req, res) => { } }) -aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req, res) => { - const node = req.oceanNode - try { - if (!req.body) { - res.status(400).send('Missing DDO object') - return - } +aquariusRoutes.post( + `${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, + validateAuthToken, + async (req, res) => { + const node = req.oceanNode + try { + console.log({ reqDdo: req.body }) + if (!req.body) { + res.status(400).send('Missing DDO object') + return + } - const requestBody = JSON.parse(req.body) - const { publisherAddress, nonce, signature } = requestBody + const requestBody = JSON.parse(req.body) + console.log({ requestBody }) + const { publisherAddress, nonce, signature } = requestBody + console.log({ publisherAddress, nonce, signature }) - // This is for backward compatibility with the old way of sending the DDO - const ddo = requestBody.ddo || JSON.parse(req.body) + // This is for backward compatibility with the old way of sending the DDO + const ddo = requestBody.ddo || JSON.parse(req.body) + console.log({ ddo }) - if (!ddo.version) { - res.status(400).send('Missing DDO version') - return - } + if (!ddo.version) { + res.status(400).send('Missing DDO version') + return + } - const result = await new ValidateDDOHandler(node).handle({ - ddo, - publisherAddress, - nonce, - signature, - command: PROTOCOL_COMMANDS.VALIDATE_DDO - }) + const result = await new ValidateDDOHandler(node).handle({ + ddo, + publisherAddress, + nonce, + signature, + command: PROTOCOL_COMMANDS.VALIDATE_DDO + }) - if (result.stream) { - const validationResult = JSON.parse(await streamToString(result.stream as Readable)) - res.json(validationResult) - } else { - res.status(result.status.httpStatus).send(result.status.error) + if (result.stream) { + const validationResult = JSON.parse( + await streamToString(result.stream as Readable) + ) + res.json(validationResult) + } else { + res.status(result.status.httpStatus).send(result.status.error) + } + } catch (error) { + HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) + res.status(500).send('Internal Server Error') } - } catch (error) { - HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) - res.status(500).send('Internal Server Error') } -}) +) diff --git a/src/components/httpRoutes/auth.ts b/src/components/httpRoutes/auth.ts index a57ec0ca0..c5cc02eaa 100644 --- a/src/components/httpRoutes/auth.ts +++ b/src/components/httpRoutes/auth.ts @@ -1,48 +1,59 @@ import express from 'express' import { HTTP_LOGGER } from '../../utils/logging/common.js' -import { OceanNode } from '../../OceanNode.js' -import { Auth } from '../Auth/index.js' +import { SERVICES_API_BASE_PATH } from '../../utils/index.js' export const authRoutes = express.Router() -const oceanNode = OceanNode.getInstance() -const auth = new Auth(oceanNode.getDatabase()) -authRoutes.post('/api/v1/auth/token', async (req, res) => { +authRoutes.post( + `${SERVICES_API_BASE_PATH}/auth/token`, + express.json(), + async (req, res) => { try { - const { signature, address, validUntil } = req.body + const { signature, address, validUntil } = req.body - if (!signature || !address) { - return res.status(400).json({ error: 'Missing required parameters' }) - } + console.log({ signature, address, validUntil }) - const token = await auth.createToken(signature, address, validUntil) + if (!signature || !address) { + return res.status(400).json({ error: 'Missing required parameters' }) + } - res.json({ token }) + const isValid = await req.oceanNode.getAuth().validateSignature(signature, address) + if (!isValid) { + return res.status(400).json({ error: 'Invalid signature' }) + } + + const token = await req.oceanNode.getAuth().createToken(address, validUntil) + + res.json({ token }) } catch (error) { - HTTP_LOGGER.error(`Error creating auth token: ${error}`) - res.status(500).json({ error: 'Internal server error' }) + HTTP_LOGGER.error(`Error creating auth token: ${error}`) + res.status(500).json({ error: 'Internal server error' }) } -}) + } +) -authRoutes.delete('/api/v1/auth/token', async (req, res) => { +authRoutes.post( + `${SERVICES_API_BASE_PATH}/auth/token/invalidate`, + express.json(), + async (req, res) => { try { - const { signature, address, token } = req.body + const { signature, address, token } = req.body - if (!signature || !address || !token) { - return res.status(400).json({ error: 'Missing required parameters' }) - } + if (!signature || !address || !token) { + return res.status(400).json({ error: 'Missing required parameters' }) + } - const tokenEntry = await auth.validateToken(token) - if (!tokenEntry) { - return res.status(401).json({ error: 'Invalid token' }) - } + const isValid = await req.oceanNode.getAuth().validateSignature(signature, address) + if (!isValid) { + return res.status(400).json({ error: 'Invalid signature' }) + } - await auth.deleteToken(token) + await req.oceanNode.getAuth().invalidateToken(token) - res.json({ success: true }) + res.json({ success: true }) } catch (error) { - HTTP_LOGGER.error(`Error deleting auth token: ${error}`) - res.status(500).json({ success: false, error: 'Internal server error' }) + HTTP_LOGGER.error(`Error deleting auth token: ${error}`) + res.status(500).json({ success: false, error: 'Internal server error' }) } -}) - + } +) diff --git a/src/components/httpRoutes/middleware/authMiddleware.ts b/src/components/httpRoutes/middleware/authMiddleware.ts index 28e60eaf6..7e6f711e0 100644 --- a/src/components/httpRoutes/middleware/authMiddleware.ts +++ b/src/components/httpRoutes/middleware/authMiddleware.ts @@ -1,42 +1,39 @@ import { Request, Response, NextFunction } from 'express' import { HTTP_LOGGER } from '../../../utils/logging/common.js' -import { OceanNode } from '../../../OceanNode.js' - -const oceanNode = OceanNode.getInstance() export interface AuthenticatedRequest extends Request { - authenticatedAddress?: string + authenticatedAddress?: string } export async function validateAuthToken( - req: AuthenticatedRequest, - res: Response, - next: NextFunction + req: AuthenticatedRequest, + res: Response, + next: NextFunction ) { - const authHeader = req.headers.authorization - - if (!authHeader) { - // If no auth header is present, check for signature in the request - return next() - } + const authHeader = req.headers.authorization - const [scheme, token] = authHeader.split(' ') + if (!authHeader) { + // If no auth header is present, check for signature in the request + return next() + } - if (scheme !== 'Bearer' || !token) { - return res.status(401).json({ error: 'Invalid authorization header format' }) - } + const [scheme, token] = authHeader.split(' ') - try { - const tokenEntry = await oceanNode.getDatabase().authToken.validateToken(token) + if (scheme !== 'Bearer' || !token) { + return res.status(401).json({ error: 'Invalid authorization header format' }) + } - if (!tokenEntry) { - return res.status(401).json({ error: 'Invalid or expired token' }) - } + try { + const tokenEntry = await req.oceanNode.getAuth().validateToken(token) - req.authenticatedAddress = tokenEntry.address - next() - } catch (error) { - HTTP_LOGGER.error(`Error validating auth token: ${error}`) - res.status(500).json({ error: 'Internal server error' }) + if (!tokenEntry) { + return res.status(401).json({ error: 'Invalid or expired token' }) } -} \ No newline at end of file + + req.authenticatedAddress = tokenEntry.address + next() + } catch (error) { + HTTP_LOGGER.error(`Error validating auth token: ${error}`) + res.status(500).json({ error: 'Internal server error' }) + } +} diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts new file mode 100644 index 000000000..fdf2b490e --- /dev/null +++ b/src/test/integration/auth.test.ts @@ -0,0 +1,233 @@ +import { expect } from 'chai' +import { JsonRpcProvider, Signer, Wallet } from 'ethers' +import { Database } from '../../components/database/index.js' +import { Auth } from '../../components/Auth/index.js' +import { getConfiguration, getMessageHash } from '../../utils/index.js' +import { + DEFAULT_TEST_TIMEOUT, + OverrideEnvConfig, + TEST_ENV_CONFIG_FILE, + buildEnvOverrideConfig, + setupEnvironment, + tearDownEnvironment, + getMockSupportedNetworks +} from '../utils/utils.js' +import { ENVIRONMENT_VARIABLES } from '../../utils/constants.js' +import { OceanNodeConfig } from '../../@types/OceanNode.js' +import { RPCS } from '../../@types/blockchain.js' +import axios from 'axios' + +describe('Auth Token Integration Tests', () => { + let config: OceanNodeConfig + let database: Database + let auth: Auth + let provider: JsonRpcProvider + let consumerAccount: Signer + let previousConfiguration: OverrideEnvConfig[] + + const mockSupportedNetworks: RPCS = getMockSupportedNetworks() + const url = 'http://localhost:8001/api/services/auth' + const validateDdoUrl = 'http://localhost:8001/api/aquarius/assets/ddo/validate' + + before(async () => { + previousConfiguration = await setupEnvironment( + TEST_ENV_CONFIG_FILE, + buildEnvOverrideConfig( + [ENVIRONMENT_VARIABLES.RPCS, ENVIRONMENT_VARIABLES.INDEXER_NETWORKS], + [JSON.stringify(mockSupportedNetworks), JSON.stringify([8996])] + ) + ) + + config = await getConfiguration(true) + database = await new Database(config.dbConfig) + auth = new Auth(database.authToken) + + provider = new JsonRpcProvider(mockSupportedNetworks['8996'].rpc) + + const consumerPrivateKey = + '0xef4b441145c1d0f3b4bc6d61d29f5c6e502359481152f869247c7a4244d45209' + consumerAccount = new Wallet(consumerPrivateKey, provider) + }) + + after(async () => { + await tearDownEnvironment(previousConfiguration) + }) + + const ddoValiationRequest = async (token: string) => { + try { + const validateResponse = await axios.post( + `${validateDdoUrl}`, + { + ddo: { + id: 'did:op:f00896cc6f5f9f2c17be06dd28bd6be085e1406bb55274cbd2b65b7271e7b104', + '@context': [], + version: '4.1.0', + nftAddress: '0x3357cCd4e75536422b61F6aeda3ad38545b9b01F', + chainId: 11155111, + metadata: { + created: new Date().toISOString(), + updated: new Date().toISOString(), + type: 'dataset', + name: 'Test DDO', + description: 'Test DDO', + tags: [], + author: 'Test Author', + license: 'https://market.oceanprotocol.com/terms', + additionalInformation: { + termsAndConditions: true + } + }, + services: [ + { + id: 'ccb398c50d6abd5b456e8d7242bd856a1767a890b537c2f8c10ba8b8a10e6025', + type: 'compute', + files: '0x0', + datatokenAddress: '0x0Cf4BE72EAD0583deD382589aFcbF34F3E860Bdc', + serviceEndpoint: '', + timeout: 86400 + } + ] + } + }, + { + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/octet-stream' + } + } + ) + + return validateResponse + } catch (error) { + console.log(`Error validating DDO: ${error}`) + return { status: error.response.status, data: error.response.data } + } + } + + describe('Token Management Tests', () => { + it('should create and validate token', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + const consumerAddress = await consumerAccount.getAddress() + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await consumerAccount.signMessage(messageHash) + + const createResponse = await axios.post(`${url}/token`, { + signature, + address: consumerAddress + }) + + expect(createResponse.status).to.equal(200) + expect(createResponse.data.token).to.be.a('string') + + const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) + expect(testEndpointResponse.status).to.equal(200) + }) + + it('should handle token expiry', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + const consumerAddress = await consumerAccount.getAddress() + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await consumerAccount.signMessage(messageHash) + + // Create token with 1 second expiry + const validUntil = Date.now() + 1000 + const createResponse = await axios.post(`${url}/token`, { + signature, + address: consumerAddress, + validUntil + }) + expect(createResponse.status).to.equal(200) + + // Wait for token to expire + await new Promise((resolve) => setTimeout(resolve, 1500)) + + const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) + expect(testEndpointResponse.status).to.equal(401) + }) + + it('should invalidate token', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + const consumerAddress = await consumerAccount.getAddress() + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await consumerAccount.signMessage(messageHash) + + const createResponse = await axios.post(`${url}/token`, { + signature, + address: consumerAddress + }) + const { token } = createResponse.data + + await axios.post(`${url}/token/invalidate`, { + signature, + address: consumerAddress, + token + }) + + const testEndpointResponse = await ddoValiationRequest(token) + expect(testEndpointResponse.status).to.equal(401) + }) + + describe('Error Cases', () => { + it('should handle invalid signatures', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + const consumerAddress = await consumerAccount.getAddress() + + try { + await axios.post(`${url}/token`, { + signature: '0xinvalid', + address: consumerAddress + }) + expect.fail('Should have thrown error for invalid signature') + } catch (error) { + expect(error.response.status).to.equal(400) + } + }) + + it('should handle invalid tokens', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + try { + const testEndpointResponse = await ddoValiationRequest('invalid-token') + expect(testEndpointResponse.status).to.equal(401) + } catch (error) { + console.log({ error }) + } + }) + + it('should handle missing parameters', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + // Missing signature + try { + await axios.post(`${url}/token`, { + address: await consumerAccount.getAddress() + }) + expect.fail('Should have thrown error for missing signature') + } catch (error) { + expect(error.response.status).to.equal(400) + } + + // Missing address + try { + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await consumerAccount.signMessage(messageHash) + + await axios.post(`${url}/token`, { + signature + }) + expect.fail('Should have thrown error for missing address') + } catch (error) { + expect(error.response.status).to.equal(400) + } + }) + }) + }) +}) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index 39690bed6..c8e27ee89 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -6,87 +6,82 @@ import { Wallet } from 'ethers' import { Auth } from '../../../components/Auth/index.js' describe('Auth Token Tests', () => { - let wallet: Wallet - let mockDatabase: Database - let config: OceanNodeConfig + let wallet: Wallet + let mockDatabase: Database + let config: OceanNodeConfig - before(async () => { - config = await getConfiguration(true) - mockDatabase = await new Database(config.dbConfig) - wallet = new Wallet(process.env.PRIVATE_KEY) - }) + before(async () => { + config = await getConfiguration(true) + mockDatabase = await new Database(config.dbConfig) + wallet = new Wallet(process.env.PRIVATE_KEY) + }) - const createToken = async (auth: Auth, address: string, validUntil: number) => { - const msg = auth.getSignatureMessage() - const messageHash = await getMessageHash(msg); - const signature = await wallet.signMessage(messageHash); - const token = await auth.createToken(signature, address, validUntil); - return token; - } + it('should create and validate a token', async () => { + const auth = new Auth(mockDatabase.authToken) + const token = await auth.createToken(wallet.address, null) + expect(token).to.be.a('string') - it('should create and validate a token', async () => { - const auth = new Auth(mockDatabase) - const token = await createToken(auth, wallet.address, null) - expect(token).to.be.a('string') + const validationResult = await auth.validateToken(token) + expect(validationResult).to.not.be.equal(null) + expect(validationResult?.address).to.equal(wallet.address) + }) - const validationResult = await auth.validateToken(token) - expect(validationResult).to.not.be.null - expect(validationResult?.address).to.equal(wallet.address) - }) + it('should validate authentication with token', async () => { + const auth = new Auth(mockDatabase.authToken) + const token = await auth.createToken(wallet.address, null) + const result = await auth.validateAuthenticationOrToken( + wallet.address, + undefined, + token + ) + expect(result.valid).to.be.equal(true) + }) + it('should validate authentication with signature', async () => { + const auth = new Auth(mockDatabase.authToken) + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await wallet.signMessage(messageHash) - it('should validate authentication with token', async () => { - const auth = new Auth(mockDatabase) - const token = await createToken(auth, wallet.address, null) - const result = await auth.validateAuthenticationOrToken(wallet.address, undefined, token) - expect(result.valid).to.be.true - }) + const result = await auth.validateAuthenticationOrToken( + wallet.address, + signature, + undefined, + message + ) + expect(result.valid).to.be.equal(true) + }) - it('should validate authentication with signature', async () => { - const auth = new Auth(mockDatabase) - const message = auth.getSignatureMessage() - const messageHash = await getMessageHash(message) - const signature = await wallet.signMessage(messageHash) + it('should fail validation with invalid token', async () => { + const auth = new Auth(mockDatabase.authToken) + const result = await auth.validateAuthenticationOrToken( + wallet.address, + undefined, + 'invalid-token' + ) + expect(result.valid).to.be.equal(false) + }) - const result = await auth.validateAuthenticationOrToken( - wallet.address, - signature, - undefined, - message - ) - expect(result.valid).to.be.true - }) + it('should fail validation with invalid signature', async () => { + const auth = new Auth(mockDatabase.authToken) + const message = 'Test message' + const invalidSignature = '0x' + '0'.repeat(130) - it('should fail validation with invalid token', async () => { - const auth = new Auth(mockDatabase) - const result = await auth.validateAuthenticationOrToken( - wallet.address, - undefined, - 'invalid-token' - ) - expect(result.valid).to.be.false - }) + const result = await auth.validateAuthenticationOrToken( + wallet.address, + invalidSignature, + undefined, + message + ) + expect(result.valid).to.be.equal(false) + }) - it('should fail validation with invalid signature', async () => { - const auth = new Auth(mockDatabase) - const message = 'Test message' - const invalidSignature = '0x' + '0'.repeat(130) + it('should respect token expiry', async () => { + const auth = new Auth(mockDatabase.authToken) + const validUntil = new Date(Date.now() - 1000) // 1 second ago + const token = await auth.createToken(wallet.address, validUntil.getTime()) - const result = await auth.validateAuthenticationOrToken( - wallet.address, - invalidSignature, - undefined, - message - ) - expect(result.valid).to.be.false - }) - - it('should respect token expiry', async () => { - const auth = new Auth(mockDatabase) - const validUntil = new Date(Date.now() - 1000) // 1 second ago - const token = await createToken(auth, wallet.address, validUntil.getTime()) - - const validationResult = await auth.validateToken(token) - expect(validationResult).to.be.null - }) -}) \ No newline at end of file + const validationResult = await auth.validateToken(token) + expect(validationResult).to.be.equal(null) + }) +}) diff --git a/src/utils/blockchain.ts b/src/utils/blockchain.ts index 593c52dee..72f4f9d31 100644 --- a/src/utils/blockchain.ts +++ b/src/utils/blockchain.ts @@ -270,7 +270,7 @@ export async function verifyMessage( } } -export async function getMessageHash(message: string): Promise { +export function getMessageHash(message: string): Uint8Array { const messageHash = ethers.solidityPackedKeccak256( ['bytes'], [ethers.hexlify(ethers.toUtf8Bytes(message))] From 9664a3be95b179435871d739704bd7057e9fea91 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 09:54:38 +0300 Subject: [PATCH 03/33] add jwt --- package-lock.json | 65 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index d458135c2..ccd5bb551 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,6 +50,7 @@ "hyperdiff": "^2.0.16", "ip": "^2.0.1", "it-pipe": "^3.0.1", + "jsonwebtoken": "^9.0.2", "libp2p": "^1.8.0", "lodash.clonedeep": "^4.5.0", "lzma-purejs-requirejs": "^1.0.0", @@ -12321,6 +12322,38 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "dev": true, @@ -12599,14 +12632,36 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "dev": true, "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "dev": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -12614,6 +12669,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, "node_modules/lodash.uniqby": { "version": "4.7.0", "dev": true, diff --git a/package.json b/package.json index 67e7da726..429619db6 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "hyperdiff": "^2.0.16", "ip": "^2.0.1", "it-pipe": "^3.0.1", + "jsonwebtoken": "^9.0.2", "libp2p": "^1.8.0", "lodash.clonedeep": "^4.5.0", "lzma-purejs-requirejs": "^1.0.0", From a41cc0291f2719cb9ace075bac91ccfe8eca4707 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 09:56:20 +0300 Subject: [PATCH 04/33] add jwt types --- package-lock.json | 19 +++++++++++++++++++ package.json | 1 + 2 files changed, 20 insertions(+) diff --git a/package-lock.json b/package-lock.json index ccd5bb551..07f7aa707 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,6 +74,7 @@ "@types/dockerode": "^3.3.31", "@types/express": "^4.17.17", "@types/ip": "^1.1.3", + "@types/jsonwebtoken": "^9.0.9", "@types/lzma-native": "^4.0.4", "@types/mocha": "^10.0.10", "@types/node": "^20.14.2", @@ -5353,6 +5354,17 @@ "version": "1.5.13", "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.9.tgz", + "integrity": "sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.14.200", "license": "MIT" @@ -5383,6 +5395,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/multer": { "version": "1.4.9", "license": "MIT", diff --git a/package.json b/package.json index 429619db6..b44d30f04 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@types/dockerode": "^3.3.31", "@types/express": "^4.17.17", "@types/ip": "^1.1.3", + "@types/jsonwebtoken": "^9.0.9", "@types/lzma-native": "^4.0.4", "@types/mocha": "^10.0.10", "@types/node": "^20.14.2", From 85d1a5fd836753368fc9f38e297b35d120a92385 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 10:30:30 +0300 Subject: [PATCH 05/33] fix unit tests --- src/OceanNode.ts | 4 +++- src/index.ts | 1 - 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/OceanNode.ts b/src/OceanNode.ts index 21727a0f1..8eeaada4b 100644 --- a/src/OceanNode.ts +++ b/src/OceanNode.ts @@ -49,7 +49,9 @@ export class OceanNode { this.coreHandlers = CoreHandlersRegistry.getInstance(this) this.requestMap = new Map() this.config = config - this.auth = new Auth(this.db.authToken) + if (this.db && this.db?.authToken) { + this.auth = new Auth(this.db.authToken) + } if (node) { node.setCoreHandlers(this.coreHandlers) } diff --git a/src/index.ts b/src/index.ts index a42641b62..398fe1811 100644 --- a/src/index.ts +++ b/src/index.ts @@ -170,7 +170,6 @@ if (config.hasHttp) { next() }) - // Add auth routes before the main routes app.use(authRoutes) From 7ac6c683f8a7e53773b983f6b24aceebdc8c4f13 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 11:20:35 +0300 Subject: [PATCH 06/33] refactor to handler --- src/@types/commands.ts | 12 ++ src/components/Auth/index.ts | 32 +----- src/components/core/handler/authHandler.ts | 104 ++++++++++++++++++ .../core/handler/coreHandlersRegistry.ts | 10 ++ src/components/httpRoutes/auth.ts | 46 +++++--- src/test/unit/auth/token.test.ts | 95 +++++++++++----- src/utils/constants.ts | 8 +- 7 files changed, 231 insertions(+), 76 deletions(-) create mode 100644 src/components/core/handler/authHandler.ts diff --git a/src/@types/commands.ts b/src/@types/commands.ts index e7c101192..74e519d0e 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -261,3 +261,15 @@ export interface StartStopIndexingCommand extends AdminCommand { export interface PolicyServerPassthroughCommand extends Command { policyServerPassthrough?: any } + +export interface CreateAuthTokenCommand extends Command { + address: string + signature: string + validUntil?: number | null +} + +export interface InvalidateAuthTokenCommand extends Command { + address: string + signature: string + token: string +} diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index c2f55f2e3..bbb9c5b9c 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -1,6 +1,5 @@ import { getMessageHash, verifyMessage } from '../../utils/index.js' import { AuthToken, AuthTokenDatabase } from '../database/AuthTokenDatabase.js' -import jwt from 'jsonwebtoken' export interface CommonValidation { valid: boolean @@ -26,35 +25,16 @@ export class Auth { return this.signatureMessage } + public getAuthTokenDatabase(): AuthTokenDatabase { + return this.authTokenDatabase + } + async validateSignature(signature: string, address: string): Promise { const messageHashBytes = getMessageHash(this.signatureMessage) const isValid = await verifyMessage(messageHashBytes, address, signature) return isValid } - async createToken( - address: string, - validUntil: number | null = null - ): Promise { - const createdAt = Date.now() - - const jwtToken = jwt.sign( - { - address, - createdAt - }, - this.jwtSecret - ) - - const token = await this.authTokenDatabase.createToken( - jwtToken, - address, - validUntil, - createdAt - ) - return token - } - async validateToken(token: string): Promise { const tokenEntry = await this.authTokenDatabase.validateToken(token) if (!tokenEntry) { @@ -63,10 +43,6 @@ export class Auth { return tokenEntry } - async invalidateToken(token: string): Promise { - await this.authTokenDatabase.invalidateToken(token) - } - async validateAuthenticationOrToken( address: string, signature?: string, diff --git a/src/components/core/handler/authHandler.ts b/src/components/core/handler/authHandler.ts new file mode 100644 index 000000000..57a586eaf --- /dev/null +++ b/src/components/core/handler/authHandler.ts @@ -0,0 +1,104 @@ +import { CommandHandler } from './handler.js' +import { P2PCommandResponse } from '../../../@types/OceanNode.js' +import { ValidateParams, validateCommandParameters } from '../../httpRoutes/validateCommands.js' +import { ReadableString } from '../../P2P/handlers.js' +import { Command } from '../../../@types/commands.js' +import { Readable } from 'stream' +import jwt from 'jsonwebtoken' + +export interface CreateAuthTokenCommand extends Command { + address: string + signature: string + validUntil?: number | null +} + +export interface InvalidateAuthTokenCommand extends Command { + address: string + signature: string + token: string +} + +export class CreateAuthTokenHandler extends CommandHandler { + validate(command: CreateAuthTokenCommand): ValidateParams { + return validateCommandParameters(command, ['address', 'signature']) + } + + async handle(task: CreateAuthTokenCommand): Promise { + const validationResponse = await this.verifyParamsAndRateLimits(task) + if (this.shouldDenyTaskHandling(validationResponse)) { + return validationResponse + } + + try { + const isValid = await this.getOceanNode().getAuth().validateSignature(task.signature, task.address) + if (!isValid) { + return { + stream: null, + status: { httpStatus: 400, error: 'Invalid signature' } + } + } + + const createdAt = Date.now() + const jwtToken = jwt.sign( + { + address: task.address, + createdAt + }, + this.getOceanNode().getAuth().getJwtSecret() + ) + + const token = await this.getOceanNode() + .getAuth() + .getAuthTokenDatabase() + .createToken(jwtToken, task.address, task.validUntil, createdAt) + + return { + stream: Readable.from(JSON.stringify({ token })), + status: { httpStatus: 200, error: null } + } + } catch (error) { + return { + stream: null, + status: { httpStatus: 500, error: `Error creating auth token: ${error}` } + } + } + } +} + +export class InvalidateAuthTokenHandler extends CommandHandler { + validate(command: InvalidateAuthTokenCommand): ValidateParams { + return validateCommandParameters(command, ['address', 'signature', 'token']) + } + + async handle(task: InvalidateAuthTokenCommand): Promise { + const validationResponse = await this.verifyParamsAndRateLimits(task) + if (this.shouldDenyTaskHandling(validationResponse)) { + return validationResponse + } + + try { + const isValid = await this.getOceanNode().getAuth().validateSignature(task.signature, task.address) + if (!isValid) { + return { + stream: null, + status: { httpStatus: 400, error: 'Invalid signature' } + } + } + + await this.getOceanNode() + .getAuth() + .getAuthTokenDatabase() + .invalidateToken(task.token) + + return { + stream: new ReadableString(JSON.stringify({ success: true })), + status: { httpStatus: 200, error: null } + } + } catch (error) { + return { + stream: null, + status: { httpStatus: 500, error: `Error invalidating auth token: ${error}` } + } + } + } +} \ No newline at end of file diff --git a/src/components/core/handler/coreHandlersRegistry.ts b/src/components/core/handler/coreHandlersRegistry.ts index 4656b8727..ffa8a6103 100644 --- a/src/components/core/handler/coreHandlersRegistry.ts +++ b/src/components/core/handler/coreHandlersRegistry.ts @@ -42,6 +42,8 @@ import { GetP2PNetworkStatsHandler, FindPeerHandler } from './p2p.js' +import { CreateAuthTokenHandler, InvalidateAuthTokenHandler } from './authHandler.js' + export type HandlerRegistry = { handlerName: string // name of the handler handlerImpl: BaseHandler // class that implements it @@ -149,6 +151,14 @@ export class CoreHandlersRegistry { new GetP2PNetworkStatsHandler(node) ) this.registerCoreHandler(PROTOCOL_COMMANDS.FIND_PEER, new FindPeerHandler(node)) + this.registerCoreHandler( + PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + new CreateAuthTokenHandler(node) + ) + this.registerCoreHandler( + PROTOCOL_COMMANDS.INVALIDATE_AUTH_TOKEN, + new InvalidateAuthTokenHandler(node) + ) } public static getInstance(node: OceanNode): CoreHandlersRegistry { diff --git a/src/components/httpRoutes/auth.ts b/src/components/httpRoutes/auth.ts index c5cc02eaa..ad5095011 100644 --- a/src/components/httpRoutes/auth.ts +++ b/src/components/httpRoutes/auth.ts @@ -1,6 +1,10 @@ import express from 'express' +import { SERVICES_API_BASE_PATH } from '../../utils/constants.js' import { HTTP_LOGGER } from '../../utils/logging/common.js' -import { SERVICES_API_BASE_PATH } from '../../utils/index.js' +import { PROTOCOL_COMMANDS } from '../../utils/constants.js' +import { CreateAuthTokenHandler, InvalidateAuthTokenHandler } from '../core/handler/authHandler.js' +import { streamToString } from '../../utils/util.js' +import { Readable } from 'stream' export const authRoutes = express.Router() @@ -11,20 +15,23 @@ authRoutes.post( try { const { signature, address, validUntil } = req.body - console.log({ signature, address, validUntil }) - if (!signature || !address) { return res.status(400).json({ error: 'Missing required parameters' }) } - const isValid = await req.oceanNode.getAuth().validateSignature(signature, address) - if (!isValid) { - return res.status(400).json({ error: 'Invalid signature' }) - } + const response = await new CreateAuthTokenHandler(req.oceanNode).handle({ + command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + signature, + address, + validUntil + }); - const token = await req.oceanNode.getAuth().createToken(address, validUntil) + if (response.status.error) { + return res.status(response.status.httpStatus).json({ error: response.status.error }) + } - res.json({ token }) + const result = JSON.parse(await streamToString(response.stream as Readable)) + res.json(result) } catch (error) { HTTP_LOGGER.error(`Error creating auth token: ${error}`) res.status(500).json({ error: 'Internal server error' }) @@ -43,17 +50,22 @@ authRoutes.post( return res.status(400).json({ error: 'Missing required parameters' }) } - const isValid = await req.oceanNode.getAuth().validateSignature(signature, address) - if (!isValid) { - return res.status(400).json({ error: 'Invalid signature' }) - } + const response = await new InvalidateAuthTokenHandler(req.oceanNode).handle({ + command: PROTOCOL_COMMANDS.INVALIDATE_AUTH_TOKEN, + signature, + address, + token + }) - await req.oceanNode.getAuth().invalidateToken(token) + if (response.status.error) { + return res.status(response.status.httpStatus).json({ error: response.status.error }) + } - res.json({ success: true }) + const result = JSON.parse(await streamToString(response.stream as Readable)) + res.json(result) } catch (error) { - HTTP_LOGGER.error(`Error deleting auth token: ${error}`) - res.status(500).json({ success: false, error: 'Internal server error' }) + HTTP_LOGGER.error(`Error invalidating auth token: ${error}`) + res.status(500).json({ error: 'Internal server error' }) } } ) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index c8e27ee89..aeb69252d 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -4,56 +4,57 @@ import { expect } from 'chai' import { Database } from '../../../components/database/index.js' import { Wallet } from 'ethers' import { Auth } from '../../../components/Auth/index.js' +import { OceanNode } from '../../../OceanNode.js' +import { CreateAuthTokenHandler, InvalidateAuthTokenHandler } from '../../../components/core/handler/authHandler.js' +import { streamToString } from '../../../utils/util.js' +import { Readable } from 'stream' describe('Auth Token Tests', () => { let wallet: Wallet let mockDatabase: Database let config: OceanNodeConfig + let oceanNode: OceanNode + let createTokenHandler: CreateAuthTokenHandler + let invalidateTokenHandler: InvalidateAuthTokenHandler + let auth: Auth before(async () => { config = await getConfiguration(true) mockDatabase = await new Database(config.dbConfig) wallet = new Wallet(process.env.PRIVATE_KEY) + oceanNode = OceanNode.getInstance(config, mockDatabase) + createTokenHandler = new CreateAuthTokenHandler(oceanNode) + invalidateTokenHandler = new InvalidateAuthTokenHandler(oceanNode) + auth = new Auth(mockDatabase.authToken) }) - it('should create and validate a token', async () => { - const auth = new Auth(mockDatabase.authToken) - const token = await auth.createToken(wallet.address, null) - expect(token).to.be.a('string') - const validationResult = await auth.validateToken(token) - expect(validationResult).to.not.be.equal(null) - expect(validationResult?.address).to.equal(wallet.address) - }) - - it('should validate authentication with token', async () => { - const auth = new Auth(mockDatabase.authToken) - const token = await auth.createToken(wallet.address, null) - const result = await auth.validateAuthenticationOrToken( - wallet.address, - undefined, - token - ) - expect(result.valid).to.be.equal(true) - }) - - it('should validate authentication with signature', async () => { - const auth = new Auth(mockDatabase.authToken) + it('should create and validate a token', async () => { const message = auth.getSignatureMessage() const messageHash = getMessageHash(message) const signature = await wallet.signMessage(messageHash) + const tokenCreateResponse = await createTokenHandler.handle({ + command: 'createAuthToken', + address: wallet.address, + signature, + }) + const data: string = await streamToString(tokenCreateResponse.stream as Readable) + expect(tokenCreateResponse.status.httpStatus).to.be.equal(200) + expect(data).to.be.a('string') + const tokenResponse = JSON.parse(data) + const token = tokenResponse.token + const result = await auth.validateAuthenticationOrToken( wallet.address, - signature, undefined, - message + token ) expect(result.valid).to.be.equal(true) }) + it('should fail validation with invalid token', async () => { - const auth = new Auth(mockDatabase.authToken) const result = await auth.validateAuthenticationOrToken( wallet.address, undefined, @@ -63,7 +64,6 @@ describe('Auth Token Tests', () => { }) it('should fail validation with invalid signature', async () => { - const auth = new Auth(mockDatabase.authToken) const message = 'Test message' const invalidSignature = '0x' + '0'.repeat(130) @@ -77,9 +77,46 @@ describe('Auth Token Tests', () => { }) it('should respect token expiry', async () => { - const auth = new Auth(mockDatabase.authToken) - const validUntil = new Date(Date.now() - 1000) // 1 second ago - const token = await auth.createToken(wallet.address, validUntil.getTime()) + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await wallet.signMessage(messageHash) + + const tokenCreateResponse = await createTokenHandler.handle({ + command: 'createAuthToken', + address: wallet.address, + signature, + validUntil: Date.now() + 1000 + }) + const data: string = await streamToString(tokenCreateResponse.stream as Readable) + const token = JSON.parse(data).token + + await new Promise(resolve => setTimeout(resolve, 1500)) + + const validationResult = await auth.validateToken(token) + expect(validationResult).to.be.equal(null) + }) + + it('should invalidate a token', async () => { + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await wallet.signMessage(messageHash) + + const tokenCreateResponse = await createTokenHandler.handle({ + command: 'createAuthToken', + address: wallet.address, + signature, + }) + const data: string = await streamToString(tokenCreateResponse.stream as Readable) + const token = JSON.parse(data).token + + const invalidateTokenResponse = await invalidateTokenHandler.handle({ + command: 'invalidateAuthToken', + address: wallet.address, + signature, + token + }) + console.log({ invalidateTokenResponse }) + expect(invalidateTokenResponse.status.httpStatus).to.be.equal(200) const validationResult = await auth.validateToken(token) expect(validationResult).to.be.equal(null) diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 4a4316452..edd438f28 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -33,7 +33,9 @@ export const PROTOCOL_COMMANDS = { GET_P2P_PEER: 'getP2PPeer', GET_P2P_PEERS: 'getP2PPeers', GET_P2P_NETWORK_STATS: 'getP2PNetworkStats', - FIND_PEER: 'findPeer' + FIND_PEER: 'findPeer', + CREATE_AUTH_TOKEN: 'createAuthToken', + INVALIDATE_AUTH_TOKEN: 'invalidateAuthToken' } // more visible, keep then close to make sure we always update both export const SUPPORTED_PROTOCOL_COMMANDS: string[] = [ @@ -67,7 +69,9 @@ export const SUPPORTED_PROTOCOL_COMMANDS: string[] = [ PROTOCOL_COMMANDS.GET_P2P_PEER, PROTOCOL_COMMANDS.GET_P2P_PEERS, PROTOCOL_COMMANDS.GET_P2P_NETWORK_STATS, - PROTOCOL_COMMANDS.FIND_PEER + PROTOCOL_COMMANDS.FIND_PEER, + PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + PROTOCOL_COMMANDS.INVALIDATE_AUTH_TOKEN ] export const MetadataStates = { From 99d99b841c388670897140538eccbd1d10e3bd0c Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 11:22:48 +0300 Subject: [PATCH 07/33] remove unused typesense schema --- src/components/core/handler/authHandler.ts | 155 ++++++++++---------- src/components/database/TypesenseSchemas.ts | 10 -- src/components/httpRoutes/auth.ts | 18 ++- src/test/unit/auth/token.test.ts | 19 +-- 4 files changed, 103 insertions(+), 99 deletions(-) diff --git a/src/components/core/handler/authHandler.ts b/src/components/core/handler/authHandler.ts index 57a586eaf..992fb0945 100644 --- a/src/components/core/handler/authHandler.ts +++ b/src/components/core/handler/authHandler.ts @@ -1,104 +1,111 @@ import { CommandHandler } from './handler.js' import { P2PCommandResponse } from '../../../@types/OceanNode.js' -import { ValidateParams, validateCommandParameters } from '../../httpRoutes/validateCommands.js' +import { + ValidateParams, + validateCommandParameters +} from '../../httpRoutes/validateCommands.js' import { ReadableString } from '../../P2P/handlers.js' import { Command } from '../../../@types/commands.js' import { Readable } from 'stream' import jwt from 'jsonwebtoken' export interface CreateAuthTokenCommand extends Command { - address: string - signature: string - validUntil?: number | null + address: string + signature: string + validUntil?: number | null } export interface InvalidateAuthTokenCommand extends Command { - address: string - signature: string - token: string + address: string + signature: string + token: string } export class CreateAuthTokenHandler extends CommandHandler { - validate(command: CreateAuthTokenCommand): ValidateParams { - return validateCommandParameters(command, ['address', 'signature']) + validate(command: CreateAuthTokenCommand): ValidateParams { + return validateCommandParameters(command, ['address', 'signature']) + } + + async handle(task: CreateAuthTokenCommand): Promise { + const validationResponse = await this.verifyParamsAndRateLimits(task) + if (this.shouldDenyTaskHandling(validationResponse)) { + return validationResponse } - async handle(task: CreateAuthTokenCommand): Promise { - const validationResponse = await this.verifyParamsAndRateLimits(task) - if (this.shouldDenyTaskHandling(validationResponse)) { - return validationResponse + try { + const isValid = await this.getOceanNode() + .getAuth() + .validateSignature(task.signature, task.address) + if (!isValid) { + return { + stream: null, + status: { httpStatus: 400, error: 'Invalid signature' } } + } - try { - const isValid = await this.getOceanNode().getAuth().validateSignature(task.signature, task.address) - if (!isValid) { - return { - stream: null, - status: { httpStatus: 400, error: 'Invalid signature' } - } - } - - const createdAt = Date.now() - const jwtToken = jwt.sign( - { - address: task.address, - createdAt - }, - this.getOceanNode().getAuth().getJwtSecret() - ) + const createdAt = Date.now() + const jwtToken = jwt.sign( + { + address: task.address, + createdAt + }, + this.getOceanNode().getAuth().getJwtSecret() + ) - const token = await this.getOceanNode() - .getAuth() - .getAuthTokenDatabase() - .createToken(jwtToken, task.address, task.validUntil, createdAt) + const token = await this.getOceanNode() + .getAuth() + .getAuthTokenDatabase() + .createToken(jwtToken, task.address, task.validUntil, createdAt) - return { - stream: Readable.from(JSON.stringify({ token })), - status: { httpStatus: 200, error: null } - } - } catch (error) { - return { - stream: null, - status: { httpStatus: 500, error: `Error creating auth token: ${error}` } - } - } + return { + stream: Readable.from(JSON.stringify({ token })), + status: { httpStatus: 200, error: null } + } + } catch (error) { + return { + stream: null, + status: { httpStatus: 500, error: `Error creating auth token: ${error}` } + } } + } } export class InvalidateAuthTokenHandler extends CommandHandler { - validate(command: InvalidateAuthTokenCommand): ValidateParams { - return validateCommandParameters(command, ['address', 'signature', 'token']) + validate(command: InvalidateAuthTokenCommand): ValidateParams { + return validateCommandParameters(command, ['address', 'signature', 'token']) + } + + async handle(task: InvalidateAuthTokenCommand): Promise { + const validationResponse = await this.verifyParamsAndRateLimits(task) + if (this.shouldDenyTaskHandling(validationResponse)) { + return validationResponse } - async handle(task: InvalidateAuthTokenCommand): Promise { - const validationResponse = await this.verifyParamsAndRateLimits(task) - if (this.shouldDenyTaskHandling(validationResponse)) { - return validationResponse + try { + const isValid = await this.getOceanNode() + .getAuth() + .validateSignature(task.signature, task.address) + if (!isValid) { + return { + stream: null, + status: { httpStatus: 400, error: 'Invalid signature' } } + } - try { - const isValid = await this.getOceanNode().getAuth().validateSignature(task.signature, task.address) - if (!isValid) { - return { - stream: null, - status: { httpStatus: 400, error: 'Invalid signature' } - } - } + await this.getOceanNode() + .getAuth() + .getAuthTokenDatabase() + .invalidateToken(task.token) - await this.getOceanNode() - .getAuth() - .getAuthTokenDatabase() - .invalidateToken(task.token) - - return { - stream: new ReadableString(JSON.stringify({ success: true })), - status: { httpStatus: 200, error: null } - } - } catch (error) { - return { - stream: null, - status: { httpStatus: 500, error: `Error invalidating auth token: ${error}` } - } - } + return { + stream: new ReadableString(JSON.stringify({ success: true })), + status: { httpStatus: 200, error: null } + } + } catch (error) { + return { + stream: null, + status: { httpStatus: 500, error: `Error invalidating auth token: ${error}` } + } } -} \ No newline at end of file + } +} diff --git a/src/components/database/TypesenseSchemas.ts b/src/components/database/TypesenseSchemas.ts index a5b25c48a..0cdf7ea5e 100644 --- a/src/components/database/TypesenseSchemas.ts +++ b/src/components/database/TypesenseSchemas.ts @@ -53,7 +53,6 @@ export type TypesenseSchemas = { logSchemas: TypesenseSchema orderSchema: TypesenseSchema ddoStateSchema: TypesenseSchema - authTokenSchemas: TypesenseSchema } const ddoSchemas = readJsonSchemas() export const typesenseSchemas: TypesenseSchemas = { @@ -127,14 +126,5 @@ export const typesenseSchemas: TypesenseSchemas = { { name: 'valid', type: 'bool' }, { name: 'error', type: 'string' } ] - }, - authTokenSchemas: { - name: 'authTokens', - enable_nested_fields: true, - fields: [ - { name: 'token', type: 'string' }, - { name: 'address', type: 'string' }, - { name: 'createdAt', type: 'int64' } - ] } } diff --git a/src/components/httpRoutes/auth.ts b/src/components/httpRoutes/auth.ts index ad5095011..f0c5979f7 100644 --- a/src/components/httpRoutes/auth.ts +++ b/src/components/httpRoutes/auth.ts @@ -1,8 +1,10 @@ import express from 'express' -import { SERVICES_API_BASE_PATH } from '../../utils/constants.js' +import { SERVICES_API_BASE_PATH, PROTOCOL_COMMANDS } from '../../utils/constants.js' import { HTTP_LOGGER } from '../../utils/logging/common.js' -import { PROTOCOL_COMMANDS } from '../../utils/constants.js' -import { CreateAuthTokenHandler, InvalidateAuthTokenHandler } from '../core/handler/authHandler.js' +import { + CreateAuthTokenHandler, + InvalidateAuthTokenHandler +} from '../core/handler/authHandler.js' import { streamToString } from '../../utils/util.js' import { Readable } from 'stream' @@ -24,10 +26,12 @@ authRoutes.post( signature, address, validUntil - }); + }) if (response.status.error) { - return res.status(response.status.httpStatus).json({ error: response.status.error }) + return res + .status(response.status.httpStatus) + .json({ error: response.status.error }) } const result = JSON.parse(await streamToString(response.stream as Readable)) @@ -58,7 +62,9 @@ authRoutes.post( }) if (response.status.error) { - return res.status(response.status.httpStatus).json({ error: response.status.error }) + return res + .status(response.status.httpStatus) + .json({ error: response.status.error }) } const result = JSON.parse(await streamToString(response.stream as Readable)) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index aeb69252d..d069f5bec 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -5,7 +5,10 @@ import { Database } from '../../../components/database/index.js' import { Wallet } from 'ethers' import { Auth } from '../../../components/Auth/index.js' import { OceanNode } from '../../../OceanNode.js' -import { CreateAuthTokenHandler, InvalidateAuthTokenHandler } from '../../../components/core/handler/authHandler.js' +import { + CreateAuthTokenHandler, + InvalidateAuthTokenHandler +} from '../../../components/core/handler/authHandler.js' import { streamToString } from '../../../utils/util.js' import { Readable } from 'stream' @@ -28,7 +31,6 @@ describe('Auth Token Tests', () => { auth = new Auth(mockDatabase.authToken) }) - it('should create and validate a token', async () => { const message = auth.getSignatureMessage() const messageHash = getMessageHash(message) @@ -37,13 +39,13 @@ describe('Auth Token Tests', () => { const tokenCreateResponse = await createTokenHandler.handle({ command: 'createAuthToken', address: wallet.address, - signature, + signature }) const data: string = await streamToString(tokenCreateResponse.stream as Readable) expect(tokenCreateResponse.status.httpStatus).to.be.equal(200) expect(data).to.be.a('string') const tokenResponse = JSON.parse(data) - const token = tokenResponse.token + const { token } = tokenResponse const result = await auth.validateAuthenticationOrToken( wallet.address, @@ -53,7 +55,6 @@ describe('Auth Token Tests', () => { expect(result.valid).to.be.equal(true) }) - it('should fail validation with invalid token', async () => { const result = await auth.validateAuthenticationOrToken( wallet.address, @@ -88,9 +89,9 @@ describe('Auth Token Tests', () => { validUntil: Date.now() + 1000 }) const data: string = await streamToString(tokenCreateResponse.stream as Readable) - const token = JSON.parse(data).token + const { token } = JSON.parse(data) - await new Promise(resolve => setTimeout(resolve, 1500)) + await new Promise((resolve) => setTimeout(resolve, 1500)) const validationResult = await auth.validateToken(token) expect(validationResult).to.be.equal(null) @@ -104,10 +105,10 @@ describe('Auth Token Tests', () => { const tokenCreateResponse = await createTokenHandler.handle({ command: 'createAuthToken', address: wallet.address, - signature, + signature }) const data: string = await streamToString(tokenCreateResponse.stream as Readable) - const token = JSON.parse(data).token + const { token } = JSON.parse(data) const invalidateTokenResponse = await invalidateTokenHandler.handle({ command: 'invalidateAuthToken', From c3451a8d7c54531100b5c04e1cce72bdcf9efda9 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 11:28:16 +0300 Subject: [PATCH 08/33] remove console logs --- src/components/httpRoutes/aquarius.ts | 4 ---- src/test/integration/auth.test.ts | 8 ++------ src/test/unit/auth/token.test.ts | 1 - 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/components/httpRoutes/aquarius.ts b/src/components/httpRoutes/aquarius.ts index 37873bf9a..7a1d51d81 100644 --- a/src/components/httpRoutes/aquarius.ts +++ b/src/components/httpRoutes/aquarius.ts @@ -139,20 +139,16 @@ aquariusRoutes.post( async (req, res) => { const node = req.oceanNode try { - console.log({ reqDdo: req.body }) if (!req.body) { res.status(400).send('Missing DDO object') return } const requestBody = JSON.parse(req.body) - console.log({ requestBody }) const { publisherAddress, nonce, signature } = requestBody - console.log({ publisherAddress, nonce, signature }) // This is for backward compatibility with the old way of sending the DDO const ddo = requestBody.ddo || JSON.parse(req.body) - console.log({ ddo }) if (!ddo.version) { res.status(400).send('Missing DDO version') diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts index fdf2b490e..4ccf9aa91 100644 --- a/src/test/integration/auth.test.ts +++ b/src/test/integration/auth.test.ts @@ -193,12 +193,8 @@ describe('Auth Token Integration Tests', () => { it('should handle invalid tokens', async function () { this.timeout(DEFAULT_TEST_TIMEOUT) - try { - const testEndpointResponse = await ddoValiationRequest('invalid-token') - expect(testEndpointResponse.status).to.equal(401) - } catch (error) { - console.log({ error }) - } + const testEndpointResponse = await ddoValiationRequest('invalid-token') + expect(testEndpointResponse.status).to.equal(401) }) it('should handle missing parameters', async function () { diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index d069f5bec..f7f3e1349 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -116,7 +116,6 @@ describe('Auth Token Tests', () => { signature, token }) - console.log({ invalidateTokenResponse }) expect(invalidateTokenResponse.status.httpStatus).to.be.equal(200) const validationResult = await auth.validateToken(token) From 02611ca5a46b72f08dc66eb6a218e85f6b47738f Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 11:36:52 +0300 Subject: [PATCH 09/33] revert ddo file --- src/components/httpRoutes/aquarius.ts | 69 ++++++++++++--------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/src/components/httpRoutes/aquarius.ts b/src/components/httpRoutes/aquarius.ts index 7a1d51d81..e95171373 100644 --- a/src/components/httpRoutes/aquarius.ts +++ b/src/components/httpRoutes/aquarius.ts @@ -10,7 +10,6 @@ import { QueryCommand } from '../../@types/commands.js' import { DatabaseFactory } from '../database/DatabaseFactory.js' import { SearchQuery } from '../../@types/DDO/SearchQuery.js' import { getConfiguration } from '../../utils/index.js' -import { validateAuthToken } from './middleware/authMiddleware.js' export const aquariusRoutes = express.Router() @@ -133,47 +132,41 @@ aquariusRoutes.get(`${AQUARIUS_API_BASE_PATH}/state/ddo`, async (req, res) => { } }) -aquariusRoutes.post( - `${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, - validateAuthToken, - async (req, res) => { - const node = req.oceanNode - try { - if (!req.body) { - res.status(400).send('Missing DDO object') - return - } +aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req, res) => { + const node = req.oceanNode + try { + if (!req.body) { + res.status(400).send('Missing DDO object') + return + } - const requestBody = JSON.parse(req.body) - const { publisherAddress, nonce, signature } = requestBody + const requestBody = JSON.parse(req.body) + const { publisherAddress, nonce, signature } = requestBody - // This is for backward compatibility with the old way of sending the DDO - const ddo = requestBody.ddo || JSON.parse(req.body) + // This is for backward compatibility with the old way of sending the DDO + const ddo = requestBody.ddo || JSON.parse(req.body) - if (!ddo.version) { - res.status(400).send('Missing DDO version') - return - } + if (!ddo.version) { + res.status(400).send('Missing DDO version') + return + } - const result = await new ValidateDDOHandler(node).handle({ - ddo, - publisherAddress, - nonce, - signature, - command: PROTOCOL_COMMANDS.VALIDATE_DDO - }) + const result = await new ValidateDDOHandler(node).handle({ + ddo, + publisherAddress, + nonce, + signature, + command: PROTOCOL_COMMANDS.VALIDATE_DDO + }) - if (result.stream) { - const validationResult = JSON.parse( - await streamToString(result.stream as Readable) - ) - res.json(validationResult) - } else { - res.status(result.status.httpStatus).send(result.status.error) - } - } catch (error) { - HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) - res.status(500).send('Internal Server Error') + if (result.stream) { + const validationResult = JSON.parse(await streamToString(result.stream as Readable)) + res.json(validationResult) + } else { + res.status(result.status.httpStatus).send(result.status.error) } + } catch (error) { + HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) + res.status(500).send('Internal Server Error') } -) +}) From 3fce736ba3894679704f0f2d751d12b056d351f8 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 12:00:24 +0300 Subject: [PATCH 10/33] refactor --- src/components/Auth/index.ts | 26 ++++++- src/components/core/handler/authHandler.ts | 21 ++---- src/components/httpRoutes/aquarius.ts | 69 ++++++++++--------- src/test/integration/auth.test.ts | 2 +- src/test/unit/auth/token.test.ts | 80 ++++------------------ 5 files changed, 83 insertions(+), 115 deletions(-) diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index bbb9c5b9c..f730ef64b 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -1,5 +1,6 @@ import { getMessageHash, verifyMessage } from '../../utils/index.js' import { AuthToken, AuthTokenDatabase } from '../database/AuthTokenDatabase.js' +import jwt from 'jsonwebtoken' export interface CommonValidation { valid: boolean @@ -25,8 +26,29 @@ export class Auth { return this.signatureMessage } - public getAuthTokenDatabase(): AuthTokenDatabase { - return this.authTokenDatabase + getJWTToken(address: string, createdAt: number): string { + const jwtToken = jwt.sign( + { + address, + createdAt + }, + this.getJwtSecret() + ) + + return jwtToken + } + + async insertToken( + address: string, + jwtToken: string, + validUntil: number, + createdAt: number + ): Promise { + await this.authTokenDatabase.createToken(jwtToken, address, validUntil, createdAt) + } + + async invalidateToken(jwtToken: string): Promise { + await this.authTokenDatabase.invalidateToken(jwtToken) } async validateSignature(signature: string, address: string): Promise { diff --git a/src/components/core/handler/authHandler.ts b/src/components/core/handler/authHandler.ts index 992fb0945..4278dca8f 100644 --- a/src/components/core/handler/authHandler.ts +++ b/src/components/core/handler/authHandler.ts @@ -7,7 +7,6 @@ import { import { ReadableString } from '../../P2P/handlers.js' import { Command } from '../../../@types/commands.js' import { Readable } from 'stream' -import jwt from 'jsonwebtoken' export interface CreateAuthTokenCommand extends Command { address: string @@ -44,21 +43,14 @@ export class CreateAuthTokenHandler extends CommandHandler { } const createdAt = Date.now() - const jwtToken = jwt.sign( - { - address: task.address, - createdAt - }, - this.getOceanNode().getAuth().getJwtSecret() - ) + const jwtToken = this.getOceanNode().getAuth().getJWTToken(task.address, createdAt) - const token = await this.getOceanNode() + await this.getOceanNode() .getAuth() - .getAuthTokenDatabase() - .createToken(jwtToken, task.address, task.validUntil, createdAt) + .insertToken(task.address, jwtToken, task.validUntil, createdAt) return { - stream: Readable.from(JSON.stringify({ token })), + stream: Readable.from(JSON.stringify({ token: jwtToken })), status: { httpStatus: 200, error: null } } } catch (error) { @@ -92,10 +84,7 @@ export class InvalidateAuthTokenHandler extends CommandHandler { } } - await this.getOceanNode() - .getAuth() - .getAuthTokenDatabase() - .invalidateToken(task.token) + await this.getOceanNode().getAuth().invalidateToken(task.token) return { stream: new ReadableString(JSON.stringify({ success: true })), diff --git a/src/components/httpRoutes/aquarius.ts b/src/components/httpRoutes/aquarius.ts index e95171373..7a1d51d81 100644 --- a/src/components/httpRoutes/aquarius.ts +++ b/src/components/httpRoutes/aquarius.ts @@ -10,6 +10,7 @@ import { QueryCommand } from '../../@types/commands.js' import { DatabaseFactory } from '../database/DatabaseFactory.js' import { SearchQuery } from '../../@types/DDO/SearchQuery.js' import { getConfiguration } from '../../utils/index.js' +import { validateAuthToken } from './middleware/authMiddleware.js' export const aquariusRoutes = express.Router() @@ -132,41 +133,47 @@ aquariusRoutes.get(`${AQUARIUS_API_BASE_PATH}/state/ddo`, async (req, res) => { } }) -aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req, res) => { - const node = req.oceanNode - try { - if (!req.body) { - res.status(400).send('Missing DDO object') - return - } +aquariusRoutes.post( + `${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, + validateAuthToken, + async (req, res) => { + const node = req.oceanNode + try { + if (!req.body) { + res.status(400).send('Missing DDO object') + return + } - const requestBody = JSON.parse(req.body) - const { publisherAddress, nonce, signature } = requestBody + const requestBody = JSON.parse(req.body) + const { publisherAddress, nonce, signature } = requestBody - // This is for backward compatibility with the old way of sending the DDO - const ddo = requestBody.ddo || JSON.parse(req.body) + // This is for backward compatibility with the old way of sending the DDO + const ddo = requestBody.ddo || JSON.parse(req.body) - if (!ddo.version) { - res.status(400).send('Missing DDO version') - return - } + if (!ddo.version) { + res.status(400).send('Missing DDO version') + return + } - const result = await new ValidateDDOHandler(node).handle({ - ddo, - publisherAddress, - nonce, - signature, - command: PROTOCOL_COMMANDS.VALIDATE_DDO - }) + const result = await new ValidateDDOHandler(node).handle({ + ddo, + publisherAddress, + nonce, + signature, + command: PROTOCOL_COMMANDS.VALIDATE_DDO + }) - if (result.stream) { - const validationResult = JSON.parse(await streamToString(result.stream as Readable)) - res.json(validationResult) - } else { - res.status(result.status.httpStatus).send(result.status.error) + if (result.stream) { + const validationResult = JSON.parse( + await streamToString(result.stream as Readable) + ) + res.json(validationResult) + } else { + res.status(result.status.httpStatus).send(result.status.error) + } + } catch (error) { + HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) + res.status(500).send('Internal Server Error') } - } catch (error) { - HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) - res.status(500).send('Internal Server Error') } -}) +) diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts index 4ccf9aa91..f17329790 100644 --- a/src/test/integration/auth.test.ts +++ b/src/test/integration/auth.test.ts @@ -143,7 +143,7 @@ describe('Auth Token Integration Tests', () => { expect(createResponse.status).to.equal(200) // Wait for token to expire - await new Promise((resolve) => setTimeout(resolve, 1500)) + await new Promise((resolve) => setTimeout(resolve, 2000)) const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) expect(testEndpointResponse.status).to.equal(401) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index f7f3e1349..7e61a2150 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -1,56 +1,31 @@ import { OceanNodeConfig } from '../../../@types/OceanNode.js' -import { getConfiguration, getMessageHash } from '../../../utils/index.js' +import { getConfiguration } from '../../../utils/index.js' import { expect } from 'chai' -import { Database } from '../../../components/database/index.js' import { Wallet } from 'ethers' import { Auth } from '../../../components/Auth/index.js' -import { OceanNode } from '../../../OceanNode.js' -import { - CreateAuthTokenHandler, - InvalidateAuthTokenHandler -} from '../../../components/core/handler/authHandler.js' -import { streamToString } from '../../../utils/util.js' -import { Readable } from 'stream' +import { AuthTokenDatabase } from '../../../components/database/AuthTokenDatabase.js' describe('Auth Token Tests', () => { let wallet: Wallet - let mockDatabase: Database + let authTokenDatabase: AuthTokenDatabase let config: OceanNodeConfig - let oceanNode: OceanNode - let createTokenHandler: CreateAuthTokenHandler - let invalidateTokenHandler: InvalidateAuthTokenHandler let auth: Auth before(async () => { config = await getConfiguration(true) - mockDatabase = await new Database(config.dbConfig) + authTokenDatabase = await AuthTokenDatabase.create(config.dbConfig) wallet = new Wallet(process.env.PRIVATE_KEY) - oceanNode = OceanNode.getInstance(config, mockDatabase) - createTokenHandler = new CreateAuthTokenHandler(oceanNode) - invalidateTokenHandler = new InvalidateAuthTokenHandler(oceanNode) - auth = new Auth(mockDatabase.authToken) + auth = new Auth(authTokenDatabase) }) it('should create and validate a token', async () => { - const message = auth.getSignatureMessage() - const messageHash = getMessageHash(message) - const signature = await wallet.signMessage(messageHash) - - const tokenCreateResponse = await createTokenHandler.handle({ - command: 'createAuthToken', - address: wallet.address, - signature - }) - const data: string = await streamToString(tokenCreateResponse.stream as Readable) - expect(tokenCreateResponse.status.httpStatus).to.be.equal(200) - expect(data).to.be.a('string') - const tokenResponse = JSON.parse(data) - const { token } = tokenResponse + const jwtToken = auth.getJWTToken(wallet.address, Date.now()) + await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) const result = await auth.validateAuthenticationOrToken( wallet.address, undefined, - token + jwtToken ) expect(result.valid).to.be.equal(true) }) @@ -78,47 +53,22 @@ describe('Auth Token Tests', () => { }) it('should respect token expiry', async () => { - const message = auth.getSignatureMessage() - const messageHash = getMessageHash(message) - const signature = await wallet.signMessage(messageHash) - - const tokenCreateResponse = await createTokenHandler.handle({ - command: 'createAuthToken', - address: wallet.address, - signature, - validUntil: Date.now() + 1000 - }) - const data: string = await streamToString(tokenCreateResponse.stream as Readable) - const { token } = JSON.parse(data) + const jwtToken = auth.getJWTToken(wallet.address, Date.now()) + await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) await new Promise((resolve) => setTimeout(resolve, 1500)) - const validationResult = await auth.validateToken(token) + const validationResult = await auth.validateToken(jwtToken) expect(validationResult).to.be.equal(null) }) it('should invalidate a token', async () => { - const message = auth.getSignatureMessage() - const messageHash = getMessageHash(message) - const signature = await wallet.signMessage(messageHash) - - const tokenCreateResponse = await createTokenHandler.handle({ - command: 'createAuthToken', - address: wallet.address, - signature - }) - const data: string = await streamToString(tokenCreateResponse.stream as Readable) - const { token } = JSON.parse(data) + const jwtToken = auth.getJWTToken(wallet.address, Date.now()) + await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) - const invalidateTokenResponse = await invalidateTokenHandler.handle({ - command: 'invalidateAuthToken', - address: wallet.address, - signature, - token - }) - expect(invalidateTokenResponse.status.httpStatus).to.be.equal(200) + await auth.invalidateToken(jwtToken) - const validationResult = await auth.validateToken(token) + const validationResult = await auth.validateToken(jwtToken) expect(validationResult).to.be.equal(null) }) }) From b131405020a4b976622fb9f6b98437553a81d7c0 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 12:44:00 +0300 Subject: [PATCH 11/33] use port 8000 tests --- src/test/integration/auth.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts index f17329790..2b99e430d 100644 --- a/src/test/integration/auth.test.ts +++ b/src/test/integration/auth.test.ts @@ -26,8 +26,9 @@ describe('Auth Token Integration Tests', () => { let previousConfiguration: OverrideEnvConfig[] const mockSupportedNetworks: RPCS = getMockSupportedNetworks() - const url = 'http://localhost:8001/api/services/auth' - const validateDdoUrl = 'http://localhost:8001/api/aquarius/assets/ddo/validate' + const url = 'http://localhost:8000/api/services/auth' + const validateDdoUrl = 'http://localhost:8000/api/aquarius/assets/ddo/validate' + before(async () => { previousConfiguration = await setupEnvironment( From d21dfdfbb3694d522ed850ea85b9134c307739b1 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 13:36:32 +0300 Subject: [PATCH 12/33] test with handler --- src/test/integration/auth.test.ts | 220 ++++++++++++++++-------------- 1 file changed, 116 insertions(+), 104 deletions(-) diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts index 2b99e430d..63485e890 100644 --- a/src/test/integration/auth.test.ts +++ b/src/test/integration/auth.test.ts @@ -1,4 +1,3 @@ -import { expect } from 'chai' import { JsonRpcProvider, Signer, Wallet } from 'ethers' import { Database } from '../../components/database/index.js' import { Auth } from '../../components/Auth/index.js' @@ -12,10 +11,14 @@ import { tearDownEnvironment, getMockSupportedNetworks } from '../utils/utils.js' -import { ENVIRONMENT_VARIABLES } from '../../utils/constants.js' +import { ENVIRONMENT_VARIABLES, PROTOCOL_COMMANDS } from '../../utils/constants.js' import { OceanNodeConfig } from '../../@types/OceanNode.js' import { RPCS } from '../../@types/blockchain.js' import axios from 'axios' +import { OceanNode } from '../../OceanNode.js' +import { CreateAuthTokenHandler } from '../../components/core/handler/authHandler.js' +import { streamToObject } from '../../utils/util.js' +import { Readable } from 'stream' describe('Auth Token Integration Tests', () => { let config: OceanNodeConfig @@ -24,6 +27,7 @@ describe('Auth Token Integration Tests', () => { let provider: JsonRpcProvider let consumerAccount: Signer let previousConfiguration: OverrideEnvConfig[] + let oceanNode: OceanNode const mockSupportedNetworks: RPCS = getMockSupportedNetworks() const url = 'http://localhost:8000/api/services/auth' @@ -42,6 +46,7 @@ describe('Auth Token Integration Tests', () => { config = await getConfiguration(true) database = await new Database(config.dbConfig) auth = new Auth(database.authToken) + oceanNode = await OceanNode.getInstance(config, database) provider = new JsonRpcProvider(mockSupportedNetworks['8996'].rpc) @@ -114,117 +119,124 @@ describe('Auth Token Integration Tests', () => { const messageHash = getMessageHash(message) const signature = await consumerAccount.signMessage(messageHash) - const createResponse = await axios.post(`${url}/token`, { - signature, - address: consumerAddress - }) - - expect(createResponse.status).to.equal(200) - expect(createResponse.data.token).to.be.a('string') - - const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) - expect(testEndpointResponse.status).to.equal(200) - }) - - it('should handle token expiry', async function () { - this.timeout(DEFAULT_TEST_TIMEOUT) - - const consumerAddress = await consumerAccount.getAddress() - const message = auth.getSignatureMessage() - const messageHash = getMessageHash(message) - const signature = await consumerAccount.signMessage(messageHash) - - // Create token with 1 second expiry - const validUntil = Date.now() + 1000 - const createResponse = await axios.post(`${url}/token`, { - signature, + const handlerResponse = await new CreateAuthTokenHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, address: consumerAddress, - validUntil + signature }) - expect(createResponse.status).to.equal(200) - // Wait for token to expire - await new Promise((resolve) => setTimeout(resolve, 2000)) + console.log({ handlerResponse }) - const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) - expect(testEndpointResponse.status).to.equal(401) - }) + const token = await streamToObject(handlerResponse.stream as Readable) - it('should invalidate token', async function () { - this.timeout(DEFAULT_TEST_TIMEOUT) + console.log({ token }) - const consumerAddress = await consumerAccount.getAddress() - const message = auth.getSignatureMessage() - const messageHash = getMessageHash(message) - const signature = await consumerAccount.signMessage(messageHash) - const createResponse = await axios.post(`${url}/token`, { - signature, - address: consumerAddress - }) - const { token } = createResponse.data - await axios.post(`${url}/token/invalidate`, { - signature, - address: consumerAddress, - token - }) - - const testEndpointResponse = await ddoValiationRequest(token) - expect(testEndpointResponse.status).to.equal(401) + // const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) + // expect(testEndpointResponse.status).to.equal(200) }) - describe('Error Cases', () => { - it('should handle invalid signatures', async function () { - this.timeout(DEFAULT_TEST_TIMEOUT) - - const consumerAddress = await consumerAccount.getAddress() - - try { - await axios.post(`${url}/token`, { - signature: '0xinvalid', - address: consumerAddress - }) - expect.fail('Should have thrown error for invalid signature') - } catch (error) { - expect(error.response.status).to.equal(400) - } - }) - - it('should handle invalid tokens', async function () { - this.timeout(DEFAULT_TEST_TIMEOUT) - - const testEndpointResponse = await ddoValiationRequest('invalid-token') - expect(testEndpointResponse.status).to.equal(401) - }) - - it('should handle missing parameters', async function () { - this.timeout(DEFAULT_TEST_TIMEOUT) - - // Missing signature - try { - await axios.post(`${url}/token`, { - address: await consumerAccount.getAddress() - }) - expect.fail('Should have thrown error for missing signature') - } catch (error) { - expect(error.response.status).to.equal(400) - } - - // Missing address - try { - const message = auth.getSignatureMessage() - const messageHash = getMessageHash(message) - const signature = await consumerAccount.signMessage(messageHash) - - await axios.post(`${url}/token`, { - signature - }) - expect.fail('Should have thrown error for missing address') - } catch (error) { - expect(error.response.status).to.equal(400) - } - }) - }) + // it('should handle token expiry', async function () { + // this.timeout(DEFAULT_TEST_TIMEOUT) + + // const consumerAddress = await consumerAccount.getAddress() + // const message = auth.getSignatureMessage() + // const messageHash = getMessageHash(message) + // const signature = await consumerAccount.signMessage(messageHash) + + // // Create token with 1 second expiry + // const validUntil = Date.now() + 1000 + // const createResponse = await axios.post(`${url}/token`, { + // signature, + // address: consumerAddress, + // validUntil + // }) + // expect(createResponse.status).to.equal(200) + + // // Wait for token to expire + // await new Promise((resolve) => setTimeout(resolve, 2000)) + + // const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) + // expect(testEndpointResponse.status).to.equal(401) + // }) + + // it('should invalidate token', async function () { + // this.timeout(DEFAULT_TEST_TIMEOUT) + + // const consumerAddress = await consumerAccount.getAddress() + // const message = auth.getSignatureMessage() + // const messageHash = getMessageHash(message) + // const signature = await consumerAccount.signMessage(messageHash) + + // const createResponse = await axios.post(`${url}/token`, { + // signature, + // address: consumerAddress + // }) + // const { token } = createResponse.data + + // await axios.post(`${url}/token/invalidate`, { + // signature, + // address: consumerAddress, + // token + // }) + + // const testEndpointResponse = await ddoValiationRequest(token) + // expect(testEndpointResponse.status).to.equal(401) + // }) + + // describe('Error Cases', () => { + // it('should handle invalid signatures', async function () { + // this.timeout(DEFAULT_TEST_TIMEOUT) + + // const consumerAddress = await consumerAccount.getAddress() + + // try { + // await axios.post(`${url}/token`, { + // signature: '0xinvalid', + // address: consumerAddress + // }) + // expect.fail('Should have thrown error for invalid signature') + // } catch (error) { + // expect(error.response.status).to.equal(400) + // } + // }) + + // it('should handle invalid tokens', async function () { + // this.timeout(DEFAULT_TEST_TIMEOUT) + + // const testEndpointResponse = await ddoValiationRequest('invalid-token') + // expect(testEndpointResponse.status).to.equal(401) + // }) + + // it('should handle missing parameters', async function () { + // this.timeout(DEFAULT_TEST_TIMEOUT) + + // // Missing signature + // try { + // await axios.post(`${url}/token`, { + // address: await consumerAccount.getAddress() + // }) + // expect.fail('Should have thrown error for missing signature') + // } catch (error) { + // expect(error.response.status).to.equal(400) + // } + + // // Missing address + // try { + // const message = auth.getSignatureMessage() + // const messageHash = getMessageHash(message) + // const signature = await consumerAccount.signMessage(messageHash) + + // await axios.post(`${url}/token`, { + // signature + // }) + // expect.fail('Should have thrown error for missing address') + // } catch (error) { + // expect(error.response.status).to.equal(400) + // } + // }) + // }) + // }) }) }) From 36d2cd165fd0795646316c02a5806b9e32ae4dbc Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 13:44:24 +0300 Subject: [PATCH 13/33] revert aquarius --- src/components/httpRoutes/aquarius.ts | 69 ++++++++++++--------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/src/components/httpRoutes/aquarius.ts b/src/components/httpRoutes/aquarius.ts index 7a1d51d81..e95171373 100644 --- a/src/components/httpRoutes/aquarius.ts +++ b/src/components/httpRoutes/aquarius.ts @@ -10,7 +10,6 @@ import { QueryCommand } from '../../@types/commands.js' import { DatabaseFactory } from '../database/DatabaseFactory.js' import { SearchQuery } from '../../@types/DDO/SearchQuery.js' import { getConfiguration } from '../../utils/index.js' -import { validateAuthToken } from './middleware/authMiddleware.js' export const aquariusRoutes = express.Router() @@ -133,47 +132,41 @@ aquariusRoutes.get(`${AQUARIUS_API_BASE_PATH}/state/ddo`, async (req, res) => { } }) -aquariusRoutes.post( - `${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, - validateAuthToken, - async (req, res) => { - const node = req.oceanNode - try { - if (!req.body) { - res.status(400).send('Missing DDO object') - return - } +aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req, res) => { + const node = req.oceanNode + try { + if (!req.body) { + res.status(400).send('Missing DDO object') + return + } - const requestBody = JSON.parse(req.body) - const { publisherAddress, nonce, signature } = requestBody + const requestBody = JSON.parse(req.body) + const { publisherAddress, nonce, signature } = requestBody - // This is for backward compatibility with the old way of sending the DDO - const ddo = requestBody.ddo || JSON.parse(req.body) + // This is for backward compatibility with the old way of sending the DDO + const ddo = requestBody.ddo || JSON.parse(req.body) - if (!ddo.version) { - res.status(400).send('Missing DDO version') - return - } + if (!ddo.version) { + res.status(400).send('Missing DDO version') + return + } - const result = await new ValidateDDOHandler(node).handle({ - ddo, - publisherAddress, - nonce, - signature, - command: PROTOCOL_COMMANDS.VALIDATE_DDO - }) + const result = await new ValidateDDOHandler(node).handle({ + ddo, + publisherAddress, + nonce, + signature, + command: PROTOCOL_COMMANDS.VALIDATE_DDO + }) - if (result.stream) { - const validationResult = JSON.parse( - await streamToString(result.stream as Readable) - ) - res.json(validationResult) - } else { - res.status(result.status.httpStatus).send(result.status.error) - } - } catch (error) { - HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) - res.status(500).send('Internal Server Error') + if (result.stream) { + const validationResult = JSON.parse(await streamToString(result.stream as Readable)) + res.json(validationResult) + } else { + res.status(result.status.httpStatus).send(result.status.error) } + } catch (error) { + HTTP_LOGGER.log(LOG_LEVELS_STR.LEVEL_ERROR, `Error: ${error}`) + res.status(500).send('Internal Server Error') } -) +}) From a5625456d12db30d0a04608f50087c1552e9c4b5 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 14:55:39 +0300 Subject: [PATCH 14/33] decorator used for valdiation --- src/@types/commands.ts | 1 + src/components/Auth/index.ts | 36 ++- src/components/core/handler/authHandler.ts | 2 +- src/components/core/handler/ddoHandler.ts | 2 + .../httpRoutes/middleware/authMiddleware.ts | 39 --- src/test/integration/auth.test.ts | 283 ++++++++---------- src/test/unit/auth/token.test.ts | 23 +- .../decorators/validate-token.decorator.ts | 44 +++ 8 files changed, 216 insertions(+), 214 deletions(-) delete mode 100644 src/components/httpRoutes/middleware/authMiddleware.ts create mode 100644 src/utils/decorators/validate-token.decorator.ts diff --git a/src/@types/commands.ts b/src/@types/commands.ts index 74e519d0e..b435af230 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -19,6 +19,7 @@ import { export interface Command { command: string // command name node?: string // if not present it means current node + authorization?: string } export interface GetP2PPeerCommand extends Command { diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index f730ef64b..c525990b9 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -65,21 +65,37 @@ export class Auth { return tokenEntry } - async validateAuthenticationOrToken( - address: string, - signature?: string, - token?: string, + /** + * Validates the authentication or token + * You need to provider either a token or an address, signature and message + * @param {string} token - The token to validate + * @param {string} address - The address to validate + * @param {string} signature - The signature to validate + * @param {string} message - The message to validate + * @returns The validation result + */ + async validateAuthenticationOrToken({ + token, + address, + signature, + message + }: { + token?: string + address?: string + signature?: string message?: string - ): Promise { + }): Promise { try { if (token) { const authToken = await this.validateToken(token) - if (authToken && authToken.address.toLowerCase() === address.toLowerCase()) { + if (authToken) { return { valid: true, error: '' } } + + return { valid: false, error: 'Invalid token' } } - if (signature && message) { + if (signature && message && address) { const messageHashBytes = getMessageHash(message) const isValid = await verifyMessage(messageHashBytes, address, signature) @@ -88,7 +104,11 @@ export class Auth { } } - return { valid: false, error: 'Invalid authentication' } + return { + valid: false, + error: + 'Invalid authentication, you need to provide either a token or an address, signature and message' + } } catch (e) { return { valid: false, error: `Error during authentication validation: ${e}` } } diff --git a/src/components/core/handler/authHandler.ts b/src/components/core/handler/authHandler.ts index 4278dca8f..cb7c70643 100644 --- a/src/components/core/handler/authHandler.ts +++ b/src/components/core/handler/authHandler.ts @@ -38,7 +38,7 @@ export class CreateAuthTokenHandler extends CommandHandler { if (!isValid) { return { stream: null, - status: { httpStatus: 400, error: 'Invalid signature' } + status: { httpStatus: 401, error: 'Invalid signature' } } } diff --git a/src/components/core/handler/ddoHandler.ts b/src/components/core/handler/ddoHandler.ts index 8e2a2f8cf..f7ec154c3 100644 --- a/src/components/core/handler/ddoHandler.ts +++ b/src/components/core/handler/ddoHandler.ts @@ -40,6 +40,7 @@ import { import { deleteIndexedMetadataIfExists, validateDDOHash } from '../../../utils/asset.js' import { Asset, DDO, DDOManager } from '@oceanprotocol/ddo-js' import { checkCredentialOnAccessList } from '../../../utils/credentials.js' +import { ValidateTokenOrSignature } from '../../../utils/decorators/validate-token.decorator.js' const MAX_NUM_PROVIDERS = 5 // after 60 seconds it returns whatever info we have available @@ -800,6 +801,7 @@ export class ValidateDDOHandler extends CommandHandler { return validation } + @ValidateTokenOrSignature() async handle(task: ValidateDDOCommand): Promise { const configuration = await getConfiguration() const validationResponse = await this.verifyParamsAndRateLimits(task) diff --git a/src/components/httpRoutes/middleware/authMiddleware.ts b/src/components/httpRoutes/middleware/authMiddleware.ts deleted file mode 100644 index 7e6f711e0..000000000 --- a/src/components/httpRoutes/middleware/authMiddleware.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Request, Response, NextFunction } from 'express' -import { HTTP_LOGGER } from '../../../utils/logging/common.js' - -export interface AuthenticatedRequest extends Request { - authenticatedAddress?: string -} - -export async function validateAuthToken( - req: AuthenticatedRequest, - res: Response, - next: NextFunction -) { - const authHeader = req.headers.authorization - - if (!authHeader) { - // If no auth header is present, check for signature in the request - return next() - } - - const [scheme, token] = authHeader.split(' ') - - if (scheme !== 'Bearer' || !token) { - return res.status(401).json({ error: 'Invalid authorization header format' }) - } - - try { - const tokenEntry = await req.oceanNode.getAuth().validateToken(token) - - if (!tokenEntry) { - return res.status(401).json({ error: 'Invalid or expired token' }) - } - - req.authenticatedAddress = tokenEntry.address - next() - } catch (error) { - HTTP_LOGGER.error(`Error validating auth token: ${error}`) - res.status(500).json({ error: 'Internal server error' }) - } -} diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts index 63485e890..ef9675429 100644 --- a/src/test/integration/auth.test.ts +++ b/src/test/integration/auth.test.ts @@ -14,11 +14,15 @@ import { import { ENVIRONMENT_VARIABLES, PROTOCOL_COMMANDS } from '../../utils/constants.js' import { OceanNodeConfig } from '../../@types/OceanNode.js' import { RPCS } from '../../@types/blockchain.js' -import axios from 'axios' import { OceanNode } from '../../OceanNode.js' -import { CreateAuthTokenHandler } from '../../components/core/handler/authHandler.js' +import { + CreateAuthTokenHandler, + InvalidateAuthTokenHandler +} from '../../components/core/handler/authHandler.js' import { streamToObject } from '../../utils/util.js' import { Readable } from 'stream' +import { expect } from 'chai' +import { ValidateDDOHandler } from '../../components/core/handler/ddoHandler.js' describe('Auth Token Integration Tests', () => { let config: OceanNodeConfig @@ -30,9 +34,6 @@ describe('Auth Token Integration Tests', () => { let oceanNode: OceanNode const mockSupportedNetworks: RPCS = getMockSupportedNetworks() - const url = 'http://localhost:8000/api/services/auth' - const validateDdoUrl = 'http://localhost:8000/api/aquarius/assets/ddo/validate' - before(async () => { previousConfiguration = await setupEnvironment( @@ -61,47 +62,41 @@ describe('Auth Token Integration Tests', () => { const ddoValiationRequest = async (token: string) => { try { - const validateResponse = await axios.post( - `${validateDdoUrl}`, - { - ddo: { - id: 'did:op:f00896cc6f5f9f2c17be06dd28bd6be085e1406bb55274cbd2b65b7271e7b104', - '@context': [], - version: '4.1.0', - nftAddress: '0x3357cCd4e75536422b61F6aeda3ad38545b9b01F', - chainId: 11155111, - metadata: { - created: new Date().toISOString(), - updated: new Date().toISOString(), - type: 'dataset', - name: 'Test DDO', - description: 'Test DDO', - tags: [], - author: 'Test Author', - license: 'https://market.oceanprotocol.com/terms', - additionalInformation: { - termsAndConditions: true - } - }, - services: [ - { - id: 'ccb398c50d6abd5b456e8d7242bd856a1767a890b537c2f8c10ba8b8a10e6025', - type: 'compute', - files: '0x0', - datatokenAddress: '0x0Cf4BE72EAD0583deD382589aFcbF34F3E860Bdc', - serviceEndpoint: '', - timeout: 86400 - } - ] - } - }, - { - headers: { - Authorization: `Bearer ${token}`, - 'Content-Type': 'application/octet-stream' - } + const validateHandler = new ValidateDDOHandler(oceanNode) + const validateResponse = await validateHandler.handle({ + command: PROTOCOL_COMMANDS.VALIDATE_DDO, + authorization: token, + ddo: { + id: 'did:op:f00896cc6f5f9f2c17be06dd28bd6be085e1406bb55274cbd2b65b7271e7b104', + '@context': [], + version: '4.1.0', + nftAddress: '0x3357cCd4e75536422b61F6aeda3ad38545b9b01F', + chainId: 11155111, + metadata: { + created: new Date().toISOString(), + updated: new Date().toISOString(), + type: 'dataset', + name: 'Test DDO', + description: 'Test DDO', + tags: [], + author: 'Test Author', + license: 'https://market.oceanprotocol.com/terms', + additionalInformation: { + termsAndConditions: true + } + }, + services: [ + { + id: 'ccb398c50d6abd5b456e8d7242bd856a1767a890b537c2f8c10ba8b8a10e6025', + type: 'compute', + files: '0x0', + datatokenAddress: '0x0Cf4BE72EAD0583deD382589aFcbF34F3E860Bdc', + serviceEndpoint: '', + timeout: 86400 + } + ] } - ) + }) return validateResponse } catch (error) { @@ -125,118 +120,106 @@ describe('Auth Token Integration Tests', () => { signature }) - console.log({ handlerResponse }) + const token = await streamToObject(handlerResponse.stream as Readable) + const testEndpointResponse = await ddoValiationRequest(token.token) + expect(testEndpointResponse.status.httpStatus).to.equal(200) + }) + + it('should handle token expiry', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + const consumerAddress = await consumerAccount.getAddress() + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await consumerAccount.signMessage(messageHash) + + const validUntil = Date.now() + 1000 + const handlerResponse = await new CreateAuthTokenHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + address: consumerAddress, + signature, + validUntil + }) const token = await streamToObject(handlerResponse.stream as Readable) - console.log({ token }) + await new Promise((resolve) => setTimeout(resolve, 2000)) + const testEndpointResponse = await ddoValiationRequest(token.token) + expect(testEndpointResponse.status.httpStatus).to.equal(401) + }) + it('should invalidate token', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) - // const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) - // expect(testEndpointResponse.status).to.equal(200) + const consumerAddress = await consumerAccount.getAddress() + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await consumerAccount.signMessage(messageHash) + + const handlerResponse = await new CreateAuthTokenHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + address: consumerAddress, + signature + }) + + const token = await streamToObject(handlerResponse.stream as Readable) + + await new InvalidateAuthTokenHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.INVALIDATE_AUTH_TOKEN, + address: consumerAddress, + signature, + token: token.token + }) + + const testEndpointResponse = await ddoValiationRequest(token.token) + expect(testEndpointResponse.status.httpStatus).to.equal(401) }) - // it('should handle token expiry', async function () { - // this.timeout(DEFAULT_TEST_TIMEOUT) - - // const consumerAddress = await consumerAccount.getAddress() - // const message = auth.getSignatureMessage() - // const messageHash = getMessageHash(message) - // const signature = await consumerAccount.signMessage(messageHash) - - // // Create token with 1 second expiry - // const validUntil = Date.now() + 1000 - // const createResponse = await axios.post(`${url}/token`, { - // signature, - // address: consumerAddress, - // validUntil - // }) - // expect(createResponse.status).to.equal(200) - - // // Wait for token to expire - // await new Promise((resolve) => setTimeout(resolve, 2000)) - - // const testEndpointResponse = await ddoValiationRequest(createResponse.data.token) - // expect(testEndpointResponse.status).to.equal(401) - // }) - - // it('should invalidate token', async function () { - // this.timeout(DEFAULT_TEST_TIMEOUT) - - // const consumerAddress = await consumerAccount.getAddress() - // const message = auth.getSignatureMessage() - // const messageHash = getMessageHash(message) - // const signature = await consumerAccount.signMessage(messageHash) - - // const createResponse = await axios.post(`${url}/token`, { - // signature, - // address: consumerAddress - // }) - // const { token } = createResponse.data - - // await axios.post(`${url}/token/invalidate`, { - // signature, - // address: consumerAddress, - // token - // }) - - // const testEndpointResponse = await ddoValiationRequest(token) - // expect(testEndpointResponse.status).to.equal(401) - // }) - - // describe('Error Cases', () => { - // it('should handle invalid signatures', async function () { - // this.timeout(DEFAULT_TEST_TIMEOUT) - - // const consumerAddress = await consumerAccount.getAddress() - - // try { - // await axios.post(`${url}/token`, { - // signature: '0xinvalid', - // address: consumerAddress - // }) - // expect.fail('Should have thrown error for invalid signature') - // } catch (error) { - // expect(error.response.status).to.equal(400) - // } - // }) - - // it('should handle invalid tokens', async function () { - // this.timeout(DEFAULT_TEST_TIMEOUT) - - // const testEndpointResponse = await ddoValiationRequest('invalid-token') - // expect(testEndpointResponse.status).to.equal(401) - // }) - - // it('should handle missing parameters', async function () { - // this.timeout(DEFAULT_TEST_TIMEOUT) - - // // Missing signature - // try { - // await axios.post(`${url}/token`, { - // address: await consumerAccount.getAddress() - // }) - // expect.fail('Should have thrown error for missing signature') - // } catch (error) { - // expect(error.response.status).to.equal(400) - // } - - // // Missing address - // try { - // const message = auth.getSignatureMessage() - // const messageHash = getMessageHash(message) - // const signature = await consumerAccount.signMessage(messageHash) - - // await axios.post(`${url}/token`, { - // signature - // }) - // expect.fail('Should have thrown error for missing address') - // } catch (error) { - // expect(error.response.status).to.equal(400) - // } - // }) - // }) - // }) + describe('Error Cases', () => { + it('should handle invalid signatures', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + const consumerAddress = await consumerAccount.getAddress() + + const response = await new CreateAuthTokenHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + address: consumerAddress, + signature: '0xinvalid' + }) + expect(response.status.httpStatus).to.equal(401) + }) + + it('should handle invalid tokens', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + const testEndpointResponse = await ddoValiationRequest('invalid-token') + expect(testEndpointResponse.status.httpStatus).to.equal(401) + }) + + it('should handle missing parameters', async function () { + this.timeout(DEFAULT_TEST_TIMEOUT) + + // Missing signature + const response = await new CreateAuthTokenHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + address: await consumerAccount.getAddress(), + signature: undefined + }) + expect(response.status.httpStatus).to.equal(400) + + // Missing address + const message = auth.getSignatureMessage() + const messageHash = getMessageHash(message) + const signature = await consumerAccount.signMessage(messageHash) + + const response2 = await new CreateAuthTokenHandler(oceanNode).handle({ + command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, + address: undefined, + signature + }) + expect(response2.status.httpStatus).to.equal(400) + }) + }) }) }) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index 7e61a2150..0d8988085 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -22,20 +22,12 @@ describe('Auth Token Tests', () => { const jwtToken = auth.getJWTToken(wallet.address, Date.now()) await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) - const result = await auth.validateAuthenticationOrToken( - wallet.address, - undefined, - jwtToken - ) + const result = await auth.validateAuthenticationOrToken({ token: jwtToken }) expect(result.valid).to.be.equal(true) }) it('should fail validation with invalid token', async () => { - const result = await auth.validateAuthenticationOrToken( - wallet.address, - undefined, - 'invalid-token' - ) + const result = await auth.validateAuthenticationOrToken({ token: 'invalid-token' }) expect(result.valid).to.be.equal(false) }) @@ -43,12 +35,11 @@ describe('Auth Token Tests', () => { const message = 'Test message' const invalidSignature = '0x' + '0'.repeat(130) - const result = await auth.validateAuthenticationOrToken( - wallet.address, - invalidSignature, - undefined, - message - ) + const result = await auth.validateAuthenticationOrToken({ + signature: invalidSignature, + message, + address: wallet.address + }) expect(result.valid).to.be.equal(false) }) diff --git a/src/utils/decorators/validate-token.decorator.ts b/src/utils/decorators/validate-token.decorator.ts new file mode 100644 index 000000000..83316405e --- /dev/null +++ b/src/utils/decorators/validate-token.decorator.ts @@ -0,0 +1,44 @@ +import { P2PCommandResponse } from '../../@types' + +export function ValidateTokenOrSignature() { + return function ( + _target: Object, + _propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor + ): TypedPropertyDescriptor { + const originalMethod = descriptor.value + + descriptor.value = async function (...args: any[]): Promise { + const task = args[0] + const { authorization, signature, message, address } = task + const jwt = authorization?.includes('Bearer') + ? authorization.split(' ')[1] + : authorization + const oceanNode = this.getOceanNode() + + const auth = oceanNode.getAuth() + const isAuthRequestValid = await auth.validateAuthenticationOrToken({ + token: jwt, + signature, + message, + address + }) + if (!isAuthRequestValid.valid) { + console.log( + `Error validating token or signature while executing command: ${task.command}` + ) + return { + status: { + httpStatus: 401, + error: 'Invalid signature' + }, + stream: null + } + } + + return await originalMethod.apply(this, args) + } + + return descriptor + } +} From b9d0ffa5037e2a072372fe3e09593c6f3931b0bb Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 15:37:51 +0300 Subject: [PATCH 15/33] fix validation tests --- src/@types/commands.ts | 1 + src/test/unit/indexer/validation.test.ts | 51 ++++++++++++------- .../decorators/validate-token.decorator.ts | 5 +- 3 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/@types/commands.ts b/src/@types/commands.ts index b435af230..7494c27b2 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -83,6 +83,7 @@ export interface ValidateDDOCommand extends Command { publisherAddress?: string nonce?: string signature?: string + message?: string } export interface StatusCommand extends Command { diff --git a/src/test/unit/indexer/validation.test.ts b/src/test/unit/indexer/validation.test.ts index 11a4b2b69..f09ae8aa6 100644 --- a/src/test/unit/indexer/validation.test.ts +++ b/src/test/unit/indexer/validation.test.ts @@ -1,6 +1,6 @@ import { DDOExample, ddov5, ddov7, ddoValidationSignature } from '../../data/ddo.js' import { getValidationSignature } from '../../../components/core/utils/validateDdoHandler.js' -import { ENVIRONMENT_VARIABLES } from '../../../utils/index.js' +import { ENVIRONMENT_VARIABLES, getConfiguration } from '../../../utils/index.js' import { expect } from 'chai' import { setupEnvironment, @@ -13,12 +13,16 @@ import { DDOManager, DDO } from '@oceanprotocol/ddo-js' import { ValidateDDOHandler } from '../../../components/core/handler/ddoHandler.js' import { OceanNode } from '../../../OceanNode.js' import { PROTOCOL_COMMANDS } from '../../../utils/constants.js' -import { ethers } from 'ethers' +import { ethers, Wallet } from 'ethers' import { RPCS } from '../../../@types/blockchain.js' - +import { Database } from '../../../components/database/index.js' +import { OceanNodeConfig } from '../../../@types/OceanNode.js' describe('Schema validation tests', () => { let envOverrides: OverrideEnvConfig[] const mockSupportedNetworks: RPCS = getMockSupportedNetworks() + let wallet: Wallet + let database: Database + let config: OceanNodeConfig before(async () => { envOverrides = buildEnvOverrideConfig( @@ -38,6 +42,10 @@ describe('Schema validation tests', () => { ] ) envOverrides = await setupEnvironment(null, envOverrides) + const privateKey = process.env.PRIVATE_KEY + wallet = new ethers.Wallet(privateKey) + config = await getConfiguration() + database = await new Database(config.dbConfig) }) after(() => { @@ -45,6 +53,18 @@ describe('Schema validation tests', () => { tearDownEnvironment(envOverrides) }) + const getWalletSignature = async (ddo: DDO, date: number) => { + const message = ddo.id + date + const messageHash = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + + const messageHashBytes = ethers.getBytes(messageHash) + const signature = await wallet.signMessage(messageHashBytes) + return signature + } + it('should pass the validation on version 4.1.0', async () => { const ddoInstance = DDOManager.getDDOClass(DDOExample) const validationResult = await ddoInstance.validate() @@ -104,7 +124,7 @@ describe('Schema validation tests', () => { }) it('should fail validation when signature is missing', async () => { - const node = OceanNode.getInstance() + const node = OceanNode.getInstance(config, database) const handler = new ValidateDDOHandler(node) const ddoInstance = DDOManager.getDDOClass(DDOExample) const task = { @@ -115,11 +135,11 @@ describe('Schema validation tests', () => { } const result = await handler.handle(task) - expect(result.status.httpStatus).to.equal(400) + expect(result.status.httpStatus).to.equal(401) }) it('should fail validation when signature is invalid', async () => { - const node = OceanNode.getInstance() + const node = OceanNode.getInstance(config, database) const handler = new ValidateDDOHandler(node) const ddoInstance = DDOManager.getDDOClass(DDOExample) const ddo: DDO = { @@ -135,31 +155,24 @@ describe('Schema validation tests', () => { const result = await handler.handle(task) - expect(result.status.httpStatus).to.equal(400) + expect(result.status.httpStatus).to.equal(401) }) it('should pass validation with valid signature', async () => { - const node = OceanNode.getInstance() + const node = OceanNode.getInstance(config, database) const handler = new ValidateDDOHandler(node) const ddoInstance = DDOManager.getDDOClass(ddoValidationSignature) const ddo = ddoInstance.getDDOData() as DDO - const privateKey = process.env.PRIVATE_KEY - const wallet = new ethers.Wallet(privateKey) - const nonce = Date.now().toString() - const message = ddo.id + nonce - const messageHash = ethers.solidityPackedKeccak256( - ['bytes'], - [ethers.hexlify(ethers.toUtf8Bytes(message))] - ) - const messageHashBytes = ethers.getBytes(messageHash) - const signature = await wallet.signMessage(messageHashBytes) + const date = Date.now() + const signature = await getWalletSignature(ddo, date) const task = { ddo, publisherAddress: await wallet.getAddress(), - nonce, + nonce: date.toString(), signature, + message: ddo.id + date, command: PROTOCOL_COMMANDS.VALIDATE_DDO } diff --git a/src/utils/decorators/validate-token.decorator.ts b/src/utils/decorators/validate-token.decorator.ts index 83316405e..7bf3e74cc 100644 --- a/src/utils/decorators/validate-token.decorator.ts +++ b/src/utils/decorators/validate-token.decorator.ts @@ -1,5 +1,7 @@ import { P2PCommandResponse } from '../../@types' +// This decorator validates the token or signature of the request +// You can use it by adding @ValidateTokenOrSignature above the handler method export function ValidateTokenOrSignature() { return function ( _target: Object, @@ -10,7 +12,8 @@ export function ValidateTokenOrSignature() { descriptor.value = async function (...args: any[]): Promise { const task = args[0] - const { authorization, signature, message, address } = task + const { authorization, signature, message } = task + const address = task.address || task.publisherAddress const jwt = authorization?.includes('Bearer') ? authorization.split(' ')[1] : authorization From e4d18eb7bfc91bb1e868fccc483d08fb5322696c Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 15:57:55 +0300 Subject: [PATCH 16/33] support for new node instance --- src/OceanNode.ts | 5 +++-- src/test/unit/indexer/validation.test.ts | 23 +++++++++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/OceanNode.ts b/src/OceanNode.ts index 8eeaada4b..9e7846c04 100644 --- a/src/OceanNode.ts +++ b/src/OceanNode.ts @@ -69,9 +69,10 @@ export class OceanNode { db?: Database, node?: OceanP2P, provider?: OceanProvider, - indexer?: OceanIndexer + indexer?: OceanIndexer, + newInstance: boolean = false ): OceanNode { - if (!OceanNode.instance) { + if (!OceanNode.instance || newInstance) { // prepare compute engines this.instance = new OceanNode(config, db, node, provider, indexer) } diff --git a/src/test/unit/indexer/validation.test.ts b/src/test/unit/indexer/validation.test.ts index f09ae8aa6..78c954a64 100644 --- a/src/test/unit/indexer/validation.test.ts +++ b/src/test/unit/indexer/validation.test.ts @@ -18,11 +18,14 @@ import { RPCS } from '../../../@types/blockchain.js' import { Database } from '../../../components/database/index.js' import { OceanNodeConfig } from '../../../@types/OceanNode.js' describe('Schema validation tests', () => { - let envOverrides: OverrideEnvConfig[] + const privateKey = process.env.PRIVATE_KEY const mockSupportedNetworks: RPCS = getMockSupportedNetworks() + + let envOverrides: OverrideEnvConfig[] let wallet: Wallet let database: Database let config: OceanNodeConfig + let oceanNode: OceanNode before(async () => { envOverrides = buildEnvOverrideConfig( @@ -42,10 +45,17 @@ describe('Schema validation tests', () => { ] ) envOverrides = await setupEnvironment(null, envOverrides) - const privateKey = process.env.PRIVATE_KEY wallet = new ethers.Wallet(privateKey) config = await getConfiguration() database = await new Database(config.dbConfig) + oceanNode = await OceanNode.getInstance( + config, + database, + undefined, + undefined, + undefined, + true + ) }) after(() => { @@ -124,8 +134,7 @@ describe('Schema validation tests', () => { }) it('should fail validation when signature is missing', async () => { - const node = OceanNode.getInstance(config, database) - const handler = new ValidateDDOHandler(node) + const handler = new ValidateDDOHandler(oceanNode) const ddoInstance = DDOManager.getDDOClass(DDOExample) const task = { ddo: ddoInstance.getDDOData() as DDO, @@ -139,8 +148,7 @@ describe('Schema validation tests', () => { }) it('should fail validation when signature is invalid', async () => { - const node = OceanNode.getInstance(config, database) - const handler = new ValidateDDOHandler(node) + const handler = new ValidateDDOHandler(oceanNode) const ddoInstance = DDOManager.getDDOClass(DDOExample) const ddo: DDO = { ...(ddoInstance.getDDOData() as DDO) @@ -159,8 +167,7 @@ describe('Schema validation tests', () => { }) it('should pass validation with valid signature', async () => { - const node = OceanNode.getInstance(config, database) - const handler = new ValidateDDOHandler(node) + const handler = new ValidateDDOHandler(oceanNode) const ddoInstance = DDOManager.getDDOClass(ddoValidationSignature) const ddo = ddoInstance.getDDOData() as DDO From 5c0c0e488749caabda480833f2bdde121b8d1e5f Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 4 Jun 2025 16:19:11 +0300 Subject: [PATCH 17/33] force refresh node --- src/test/integration/compute.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/integration/compute.test.ts b/src/test/integration/compute.test.ts index 4dd35103c..2aab6d135 100644 --- a/src/test/integration/compute.test.ts +++ b/src/test/integration/compute.test.ts @@ -146,7 +146,7 @@ describe('Compute', () => { ) config = await getConfiguration(true) dbconn = await new Database(config.dbConfig) - oceanNode = await OceanNode.getInstance(config, dbconn, null, null, null) + oceanNode = await OceanNode.getInstance(config, dbconn, null, null, null, true) indexer = new OceanIndexer(dbconn, config.indexingNetworks) oceanNode.addIndexer(indexer) oceanNode.addC2DEngines() From 2e891dac77172bef2cce16795da8eefd8e9f43e9 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 09:14:30 +0300 Subject: [PATCH 18/33] new instance test --- src/test/integration/operationsDashboard.test.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/test/integration/operationsDashboard.test.ts b/src/test/integration/operationsDashboard.test.ts index c382e0f8c..69b690a37 100644 --- a/src/test/integration/operationsDashboard.test.ts +++ b/src/test/integration/operationsDashboard.test.ts @@ -106,7 +106,14 @@ describe('Should test admin operations', () => { config = await getConfiguration(true) // Force reload the configuration dbconn = await new Database(config.dbConfig) - oceanNode = await OceanNode.getInstance(config, dbconn) + oceanNode = await OceanNode.getInstance( + config, + dbconn, + undefined, + undefined, + undefined, + true + ) indexer = new OceanIndexer(dbconn, config.indexingNetworks) oceanNode.addIndexer(indexer) }) From 8a4a9972a7994b401180f94e55823600f7e6f194 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 09:43:18 +0300 Subject: [PATCH 19/33] fix bad enum --- src/@types/commands.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/@types/commands.ts b/src/@types/commands.ts index 7494c27b2..b4c2feec7 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -252,8 +252,8 @@ export interface JobStatus { hash: string } export enum IndexingCommand { - STOP_THREAD = 'start', - START_THREAD = 'stop' + STOP_THREAD = 'stop', + START_THREAD = 'start' } export interface StartStopIndexingCommand extends AdminCommand { chainId?: number From 0fe8395dd038458f8782130d7fb8c002c84b2549 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 10:03:30 +0300 Subject: [PATCH 20/33] force refresh handler --- src/@types/commands.ts | 4 ++-- src/components/core/handler/coreHandlersRegistry.ts | 7 +++++-- src/test/integration/operationsDashboard.test.ts | 13 ++++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/@types/commands.ts b/src/@types/commands.ts index b4c2feec7..7494c27b2 100644 --- a/src/@types/commands.ts +++ b/src/@types/commands.ts @@ -252,8 +252,8 @@ export interface JobStatus { hash: string } export enum IndexingCommand { - STOP_THREAD = 'stop', - START_THREAD = 'start' + STOP_THREAD = 'start', + START_THREAD = 'stop' } export interface StartStopIndexingCommand extends AdminCommand { chainId?: number diff --git a/src/components/core/handler/coreHandlersRegistry.ts b/src/components/core/handler/coreHandlersRegistry.ts index ffa8a6103..9f76a6b60 100644 --- a/src/components/core/handler/coreHandlersRegistry.ts +++ b/src/components/core/handler/coreHandlersRegistry.ts @@ -161,8 +161,11 @@ export class CoreHandlersRegistry { ) } - public static getInstance(node: OceanNode): CoreHandlersRegistry { - if (!CoreHandlersRegistry.instance) { + public static getInstance( + node: OceanNode, + newInstance: boolean = false + ): CoreHandlersRegistry { + if (!CoreHandlersRegistry.instance || newInstance) { this.instance = new CoreHandlersRegistry(node) } return this.instance diff --git a/src/test/integration/operationsDashboard.test.ts b/src/test/integration/operationsDashboard.test.ts index 69b690a37..3c402cd2d 100644 --- a/src/test/integration/operationsDashboard.test.ts +++ b/src/test/integration/operationsDashboard.test.ts @@ -106,14 +106,7 @@ describe('Should test admin operations', () => { config = await getConfiguration(true) // Force reload the configuration dbconn = await new Database(config.dbConfig) - oceanNode = await OceanNode.getInstance( - config, - dbconn, - undefined, - undefined, - undefined, - true - ) + oceanNode = await OceanNode.getInstance(config, dbconn) indexer = new OceanIndexer(dbconn, config.indexingNetworks) oceanNode.addIndexer(indexer) }) @@ -359,7 +352,8 @@ describe('Should test admin operations', () => { // ----------------------------------------- // IndexingThreadHandler const indexingHandler: IndexingThreadHandler = CoreHandlersRegistry.getInstance( - oceanNode + oceanNode, + true ).getHandler(PROTOCOL_COMMANDS.HANDLE_INDEXING_THREAD) as IndexingThreadHandler const signature = await getSignature(expiryTimestamp.toString()) @@ -386,6 +380,7 @@ describe('Should test admin operations', () => { // should exist a running thread for this network atm const response = await indexingHandler.handle(indexingStopCommand) + console.log({ responseStoppingThread: response }) assert(response.stream, 'Failed to get stream when stoping thread') expect(response.status.httpStatus).to.be.equal(200) From c2624574bcb7fee37a2e0445f175df2410859a9d Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 10:43:16 +0300 Subject: [PATCH 21/33] cli tests paid compute --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f68fa16aa..325f7caad 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -281,7 +281,7 @@ jobs: with: repository: 'oceanprotocol/ocean-cli' path: 'ocean-cli' - ref: 'main' + ref: 'feature/paid-compute' - name: Setup Ocean CLI working-directory: ${{ github.workspace }}/ocean-cli run: | From eb1483c2c2cf81a654a094e55673a4710d276e86 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 11:20:21 +0300 Subject: [PATCH 22/33] revert main cli --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 325f7caad..f68fa16aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -281,7 +281,7 @@ jobs: with: repository: 'oceanprotocol/ocean-cli' path: 'ocean-cli' - ref: 'feature/paid-compute' + ref: 'main' - name: Setup Ocean CLI working-directory: ${{ github.workspace }}/ocean-cli run: | From 1c00310672de460d9823a00dc37838d30add6351 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 11:22:50 +0300 Subject: [PATCH 23/33] specify message in handler --- src/components/httpRoutes/aquarius.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/httpRoutes/aquarius.ts b/src/components/httpRoutes/aquarius.ts index e95171373..7ff15f8e3 100644 --- a/src/components/httpRoutes/aquarius.ts +++ b/src/components/httpRoutes/aquarius.ts @@ -156,6 +156,7 @@ aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req, publisherAddress, nonce, signature, + message: ddo.id + nonce, command: PROTOCOL_COMMANDS.VALIDATE_DDO }) From 54926c2f271b76f7172e6d72d1482f56885147da Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 11:43:03 +0300 Subject: [PATCH 24/33] remove duplicate routes --- src/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index 398fe1811..603abedbd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,6 @@ import cors from 'cors' import { scheduleCronJobs } from './utils/cronjobs/scheduleCronJobs.js' import { requestValidator } from './components/httpRoutes/requestValidator.js' import { hasValidDBConfiguration } from './utils/database.js' -import { authRoutes } from './components/httpRoutes/auth.js' const app: Express = express() @@ -170,9 +169,6 @@ if (config.hasHttp) { next() }) - // Add auth routes before the main routes - app.use(authRoutes) - // Integrate static file serving middleware app.use(removeExtraSlashes) app.use('/', httpRoutes) From f0d2d6608a957ca8b78e32d9703b066db3667503 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 16:21:15 +0300 Subject: [PATCH 25/33] implement skipValidation params --- src/components/core/handler/ddoHandler.ts | 38 ++++--------------- .../decorators/validate-token.decorator.ts | 26 ++++++++++--- 2 files changed, 27 insertions(+), 37 deletions(-) diff --git a/src/components/core/handler/ddoHandler.ts b/src/components/core/handler/ddoHandler.ts index f7ec154c3..a5d30466e 100644 --- a/src/components/core/handler/ddoHandler.ts +++ b/src/components/core/handler/ddoHandler.ts @@ -791,6 +791,11 @@ export class FindDdoHandler extends CommandHandler { } } +export async function skipValidation(): Promise { + const configuration = await getConfiguration() + return configuration.validateUnsignedDDO +} + export class ValidateDDOHandler extends CommandHandler { validate(command: ValidateDDOCommand): ValidateParams { let validation = validateCommandParameters(command, ['ddo']) @@ -801,9 +806,9 @@ export class ValidateDDOHandler extends CommandHandler { return validation } - @ValidateTokenOrSignature() + // Skip validation if allowed by env variable + @ValidateTokenOrSignature(skipValidation) async handle(task: ValidateDDOCommand): Promise { - const configuration = await getConfiguration() const validationResponse = await this.verifyParamsAndRateLimits(task) if (this.shouldDenyTaskHandling(validationResponse)) { return validationResponse @@ -812,35 +817,6 @@ export class ValidateDDOHandler extends CommandHandler { const ddoInstance = DDOManager.getDDOClass(task.ddo) const validation = await ddoInstance.validate() - const { ddo, publisherAddress, nonce, signature: signatureFromRequest } = task - if (configuration.validateUnsignedDDO === false) { - if (!publisherAddress || !nonce || !signatureFromRequest) { - return { - stream: null, - status: { - httpStatus: 400, - error: - 'A signature is required to validate a DDO, please provide a signed message with the publisher address, nonce and signature' - } - } - } - } - - if (publisherAddress && nonce && signatureFromRequest) { - const isValid = validateDdoSignedByPublisher( - ddo, - nonce, - signatureFromRequest, - publisherAddress - ) - if (!isValid) { - return { - stream: null, - status: { httpStatus: 400, error: 'Invalid signature' } - } - } - } - if (validation[0] === false) { CORE_LOGGER.logMessageWithEmoji( `Validation failed with error: ${validation[1]}`, diff --git a/src/utils/decorators/validate-token.decorator.ts b/src/utils/decorators/validate-token.decorator.ts index 7bf3e74cc..8339d5dd7 100644 --- a/src/utils/decorators/validate-token.decorator.ts +++ b/src/utils/decorators/validate-token.decorator.ts @@ -1,16 +1,30 @@ import { P2PCommandResponse } from '../../@types' -// This decorator validates the token or signature of the request -// You can use it by adding @ValidateTokenOrSignature above the handler method -export function ValidateTokenOrSignature() { +/** + * This decorator validates the token or signature of the request + * You can use it by adding @ValidateTokenOrSignature above the handler method + * @param skipValidation - If true, the validation will be skipped. You can also pass a function that returns a boolean. + */ +export function ValidateTokenOrSignature( + skipValidation?: boolean | (() => Promise) +) { return function ( _target: Object, _propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor - ): TypedPropertyDescriptor { + descriptor: TypedPropertyDescriptor<(...args: any[]) => Promise> + ) { const originalMethod = descriptor.value descriptor.value = async function (...args: any[]): Promise { + let shouldSkip = skipValidation + if (typeof skipValidation === 'function') { + shouldSkip = await skipValidation() + } + + if (shouldSkip) { + return originalMethod.apply(this, args) + } + const task = args[0] const { authorization, signature, message } = task const address = task.address || task.publisherAddress @@ -33,7 +47,7 @@ export function ValidateTokenOrSignature() { return { status: { httpStatus: 401, - error: 'Invalid signature' + error: 'Invalid token or signature' }, stream: null } From 7120eb17cbc1065dcc068e419678ec09713917f4 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Thu, 5 Jun 2025 16:27:40 +0300 Subject: [PATCH 26/33] override env tests --- src/test/integration/auth.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts index ef9675429..be5fe608b 100644 --- a/src/test/integration/auth.test.ts +++ b/src/test/integration/auth.test.ts @@ -39,8 +39,12 @@ describe('Auth Token Integration Tests', () => { previousConfiguration = await setupEnvironment( TEST_ENV_CONFIG_FILE, buildEnvOverrideConfig( - [ENVIRONMENT_VARIABLES.RPCS, ENVIRONMENT_VARIABLES.INDEXER_NETWORKS], - [JSON.stringify(mockSupportedNetworks), JSON.stringify([8996])] + [ + ENVIRONMENT_VARIABLES.RPCS, + ENVIRONMENT_VARIABLES.INDEXER_NETWORKS, + ENVIRONMENT_VARIABLES.VALIDATE_UNSIGNED_DDO + ], + [JSON.stringify(mockSupportedNetworks), JSON.stringify([8996]), 'false'] ) ) From 38e0101c01ce0cb23e667eabda89551723a3411a Mon Sep 17 00:00:00 2001 From: Razvan Giurgiu Date: Fri, 6 Jun 2025 11:07:28 +0000 Subject: [PATCH 27/33] add header in handler --- src/components/httpRoutes/aquarius.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/httpRoutes/aquarius.ts b/src/components/httpRoutes/aquarius.ts index 7ff15f8e3..ebaf87e7b 100644 --- a/src/components/httpRoutes/aquarius.ts +++ b/src/components/httpRoutes/aquarius.ts @@ -141,6 +141,7 @@ aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req, } const requestBody = JSON.parse(req.body) + const authorization = req.headers?.authorization const { publisherAddress, nonce, signature } = requestBody // This is for backward compatibility with the old way of sending the DDO @@ -154,6 +155,7 @@ aquariusRoutes.post(`${AQUARIUS_API_BASE_PATH}/assets/ddo/validate`, async (req, const result = await new ValidateDDOHandler(node).handle({ ddo, publisherAddress, + authorization, nonce, signature, message: ddo.id + nonce, From 987d1948cdc0eee30eec993e97915190ebb2368c Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Fri, 6 Jun 2025 14:13:54 +0300 Subject: [PATCH 28/33] add docker comput envs in ci --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f68fa16aa..e877f080e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -265,6 +265,7 @@ jobs: DB_TYPE: 'elasticsearch' MAX_REQ_PER_MINUTE: 320 MAX_CONNECTIONS_PER_MINUTE: 320 + DOCKER_COMPUTE_ENVIRONMENTS: '[{"socketPath":"/var/run/docker.sock","resources":[{"id":"disk","total":1000000000}],"storageExpiry":604800,"maxJobDuration":3600,"fees":{"8996":[{"prices":[{"id":"cpu","price":1}]}]},"free":{"maxJobDuration":60,"maxJobs":3,"resources":[{"id":"cpu","max":1},{"id":"ram","max":1000000000},{"id":"disk","max":1000000000}]}}]' - name: Check Ocean Node is running run: | for i in $(seq 1 90); do From 6bfb24840255120c24c3962eed3b65c829236148 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Tue, 10 Jun 2025 13:19:05 +0300 Subject: [PATCH 29/33] reorder priority --- src/components/Auth/index.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index c525990b9..2db05b653 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -86,15 +86,6 @@ export class Auth { message?: string }): Promise { try { - if (token) { - const authToken = await this.validateToken(token) - if (authToken) { - return { valid: true, error: '' } - } - - return { valid: false, error: 'Invalid token' } - } - if (signature && message && address) { const messageHashBytes = getMessageHash(message) const isValid = await verifyMessage(messageHashBytes, address, signature) @@ -104,6 +95,15 @@ export class Auth { } } + if (token) { + const authToken = await this.validateToken(token) + if (authToken) { + return { valid: true, error: '' } + } + + return { valid: false, error: 'Invalid token' } + } + return { valid: false, error: From 92e003abb83a57e10ecb3be034aae7096400441d Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Tue, 10 Jun 2025 17:06:52 +0300 Subject: [PATCH 30/33] add nonce --- src/components/Auth/index.ts | 40 +++++++++--------- src/components/core/handler/authHandler.ts | 48 +++++++++++++++------- src/components/httpRoutes/auth.ts | 6 ++- src/test/integration/auth.test.ts | 34 +++++++++++---- src/test/unit/auth/token.test.ts | 10 +++-- 5 files changed, 91 insertions(+), 47 deletions(-) diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index 2db05b653..a757cdd59 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -1,6 +1,7 @@ -import { getMessageHash, verifyMessage } from '../../utils/index.js' import { AuthToken, AuthTokenDatabase } from '../database/AuthTokenDatabase.js' import jwt from 'jsonwebtoken' +import { checkNonce, NonceResponse } from '../core/utils/nonceHandler.js' +import { OceanNode } from '../../OceanNode.js' export interface CommonValidation { valid: boolean @@ -10,26 +11,25 @@ export interface CommonValidation { export class Auth { private authTokenDatabase: AuthTokenDatabase private jwtSecret: string - private signatureMessage: string public constructor(authTokenDatabase: AuthTokenDatabase) { this.authTokenDatabase = authTokenDatabase this.jwtSecret = process.env.JWT_SECRET || 'ocean-node-secret' - this.signatureMessage = process.env.SIGNATURE_MESSAGE || 'token-auth' } public getJwtSecret(): string { return this.jwtSecret } - public getSignatureMessage(): string { - return this.signatureMessage + public getMessage(address: string, nonce: string): string { + return address + nonce } - getJWTToken(address: string, createdAt: number): string { + getJWTToken(address: string, nonce: string, createdAt: number): string { const jwtToken = jwt.sign( { address, + nonce, createdAt }, this.getJwtSecret() @@ -51,12 +51,6 @@ export class Auth { await this.authTokenDatabase.invalidateToken(jwtToken) } - async validateSignature(signature: string, address: string): Promise { - const messageHashBytes = getMessageHash(this.signatureMessage) - const isValid = await verifyMessage(messageHashBytes, address, signature) - return isValid - } - async validateToken(token: string): Promise { const tokenEntry = await this.authTokenDatabase.validateToken(token) if (!tokenEntry) { @@ -77,21 +71,29 @@ export class Auth { async validateAuthenticationOrToken({ token, address, + nonce, signature, message }: { token?: string address?: string + nonce?: string signature?: string message?: string }): Promise { try { - if (signature && message && address) { - const messageHashBytes = getMessageHash(message) - const isValid = await verifyMessage(messageHashBytes, address, signature) - - if (isValid) { - return { valid: true, error: '' } + if (signature && message && address && nonce) { + const oceanNode = OceanNode.getInstance() + const nonceCheckResult: NonceResponse = await checkNonce( + oceanNode.getDatabase().nonce, + address, + parseInt(nonce), + signature, + this.getMessage(address, nonce) + ) + + if (!nonceCheckResult.valid) { + return { valid: false, error: nonceCheckResult.error } } } @@ -107,7 +109,7 @@ export class Auth { return { valid: false, error: - 'Invalid authentication, you need to provide either a token or an address, signature and message' + 'Invalid authentication, you need to provide either a token or an address, signature, message and nonce' } } catch (e) { return { valid: false, error: `Error during authentication validation: ${e}` } diff --git a/src/components/core/handler/authHandler.ts b/src/components/core/handler/authHandler.ts index cb7c70643..6e7e5af89 100644 --- a/src/components/core/handler/authHandler.ts +++ b/src/components/core/handler/authHandler.ts @@ -7,16 +7,19 @@ import { import { ReadableString } from '../../P2P/handlers.js' import { Command } from '../../../@types/commands.js' import { Readable } from 'stream' +import { checkNonce, NonceResponse } from '../utils/nonceHandler.js' -export interface CreateAuthTokenCommand extends Command { +export interface AuthMessage { address: string + nonce: string signature: string +} + +export interface CreateAuthTokenCommand extends AuthMessage, Command { validUntil?: number | null } -export interface InvalidateAuthTokenCommand extends Command { - address: string - signature: string +export interface InvalidateAuthTokenCommand extends AuthMessage, Command { token: string } @@ -26,24 +29,34 @@ export class CreateAuthTokenHandler extends CommandHandler { } async handle(task: CreateAuthTokenCommand): Promise { + const { address, nonce, signature } = task + const nonceDb = this.getOceanNode().getDatabase().nonce + const auth = this.getOceanNode().getAuth() const validationResponse = await this.verifyParamsAndRateLimits(task) if (this.shouldDenyTaskHandling(validationResponse)) { return validationResponse } try { - const isValid = await this.getOceanNode() - .getAuth() - .validateSignature(task.signature, task.address) - if (!isValid) { + const nonceCheckResult: NonceResponse = await checkNonce( + nonceDb, + address, + parseInt(nonce), + signature, + auth.getMessage(address, nonce) + ) + + if (!nonceCheckResult.valid) { return { stream: null, - status: { httpStatus: 401, error: 'Invalid signature' } + status: { httpStatus: 401, error: nonceCheckResult.error } } } const createdAt = Date.now() - const jwtToken = this.getOceanNode().getAuth().getJWTToken(task.address, createdAt) + const jwtToken = this.getOceanNode() + .getAuth() + .getJWTToken(task.address, task.nonce, createdAt) await this.getOceanNode() .getAuth() @@ -68,15 +81,22 @@ export class InvalidateAuthTokenHandler extends CommandHandler { } async handle(task: InvalidateAuthTokenCommand): Promise { + const { address, nonce, signature, token } = task + const nonceDb = this.getOceanNode().getDatabase().nonce + const auth = this.getOceanNode().getAuth() const validationResponse = await this.verifyParamsAndRateLimits(task) if (this.shouldDenyTaskHandling(validationResponse)) { return validationResponse } try { - const isValid = await this.getOceanNode() - .getAuth() - .validateSignature(task.signature, task.address) + const isValid = await checkNonce( + nonceDb, + address, + parseInt(nonce), + signature, + auth.getMessage(address, nonce) + ) if (!isValid) { return { stream: null, @@ -84,7 +104,7 @@ export class InvalidateAuthTokenHandler extends CommandHandler { } } - await this.getOceanNode().getAuth().invalidateToken(task.token) + await this.getOceanNode().getAuth().invalidateToken(token) return { stream: new ReadableString(JSON.stringify({ success: true })), diff --git a/src/components/httpRoutes/auth.ts b/src/components/httpRoutes/auth.ts index f0c5979f7..ac2b0ef0a 100644 --- a/src/components/httpRoutes/auth.ts +++ b/src/components/httpRoutes/auth.ts @@ -15,7 +15,7 @@ authRoutes.post( express.json(), async (req, res) => { try { - const { signature, address, validUntil } = req.body + const { signature, address, nonce, validUntil } = req.body if (!signature || !address) { return res.status(400).json({ error: 'Missing required parameters' }) @@ -25,6 +25,7 @@ authRoutes.post( command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, signature, address, + nonce, validUntil }) @@ -48,7 +49,7 @@ authRoutes.post( express.json(), async (req, res) => { try { - const { signature, address, token } = req.body + const { signature, address, nonce, token } = req.body if (!signature || !address || !token) { return res.status(400).json({ error: 'Missing required parameters' }) @@ -58,6 +59,7 @@ authRoutes.post( command: PROTOCOL_COMMANDS.INVALIDATE_AUTH_TOKEN, signature, address, + nonce, token }) diff --git a/src/test/integration/auth.test.ts b/src/test/integration/auth.test.ts index be5fe608b..417247ca1 100644 --- a/src/test/integration/auth.test.ts +++ b/src/test/integration/auth.test.ts @@ -64,6 +64,10 @@ describe('Auth Token Integration Tests', () => { await tearDownEnvironment(previousConfiguration) }) + const getRandomNonce = () => { + return Date.now().toString() + } + const ddoValiationRequest = async (token: string) => { try { const validateHandler = new ValidateDDOHandler(oceanNode) @@ -114,14 +118,16 @@ describe('Auth Token Integration Tests', () => { this.timeout(DEFAULT_TEST_TIMEOUT) const consumerAddress = await consumerAccount.getAddress() - const message = auth.getSignatureMessage() + const nonce = getRandomNonce() + const message = auth.getMessage(consumerAddress, nonce) const messageHash = getMessageHash(message) const signature = await consumerAccount.signMessage(messageHash) const handlerResponse = await new CreateAuthTokenHandler(oceanNode).handle({ command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, address: consumerAddress, - signature + signature, + nonce }) const token = await streamToObject(handlerResponse.stream as Readable) @@ -133,7 +139,8 @@ describe('Auth Token Integration Tests', () => { this.timeout(DEFAULT_TEST_TIMEOUT) const consumerAddress = await consumerAccount.getAddress() - const message = auth.getSignatureMessage() + const nonce = getRandomNonce() + const message = auth.getMessage(consumerAddress, nonce) const messageHash = getMessageHash(message) const signature = await consumerAccount.signMessage(messageHash) @@ -142,6 +149,7 @@ describe('Auth Token Integration Tests', () => { command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, address: consumerAddress, signature, + nonce, validUntil }) @@ -157,22 +165,26 @@ describe('Auth Token Integration Tests', () => { this.timeout(DEFAULT_TEST_TIMEOUT) const consumerAddress = await consumerAccount.getAddress() - const message = auth.getSignatureMessage() + const nonce = getRandomNonce() + const message = auth.getMessage(consumerAddress, nonce) const messageHash = getMessageHash(message) const signature = await consumerAccount.signMessage(messageHash) const handlerResponse = await new CreateAuthTokenHandler(oceanNode).handle({ command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, address: consumerAddress, - signature + signature, + nonce }) const token = await streamToObject(handlerResponse.stream as Readable) + const newNonce = getRandomNonce() await new InvalidateAuthTokenHandler(oceanNode).handle({ command: PROTOCOL_COMMANDS.INVALIDATE_AUTH_TOKEN, address: consumerAddress, signature, + nonce: newNonce, token: token.token }) @@ -189,7 +201,8 @@ describe('Auth Token Integration Tests', () => { const response = await new CreateAuthTokenHandler(oceanNode).handle({ command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, address: consumerAddress, - signature: '0xinvalid' + signature: '0xinvalid', + nonce: getRandomNonce() }) expect(response.status.httpStatus).to.equal(401) }) @@ -208,19 +221,22 @@ describe('Auth Token Integration Tests', () => { const response = await new CreateAuthTokenHandler(oceanNode).handle({ command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, address: await consumerAccount.getAddress(), - signature: undefined + signature: undefined, + nonce: getRandomNonce() }) expect(response.status.httpStatus).to.equal(400) // Missing address - const message = auth.getSignatureMessage() + const nonce = getRandomNonce() + const message = auth.getMessage(await consumerAccount.getAddress(), nonce) const messageHash = getMessageHash(message) const signature = await consumerAccount.signMessage(messageHash) const response2 = await new CreateAuthTokenHandler(oceanNode).handle({ command: PROTOCOL_COMMANDS.CREATE_AUTH_TOKEN, address: undefined, - signature + signature, + nonce: getRandomNonce() }) expect(response2.status.httpStatus).to.equal(400) }) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index 0d8988085..d830a54f1 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -18,8 +18,12 @@ describe('Auth Token Tests', () => { auth = new Auth(authTokenDatabase) }) + const getRandomNonce = () => { + return Math.floor(Math.random() * 1000000).toString() + } + it('should create and validate a token', async () => { - const jwtToken = auth.getJWTToken(wallet.address, Date.now()) + const jwtToken = auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) const result = await auth.validateAuthenticationOrToken({ token: jwtToken }) @@ -44,7 +48,7 @@ describe('Auth Token Tests', () => { }) it('should respect token expiry', async () => { - const jwtToken = auth.getJWTToken(wallet.address, Date.now()) + const jwtToken = auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) await new Promise((resolve) => setTimeout(resolve, 1500)) @@ -54,7 +58,7 @@ describe('Auth Token Tests', () => { }) it('should invalidate a token', async () => { - const jwtToken = auth.getJWTToken(wallet.address, Date.now()) + const jwtToken = auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) await auth.invalidateToken(jwtToken) From d9c44c14017c81a61876fe8a7ba57ef0ebadba87 Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 11 Jun 2025 09:27:57 +0300 Subject: [PATCH 31/33] check nonce --- src/components/Auth/index.ts | 10 ++++--- src/test/unit/auth/token.test.ts | 3 +- src/test/unit/indexer/validation.test.ts | 30 ++++++++----------- .../decorators/validate-token.decorator.ts | 4 +-- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index a757cdd59..df8eea7a3 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -72,17 +72,15 @@ export class Auth { token, address, nonce, - signature, - message + signature }: { token?: string address?: string nonce?: string signature?: string - message?: string }): Promise { try { - if (signature && message && address && nonce) { + if (signature && address && nonce) { const oceanNode = OceanNode.getInstance() const nonceCheckResult: NonceResponse = await checkNonce( oceanNode.getDatabase().nonce, @@ -95,6 +93,10 @@ export class Auth { if (!nonceCheckResult.valid) { return { valid: false, error: nonceCheckResult.error } } + + if (nonceCheckResult.valid) { + return { valid: true, error: '' } + } } if (token) { diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index d830a54f1..dda31db39 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -36,12 +36,11 @@ describe('Auth Token Tests', () => { }) it('should fail validation with invalid signature', async () => { - const message = 'Test message' const invalidSignature = '0x' + '0'.repeat(130) const result = await auth.validateAuthenticationOrToken({ signature: invalidSignature, - message, + nonce: getRandomNonce(), address: wallet.address }) expect(result.valid).to.be.equal(false) diff --git a/src/test/unit/indexer/validation.test.ts b/src/test/unit/indexer/validation.test.ts index 78c954a64..aa7edbb21 100644 --- a/src/test/unit/indexer/validation.test.ts +++ b/src/test/unit/indexer/validation.test.ts @@ -63,18 +63,6 @@ describe('Schema validation tests', () => { tearDownEnvironment(envOverrides) }) - const getWalletSignature = async (ddo: DDO, date: number) => { - const message = ddo.id + date - const messageHash = ethers.solidityPackedKeccak256( - ['bytes'], - [ethers.hexlify(ethers.toUtf8Bytes(message))] - ) - - const messageHashBytes = ethers.getBytes(messageHash) - const signature = await wallet.signMessage(messageHashBytes) - return signature - } - it('should pass the validation on version 4.1.0', async () => { const ddoInstance = DDOManager.getDDOClass(DDOExample) const validationResult = await ddoInstance.validate() @@ -170,16 +158,22 @@ describe('Schema validation tests', () => { const handler = new ValidateDDOHandler(oceanNode) const ddoInstance = DDOManager.getDDOClass(ddoValidationSignature) const ddo = ddoInstance.getDDOData() as DDO - - const date = Date.now() - const signature = await getWalletSignature(ddo, date) + const auth = oceanNode.getAuth() + const publisherAddress = await wallet.getAddress() + const nonce = Date.now().toString() + const message = auth.getMessage(publisherAddress, nonce) + const messageHash = ethers.solidityPackedKeccak256( + ['bytes'], + [ethers.hexlify(ethers.toUtf8Bytes(message))] + ) + const messageHashBytes = ethers.toBeArray(messageHash) + const signature = await wallet.signMessage(messageHashBytes) const task = { ddo, - publisherAddress: await wallet.getAddress(), - nonce: date.toString(), + publisherAddress, + nonce, signature, - message: ddo.id + date, command: PROTOCOL_COMMANDS.VALIDATE_DDO } diff --git a/src/utils/decorators/validate-token.decorator.ts b/src/utils/decorators/validate-token.decorator.ts index 8339d5dd7..16b0b60a1 100644 --- a/src/utils/decorators/validate-token.decorator.ts +++ b/src/utils/decorators/validate-token.decorator.ts @@ -26,7 +26,7 @@ export function ValidateTokenOrSignature( } const task = args[0] - const { authorization, signature, message } = task + const { authorization, signature, nonce } = task const address = task.address || task.publisherAddress const jwt = authorization?.includes('Bearer') ? authorization.split(' ')[1] @@ -37,7 +37,7 @@ export function ValidateTokenOrSignature( const isAuthRequestValid = await auth.validateAuthenticationOrToken({ token: jwt, signature, - message, + nonce, address }) if (!isAuthRequestValid.valid) { From 797a0662084a147b37f9fd44136ea9d82038b202 Mon Sep 17 00:00:00 2001 From: Razvan Giurgiu Date: Wed, 11 Jun 2025 07:31:24 +0000 Subject: [PATCH 32/33] name routes --- src/components/httpRoutes/routeUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/components/httpRoutes/routeUtils.ts b/src/components/httpRoutes/routeUtils.ts index a889c95d3..7615e298c 100644 --- a/src/components/httpRoutes/routeUtils.ts +++ b/src/components/httpRoutes/routeUtils.ts @@ -189,6 +189,16 @@ routesNames.set('PolicyServerPassthrough', { method: 'post' }) +routesNames.set('generateAuthToken', { + path: `${SERVICES_API_BASE_PATH}/auth/token`, + method: 'post' +}) + +routesNames.set('invalidateAuthToken', { + path: `${SERVICES_API_BASE_PATH}/auth/token/invalidate`, + method: 'post' +}) + export function addMapping(path: any, layer: any) { if (layer.route) { layer.route.stack.forEach(addMapping.bind(null, path.concat(split(layer.route.path)))) From 3476f90e85972b6967162bb0e92e9f067a7810de Mon Sep 17 00:00:00 2001 From: giurgiur99 Date: Wed, 11 Jun 2025 16:51:38 +0300 Subject: [PATCH 33/33] env example and use config --- .env.example | 1 + docs/env.md | 1 + src/@types/OceanNode.ts | 1 + src/components/Auth/index.ts | 12 ++++++------ src/components/core/handler/authHandler.ts | 2 +- src/test/unit/auth/token.test.ts | 6 +++--- src/utils/config.ts | 3 ++- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.env.example b/.env.example index f1d59352d..16e6c68c7 100644 --- a/.env.example +++ b/.env.example @@ -38,6 +38,7 @@ export MAX_CHECKSUM_LENGTH= export LOG_LEVEL= export HTTP_API_PORT= export VALIDATE_UNSIGNED_DDO= +export JWT_SECRET= ## p2p diff --git a/docs/env.md b/docs/env.md index aba2718d6..a8b29df22 100644 --- a/docs/env.md +++ b/docs/env.md @@ -33,6 +33,7 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/ - `AUTHORIZED_PUBLISHERS`: Authorized list of publishers. If present, Node will only index assets published by the accounts in the list. Example: `"[\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"]"` - `AUTHORIZED_PUBLISHERS_LIST`: AccessList contract addresses (per chain). If present, Node will only index assets published by the accounts present on the given access lists. Example: `"{ \"8996\": [\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"] }"` - `VALIDATE_UNSIGNED_DDO`: If set to `false`, the node will not validate unsigned DDOs and will request a signed message with the publisher address, nonce and signature. Default is `true`. Example: `false` +- `JWT_SECRET`: Secret used to sign JWT tokens. Default is `ocean-node-secret`. Example: `"my-secret-jwt-token"` ## Payments diff --git a/src/@types/OceanNode.ts b/src/@types/OceanNode.ts index 8b5c2ecbe..55b4db5ff 100644 --- a/src/@types/OceanNode.ts +++ b/src/@types/OceanNode.ts @@ -113,6 +113,7 @@ export interface OceanNodeConfig { unsafeURLs?: string[] isBootstrap?: boolean validateUnsignedDDO?: boolean + jwtSecret?: string } export interface P2PStatusResponse { diff --git a/src/components/Auth/index.ts b/src/components/Auth/index.ts index df8eea7a3..fbc8f726f 100644 --- a/src/components/Auth/index.ts +++ b/src/components/Auth/index.ts @@ -2,6 +2,7 @@ import { AuthToken, AuthTokenDatabase } from '../database/AuthTokenDatabase.js' import jwt from 'jsonwebtoken' import { checkNonce, NonceResponse } from '../core/utils/nonceHandler.js' import { OceanNode } from '../../OceanNode.js' +import { getConfiguration } from '../../utils/index.js' export interface CommonValidation { valid: boolean @@ -10,29 +11,28 @@ export interface CommonValidation { export class Auth { private authTokenDatabase: AuthTokenDatabase - private jwtSecret: string public constructor(authTokenDatabase: AuthTokenDatabase) { this.authTokenDatabase = authTokenDatabase - this.jwtSecret = process.env.JWT_SECRET || 'ocean-node-secret' } - public getJwtSecret(): string { - return this.jwtSecret + public async getJwtSecret(): Promise { + const config = await getConfiguration() + return config.jwtSecret } public getMessage(address: string, nonce: string): string { return address + nonce } - getJWTToken(address: string, nonce: string, createdAt: number): string { + async getJWTToken(address: string, nonce: string, createdAt: number): Promise { const jwtToken = jwt.sign( { address, nonce, createdAt }, - this.getJwtSecret() + await this.getJwtSecret() ) return jwtToken diff --git a/src/components/core/handler/authHandler.ts b/src/components/core/handler/authHandler.ts index 6e7e5af89..b1400a10f 100644 --- a/src/components/core/handler/authHandler.ts +++ b/src/components/core/handler/authHandler.ts @@ -54,7 +54,7 @@ export class CreateAuthTokenHandler extends CommandHandler { } const createdAt = Date.now() - const jwtToken = this.getOceanNode() + const jwtToken = await this.getOceanNode() .getAuth() .getJWTToken(task.address, task.nonce, createdAt) diff --git a/src/test/unit/auth/token.test.ts b/src/test/unit/auth/token.test.ts index dda31db39..f185de0ba 100644 --- a/src/test/unit/auth/token.test.ts +++ b/src/test/unit/auth/token.test.ts @@ -23,7 +23,7 @@ describe('Auth Token Tests', () => { } it('should create and validate a token', async () => { - const jwtToken = auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) + const jwtToken = await auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) const result = await auth.validateAuthenticationOrToken({ token: jwtToken }) @@ -47,7 +47,7 @@ describe('Auth Token Tests', () => { }) it('should respect token expiry', async () => { - const jwtToken = auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) + const jwtToken = await auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) await new Promise((resolve) => setTimeout(resolve, 1500)) @@ -57,7 +57,7 @@ describe('Auth Token Tests', () => { }) it('should invalidate a token', async () => { - const jwtToken = auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) + const jwtToken = await auth.getJWTToken(wallet.address, getRandomNonce(), Date.now()) await auth.insertToken(wallet.address, jwtToken, Date.now() + 1000, Date.now()) await auth.invalidateToken(jwtToken) diff --git a/src/utils/config.ts b/src/utils/config.ts index 8f64f4c6d..cbaf3ecc4 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -832,7 +832,8 @@ async function getEnvConfig(isStartup?: boolean): Promise { ), isBootstrap: getBoolEnvValue('IS_BOOTSTRAP', false), claimDurationTimeout: getIntEnvValue(process.env.ESCROW_CLAIM_TIMEOUT, 600), - validateUnsignedDDO: getBoolEnvValue('VALIDATE_UNSIGNED_DDO', true) + validateUnsignedDDO: getBoolEnvValue('VALIDATE_UNSIGNED_DDO', true), + jwtSecret: getEnvValue(process.env.JWT_SECRET, 'ocean-node-secret') } if (!previousConfiguration) {