Skip to content

Commit 5fbd0fe

Browse files
authored
Auth token routes (#952)
* routes and unit tests * unit and integration tests * add jwt * add jwt types * fix unit tests * refactor to handler * remove unused typesense schema * remove console logs * revert ddo file * refactor * use port 8000 tests * test with handler * revert aquarius * decorator used for valdiation * fix validation tests * support for new node instance * force refresh node * new instance test * fix bad enum * force refresh handler * cli tests paid compute * revert main cli * specify message in handler * remove duplicate routes * implement skipValidation params * override env tests * add header in handler * add docker comput envs in ci * reorder priority * add nonce * check nonce * name routes * env example and use config
1 parent e8cc695 commit 5fbd0fe

28 files changed

Lines changed: 1083 additions & 61 deletions

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export MAX_CHECKSUM_LENGTH=
3838
export LOG_LEVEL=
3939
export HTTP_API_PORT=
4040
export VALIDATE_UNSIGNED_DDO=
41+
export JWT_SECRET=
4142

4243
## p2p
4344

docs/env.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Environmental variables are also tracked in `ENVIRONMENT_VARIABLES` within `src/
3333
- `AUTHORIZED_PUBLISHERS`: Authorized list of publishers. If present, Node will only index assets published by the accounts in the list. Example: `"[\"0x967da4048cD07aB37855c090aAF366e4ce1b9F48\",\"0x388C818CA8B9251b393131C08a736A67ccB19297\"]"`
3434
- `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\"] }"`
3535
- `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`
36+
- `JWT_SECRET`: Secret used to sign JWT tokens. Default is `ocean-node-secret`. Example: `"my-secret-jwt-token"`
3637

3738
## Payments
3839

package-lock.json

Lines changed: 82 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"hyperdiff": "^2.0.16",
9090
"ip": "^2.0.1",
9191
"it-pipe": "^3.0.1",
92+
"jsonwebtoken": "^9.0.2",
9293
"libp2p": "^1.8.0",
9394
"lodash.clonedeep": "^4.5.0",
9495
"lzma-purejs-requirejs": "^1.0.0",
@@ -112,6 +113,7 @@
112113
"@types/dockerode": "^3.3.31",
113114
"@types/express": "^4.17.17",
114115
"@types/ip": "^1.1.3",
116+
"@types/jsonwebtoken": "^9.0.9",
115117
"@types/lzma-native": "^4.0.4",
116118
"@types/mocha": "^10.0.10",
117119
"@types/node": "^20.14.2",

src/@types/OceanNode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export interface OceanNodeConfig {
113113
unsafeURLs?: string[]
114114
isBootstrap?: boolean
115115
validateUnsignedDDO?: boolean
116+
jwtSecret?: string
116117
}
117118

118119
export interface P2PStatusResponse {

src/@types/commands.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
export interface Command {
2020
command: string // command name
2121
node?: string // if not present it means current node
22+
authorization?: string
2223
}
2324

2425
export interface GetP2PPeerCommand extends Command {
@@ -82,6 +83,7 @@ export interface ValidateDDOCommand extends Command {
8283
publisherAddress?: string
8384
nonce?: string
8485
signature?: string
86+
message?: string
8587
}
8688

8789
export interface StatusCommand extends Command {
@@ -261,3 +263,15 @@ export interface StartStopIndexingCommand extends AdminCommand {
261263
export interface PolicyServerPassthroughCommand extends Command {
262264
policyServerPassthrough?: any
263265
}
266+
267+
export interface CreateAuthTokenCommand extends Command {
268+
address: string
269+
signature: string
270+
validUntil?: number | null
271+
}
272+
273+
export interface InvalidateAuthTokenCommand extends Command {
274+
address: string
275+
signature: string
276+
token: string
277+
}

src/OceanNode.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { pipe } from 'it-pipe'
1212
import { GENERIC_EMOJIS, LOG_LEVELS_STR } from './utils/logging/Logger.js'
1313
import { BaseHandler } from './components/core/handler/handler.js'
1414
import { C2DEngines } from './components/c2d/compute_engines.js'
15+
import { Auth } from './components/Auth/index.js'
1516

1617
export interface RequestLimiter {
1718
requester: string | string[] // IP address or peer ID
@@ -35,6 +36,7 @@ export class OceanNode {
3536
// requester
3637
private remoteCaller: string | string[]
3738
private requestMap: Map<string, RequestLimiter>
39+
private auth: Auth
3840

3941
// eslint-disable-next-line no-useless-constructor
4042
private constructor(
@@ -47,6 +49,9 @@ export class OceanNode {
4749
this.coreHandlers = CoreHandlersRegistry.getInstance(this)
4850
this.requestMap = new Map<string, RequestLimiter>()
4951
this.config = config
52+
if (this.db && this.db?.authToken) {
53+
this.auth = new Auth(this.db.authToken)
54+
}
5055
if (node) {
5156
node.setCoreHandlers(this.coreHandlers)
5257
}
@@ -64,9 +69,10 @@ export class OceanNode {
6469
db?: Database,
6570
node?: OceanP2P,
6671
provider?: OceanProvider,
67-
indexer?: OceanIndexer
72+
indexer?: OceanIndexer,
73+
newInstance: boolean = false
6874
): OceanNode {
69-
if (!OceanNode.instance) {
75+
if (!OceanNode.instance || newInstance) {
7076
// prepare compute engines
7177
this.instance = new OceanNode(config, db, node, provider, indexer)
7278
}
@@ -136,6 +142,10 @@ export class OceanNode {
136142
return this.requestMap
137143
}
138144

145+
public getAuth(): Auth {
146+
return this.auth
147+
}
148+
139149
/**
140150
* Use this method to direct calls to the node as node cannot dial into itself
141151
* @param message command message

src/components/Auth/index.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { AuthToken, AuthTokenDatabase } from '../database/AuthTokenDatabase.js'
2+
import jwt from 'jsonwebtoken'
3+
import { checkNonce, NonceResponse } from '../core/utils/nonceHandler.js'
4+
import { OceanNode } from '../../OceanNode.js'
5+
import { getConfiguration } from '../../utils/index.js'
6+
7+
export interface CommonValidation {
8+
valid: boolean
9+
error: string
10+
}
11+
12+
export class Auth {
13+
private authTokenDatabase: AuthTokenDatabase
14+
15+
public constructor(authTokenDatabase: AuthTokenDatabase) {
16+
this.authTokenDatabase = authTokenDatabase
17+
}
18+
19+
public async getJwtSecret(): Promise<string> {
20+
const config = await getConfiguration()
21+
return config.jwtSecret
22+
}
23+
24+
public getMessage(address: string, nonce: string): string {
25+
return address + nonce
26+
}
27+
28+
async getJWTToken(address: string, nonce: string, createdAt: number): Promise<string> {
29+
const jwtToken = jwt.sign(
30+
{
31+
address,
32+
nonce,
33+
createdAt
34+
},
35+
await this.getJwtSecret()
36+
)
37+
38+
return jwtToken
39+
}
40+
41+
async insertToken(
42+
address: string,
43+
jwtToken: string,
44+
validUntil: number,
45+
createdAt: number
46+
): Promise<void> {
47+
await this.authTokenDatabase.createToken(jwtToken, address, validUntil, createdAt)
48+
}
49+
50+
async invalidateToken(jwtToken: string): Promise<void> {
51+
await this.authTokenDatabase.invalidateToken(jwtToken)
52+
}
53+
54+
async validateToken(token: string): Promise<AuthToken | null> {
55+
const tokenEntry = await this.authTokenDatabase.validateToken(token)
56+
if (!tokenEntry) {
57+
return null
58+
}
59+
return tokenEntry
60+
}
61+
62+
/**
63+
* Validates the authentication or token
64+
* You need to provider either a token or an address, signature and message
65+
* @param {string} token - The token to validate
66+
* @param {string} address - The address to validate
67+
* @param {string} signature - The signature to validate
68+
* @param {string} message - The message to validate
69+
* @returns The validation result
70+
*/
71+
async validateAuthenticationOrToken({
72+
token,
73+
address,
74+
nonce,
75+
signature
76+
}: {
77+
token?: string
78+
address?: string
79+
nonce?: string
80+
signature?: string
81+
}): Promise<CommonValidation> {
82+
try {
83+
if (signature && address && nonce) {
84+
const oceanNode = OceanNode.getInstance()
85+
const nonceCheckResult: NonceResponse = await checkNonce(
86+
oceanNode.getDatabase().nonce,
87+
address,
88+
parseInt(nonce),
89+
signature,
90+
this.getMessage(address, nonce)
91+
)
92+
93+
if (!nonceCheckResult.valid) {
94+
return { valid: false, error: nonceCheckResult.error }
95+
}
96+
97+
if (nonceCheckResult.valid) {
98+
return { valid: true, error: '' }
99+
}
100+
}
101+
102+
if (token) {
103+
const authToken = await this.validateToken(token)
104+
if (authToken) {
105+
return { valid: true, error: '' }
106+
}
107+
108+
return { valid: false, error: 'Invalid token' }
109+
}
110+
111+
return {
112+
valid: false,
113+
error:
114+
'Invalid authentication, you need to provide either a token or an address, signature, message and nonce'
115+
}
116+
} catch (e) {
117+
return { valid: false, error: `Error during authentication validation: ${e}` }
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)