From 4be96ba61af31e13a54c03dc7cdaab0d6ac6eeac Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Mon, 23 Jun 2025 12:32:35 +1000 Subject: [PATCH 1/4] chore: aligned rpc with tailscale login flow --- src/client/handlers/AuthSignToken.ts | 37 +++------------------------- src/client/types.ts | 12 --------- 2 files changed, 4 insertions(+), 45 deletions(-) diff --git a/src/client/handlers/AuthSignToken.ts b/src/client/handlers/AuthSignToken.ts index 4a38eb484..f8445e667 100644 --- a/src/client/handlers/AuthSignToken.ts +++ b/src/client/handlers/AuthSignToken.ts @@ -1,56 +1,27 @@ import type { ClientRPCRequestParams, ClientRPCResponseResult, - IdentityRequestData, IdentityResponseData, - TokenIdentityRequest, TokenIdentityResponse, } from '../types.js'; import type KeyRing from '../../keys/KeyRing.js'; -import type { PublicKey } from '../../keys/types.js'; import { UnaryHandler } from '@matrixai/rpc'; import Token from '../../tokens/Token.js'; -import * as clientErrors from '../errors.js'; import * as nodesUtils from '../../nodes/utils.js'; class AuthSignToken extends UnaryHandler< { keyRing: KeyRing; }, - ClientRPCRequestParams, + ClientRPCRequestParams, ClientRPCResponseResult > { - public handle = async ( - input: ClientRPCRequestParams, - ): Promise => { + public handle = async (): Promise => { const { keyRing }: { keyRing: KeyRing } = this.container; - - // Get and verify incoming node - const inputToken = { payload: input.payload, signatures: input.signatures }; - const incomingToken = Token.fromEncoded(inputToken); - if (!('publicKey' in incomingToken.payload)) { - throw new clientErrors.ErrorClientAuthenticationInvalidToken( - 'Input token does not contain public key', - ); - } - const incomingPublicKey = Buffer.from( - incomingToken.payload.publicKey, - 'base64url', - ) as PublicKey; - if (!incomingToken.verifyWithPublicKey(incomingPublicKey)) { - throw new clientErrors.ErrorClientAuthenticationInvalidToken( - 'Incoming token does not match its signature', - ); - } - - // Create the outgoing token with the incoming token integrated into the - // payload. - const outgoingTokenPayload: IdentityResponseData = { - requestToken: inputToken, + const tokenPayload: IdentityResponseData = { nodeId: nodesUtils.encodeNodeId(keyRing.getNodeId()), }; - const outgoingToken = - Token.fromPayload(outgoingTokenPayload); + const outgoingToken = Token.fromPayload(tokenPayload); outgoingToken.signWithPrivateKey(keyRing.keyPair); return outgoingToken.toEncoded(); }; diff --git a/src/client/types.ts b/src/client/types.ts index e9900e89d..7329cd01c 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -108,17 +108,7 @@ type TokenMessage = { token: ProviderToken; }; -// Return URL must be present on the token, otherwise token contents is decided -// by the client. -type IdentityRequestData = TokenPayload & { - returnURL: string; - publicKey: string; -}; - -type TokenIdentityRequest = SignedTokenEncoded; - type IdentityResponseData = TokenPayload & { - requestToken: TokenIdentityRequest; nodeId: NodeIdEncoded; }; @@ -422,9 +412,7 @@ export type { ClaimIdMessage, ClaimNodeMessage, TokenMessage, - IdentityRequestData, IdentityResponseData, - TokenIdentityRequest, TokenIdentityResponse, NodeIdMessage, AddressMessage, From 01f75969d19bd3c98ce57b4ca85d3fdedaba26e4 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Mon, 23 Jun 2025 12:39:42 +1000 Subject: [PATCH 2/4] test: updated tests to reflect new behaviour fix: tests --- tests/client/handlers/auth.test.ts | 47 ++++-------------------------- 1 file changed, 6 insertions(+), 41 deletions(-) diff --git a/tests/client/handlers/auth.test.ts b/tests/client/handlers/auth.test.ts index 37115248d..f6888cc45 100644 --- a/tests/client/handlers/auth.test.ts +++ b/tests/client/handlers/auth.test.ts @@ -1,7 +1,4 @@ -import type { - IdentityRequestData, - IdentityResponseData, -} from '#src/client/types.js'; +import type { IdentityResponseData } from '#src/client/types.js'; import type { TLSConfig } from '#network/types.js'; import fs from 'node:fs'; import path from 'node:path'; @@ -17,7 +14,7 @@ import Token from '#tokens/Token.js'; import ClientService from '#client/ClientService.js'; import * as keysUtils from '#keys/utils/index.js'; import * as networkUtils from '#network/utils.js'; -import * as clientErrors from '#client/errors.js'; +import * as nodesUtils from '#nodes/utils.js'; describe('authSignToken', () => { const logger = new Logger('authSignToken test', LogLevel.WARN, [ @@ -91,44 +88,12 @@ describe('authSignToken', () => { }); }); - test('should sign a valid token', async () => { - // Create token with separate key pair - const keyPair = keysUtils.generateKeyPair(); - const token = Token.fromPayload({ - publicKey: keyPair.publicKey.toString('base64url'), - returnURL: 'test', - }); - token.signWithPrivateKey(keyPair); - - // Get the node to sign the token as well - const encodedToken = token.toEncoded(); - const identityToken = await rpcClient.methods.authSignToken(encodedToken); - - // Check the signature of both the incoming token and the original sent token + test('should return a signed token', async () => { + const identityToken = await rpcClient.methods.authSignToken({}); const decodedToken = Token.fromEncoded(identityToken); const decodedPublicKey = keysUtils.publicKeyFromNodeId(keyRing.getNodeId()); expect(decodedToken.verifyWithPublicKey(decodedPublicKey)).toBeTrue(); - const requestToken = Token.fromEncoded( - decodedToken.payload.requestToken, - ); - expect(requestToken.verifyWithPublicKey(keyPair.publicKey)).toBeTrue(); - }); - - test('should fail if public key does not match signature', async () => { - // Create token with a key pair and sign it with another - const keyPair1 = keysUtils.generateKeyPair(); - const keyPair2 = keysUtils.generateKeyPair(); - const token = Token.fromPayload({ - publicKey: keyPair1.publicKey.toString('base64url'), - returnURL: 'test', - }); - token.signWithPrivateKey(keyPair2); - - // The token should fail validation - const encodedToken = token.toEncoded(); - await testsUtils.expectRemoteError( - rpcClient.methods.authSignToken(encodedToken), - clientErrors.ErrorClientAuthenticationInvalidToken, - ); + const encodedNodeId = nodesUtils.encodeNodeId(keyRing.getNodeId()); + expect(decodedToken.payload.nodeId).toBe(encodedNodeId); }); }); From 5eda0b81d143056d1fbd5d771346298d3e6c9a42 Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Mon, 23 Jun 2025 15:52:52 +1000 Subject: [PATCH 3/4] chore: added expiry to jwt chore: added stricter types fix: rpc manifest --- src/client/callers/authIdentityToken.ts | 12 +++++++++++ src/client/callers/authSignToken.ts | 12 ----------- src/client/callers/index.ts | 6 +++--- src/client/errors.ts | 12 +++++------ ...{AuthSignToken.ts => AuthIdentityToken.ts} | 20 +++++++++++++------ src/client/handlers/index.ts | 6 +++--- src/client/types.ts | 4 +++- tests/client/handlers/auth.test.ts | 20 ++++++++++--------- 8 files changed, 52 insertions(+), 40 deletions(-) create mode 100644 src/client/callers/authIdentityToken.ts delete mode 100644 src/client/callers/authSignToken.ts rename src/client/handlers/{AuthSignToken.ts => AuthIdentityToken.ts} (58%) diff --git a/src/client/callers/authIdentityToken.ts b/src/client/callers/authIdentityToken.ts new file mode 100644 index 000000000..78b248c0c --- /dev/null +++ b/src/client/callers/authIdentityToken.ts @@ -0,0 +1,12 @@ +import type { HandlerTypes } from '@matrixai/rpc'; +import type AuthIdentityToken from '../handlers/AuthIdentityToken.js'; +import { UnaryCaller } from '@matrixai/rpc'; + +type CallerTypes = HandlerTypes; + +const authIdentityToken = new UnaryCaller< + CallerTypes['input'], + CallerTypes['output'] +>(); + +export default authIdentityToken; diff --git a/src/client/callers/authSignToken.ts b/src/client/callers/authSignToken.ts deleted file mode 100644 index 34861e645..000000000 --- a/src/client/callers/authSignToken.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { HandlerTypes } from '@matrixai/rpc'; -import type AuthSignToken from '../handlers/AuthSignToken.js'; -import { UnaryCaller } from '@matrixai/rpc'; - -type CallerTypes = HandlerTypes; - -const authSignToken = new UnaryCaller< - CallerTypes['input'], - CallerTypes['output'] ->(); - -export default authSignToken; diff --git a/src/client/callers/index.ts b/src/client/callers/index.ts index d3aa78c8e..7a00bc4ec 100644 --- a/src/client/callers/index.ts +++ b/src/client/callers/index.ts @@ -4,7 +4,7 @@ import agentStop from './agentStop.js'; import agentUnlock from './agentUnlock.js'; import auditEventsGet from './auditEventsGet.js'; import auditMetricGet from './auditMetricGet.js'; -import authSignToken from './authSignToken.js'; +import authIdentityToken from './authIdentityToken.js'; import gestaltsActionsGetByIdentity from './gestaltsActionsGetByIdentity.js'; import gestaltsActionsGetByNode from './gestaltsActionsGetByNode.js'; import gestaltsActionsSetByIdentity from './gestaltsActionsSetByIdentity.js'; @@ -86,7 +86,7 @@ const clientManifest = { agentUnlock, auditEventsGet, auditMetricGet, - authSignToken, + authIdentityToken, gestaltsActionsGetByIdentity, gestaltsActionsGetByNode, gestaltsActionsSetByIdentity, @@ -167,7 +167,7 @@ export { agentStop, agentUnlock, auditEventsGet, - authSignToken, + authIdentityToken, gestaltsActionsGetByIdentity, gestaltsActionsGetByNode, gestaltsActionsSetByIdentity, diff --git a/src/client/errors.ts b/src/client/errors.ts index f5ba1cb9a..c2fa3edf5 100644 --- a/src/client/errors.ts +++ b/src/client/errors.ts @@ -28,6 +28,11 @@ class ErrorClientProtocolError extends ErrorClient { exitCode = sysexits.USAGE; } +class ErrorClientAuthenticationInvalidJTI extends ErrorClient { + static description = 'Failed to generate JTI'; + exitCode = sysexits.PROTOCOL; +} + class ErrorClientService extends ErrorClient {} class ErrorClientServiceRunning extends ErrorClientService { @@ -50,11 +55,6 @@ class ErrorClientVerificationFailed extends ErrorClientService { exitCode = sysexits.USAGE; } -class ErrorClientAuthenticationInvalidToken extends ErrorClient { - static description = 'Token is invalid'; - exitCode = sysexits.PROTOCOL; -} - export { ErrorClient, ErrorClientAuthMissing, @@ -62,10 +62,10 @@ export { ErrorClientAuthDenied, ErrorClientInvalidHeader, ErrorClientProtocolError, + ErrorClientAuthenticationInvalidJTI, ErrorClientService, ErrorClientServiceRunning, ErrorClientServiceNotRunning, ErrorClientServiceDestroyed, ErrorClientVerificationFailed, - ErrorClientAuthenticationInvalidToken, }; diff --git a/src/client/handlers/AuthSignToken.ts b/src/client/handlers/AuthIdentityToken.ts similarity index 58% rename from src/client/handlers/AuthSignToken.ts rename to src/client/handlers/AuthIdentityToken.ts index f8445e667..9c81a20fc 100644 --- a/src/client/handlers/AuthSignToken.ts +++ b/src/client/handlers/AuthIdentityToken.ts @@ -5,11 +5,13 @@ import type { TokenIdentityResponse, } from '../types.js'; import type KeyRing from '../../keys/KeyRing.js'; +import { IdSortable } from '@matrixai/id'; import { UnaryHandler } from '@matrixai/rpc'; import Token from '../../tokens/Token.js'; import * as nodesUtils from '../../nodes/utils.js'; +import * as clientErrors from '../errors.js'; -class AuthSignToken extends UnaryHandler< +class AuthIdentityToken extends UnaryHandler< { keyRing: KeyRing; }, @@ -18,13 +20,19 @@ class AuthSignToken extends UnaryHandler< > { public handle = async (): Promise => { const { keyRing }: { keyRing: KeyRing } = this.container; - const tokenPayload: IdentityResponseData = { - nodeId: nodesUtils.encodeNodeId(keyRing.getNodeId()), - }; - const outgoingToken = Token.fromPayload(tokenPayload); + const idGen = new IdSortable(); + const jti = idGen.next().value; + if (jti == null) { + throw new clientErrors.ErrorClientAuthenticationInvalidJTI(); + } + const outgoingToken = Token.fromPayload({ + jti: jti.toMultibase('base64'), + exp: Math.floor(Date.now() / 1000) + 60, // 60 seconds after issuing + iss: nodesUtils.encodeNodeId(keyRing.getNodeId()), + }); outgoingToken.signWithPrivateKey(keyRing.keyPair); return outgoingToken.toEncoded(); }; } -export default AuthSignToken; +export default AuthIdentityToken; diff --git a/src/client/handlers/index.ts b/src/client/handlers/index.ts index d846ed093..f4b9a6588 100644 --- a/src/client/handlers/index.ts +++ b/src/client/handlers/index.ts @@ -22,7 +22,7 @@ import AgentStop from './AgentStop.js'; import AgentUnlock from './AgentUnlock.js'; import AuditEventsGet from './AuditEventsGet.js'; import AuditMetricGet from './AuditMetricGet.js'; -import AuthSignToken from './AuthSignToken.js'; +import AuthIdentityToken from './AuthIdentityToken.js'; import GestaltsActionsGetByIdentity from './GestaltsActionsGetByIdentity.js'; import GestaltsActionsGetByNode from './GestaltsActionsGetByNode.js'; import GestaltsActionsSetByIdentity from './GestaltsActionsSetByIdentity.js'; @@ -123,7 +123,7 @@ const serverManifest = (container: { agentUnlock: new AgentUnlock(container), auditEventsGet: new AuditEventsGet(container), auditMetricGet: new AuditMetricGet(container), - authSignToken: new AuthSignToken(container), + authIdentityToken: new AuthIdentityToken(container), gestaltsActionsGetByIdentity: new GestaltsActionsGetByIdentity(container), gestaltsActionsGetByNode: new GestaltsActionsGetByNode(container), gestaltsActionsSetByIdentity: new GestaltsActionsSetByIdentity(container), @@ -210,7 +210,7 @@ export { AgentUnlock, AuditEventsGet, AuditMetricGet, - AuthSignToken, + AuthIdentityToken, GestaltsActionsGetByIdentity, GestaltsActionsGetByNode, GestaltsActionsSetByIdentity, diff --git a/src/client/types.ts b/src/client/types.ts index 7329cd01c..bef9ee91b 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -109,7 +109,9 @@ type TokenMessage = { }; type IdentityResponseData = TokenPayload & { - nodeId: NodeIdEncoded; + jti: string; + exp: number; + iss: NodeIdEncoded; }; type TokenIdentityResponse = SignedTokenEncoded; diff --git a/tests/client/handlers/auth.test.ts b/tests/client/handlers/auth.test.ts index f6888cc45..9a53a6251 100644 --- a/tests/client/handlers/auth.test.ts +++ b/tests/client/handlers/auth.test.ts @@ -7,8 +7,8 @@ import Logger, { formatting, LogLevel, StreamHandler } from '@matrixai/logger'; import { RPCClient } from '@matrixai/rpc'; import { WebSocketClient } from '@matrixai/ws'; import * as testsUtils from '../../utils/index.js'; -import { AuthSignToken } from '#client/handlers/index.js'; -import { authSignToken } from '#client/callers/index.js'; +import { AuthIdentityToken } from '#client/handlers/index.js'; +import { authIdentityToken } from '#client/callers/index.js'; import KeyRing from '#keys/KeyRing.js'; import Token from '#tokens/Token.js'; import ClientService from '#client/ClientService.js'; @@ -16,8 +16,8 @@ import * as keysUtils from '#keys/utils/index.js'; import * as networkUtils from '#network/utils.js'; import * as nodesUtils from '#nodes/utils.js'; -describe('authSignToken', () => { - const logger = new Logger('authSignToken test', LogLevel.WARN, [ +describe('authIdentityToken', () => { + const logger = new Logger('authIdentityToken test', LogLevel.WARN, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), @@ -30,7 +30,7 @@ describe('authSignToken', () => { let clientService: ClientService; let webSocketClient: WebSocketClient; let rpcClient: RPCClient<{ - authSignToken: typeof authSignToken; + authIdentityToken: typeof authIdentityToken; }>; beforeEach(async () => { @@ -53,7 +53,7 @@ describe('authSignToken', () => { }); await clientService.start({ manifest: { - authSignToken: new AuthSignToken({ + authIdentityToken: new AuthIdentityToken({ keyRing, }), }, @@ -69,7 +69,7 @@ describe('authSignToken', () => { }); rpcClient = new RPCClient({ manifest: { - authSignToken, + authIdentityToken, }, streamFactory: () => webSocketClient.connection.newStream(), toError: networkUtils.toError, @@ -89,11 +89,13 @@ describe('authSignToken', () => { }); test('should return a signed token', async () => { - const identityToken = await rpcClient.methods.authSignToken({}); + const identityToken = await rpcClient.methods.authIdentityToken({}); const decodedToken = Token.fromEncoded(identityToken); const decodedPublicKey = keysUtils.publicKeyFromNodeId(keyRing.getNodeId()); expect(decodedToken.verifyWithPublicKey(decodedPublicKey)).toBeTrue(); const encodedNodeId = nodesUtils.encodeNodeId(keyRing.getNodeId()); - expect(decodedToken.payload.nodeId).toBe(encodedNodeId); + expect(decodedToken.payload.iss).toBe(encodedNodeId); + expect(decodedToken.payload.exp).toBeDefined(); + expect(decodedToken.payload.jti).toBeDefined(); }); }); From d40fbc958024eca5900677cff48dc03e7c848c6e Mon Sep 17 00:00:00 2001 From: Aryan Jassal Date: Mon, 23 Jun 2025 16:48:30 +1000 Subject: [PATCH 4/4] fix: typescript project warnings --- tests/client/handlers/auth.test.ts | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/client/handlers/auth.test.ts b/tests/client/handlers/auth.test.ts index 9a53a6251..58b277613 100644 --- a/tests/client/handlers/auth.test.ts +++ b/tests/client/handlers/auth.test.ts @@ -1,4 +1,4 @@ -import type { IdentityResponseData } from '#src/client/types.js'; +import type { IdentityResponseData } from '#client/types.js'; import type { TLSConfig } from '#network/types.js'; import fs from 'node:fs'; import path from 'node:path'; diff --git a/tsconfig.json b/tsconfig.json index 8a3a5d48b..4366b460c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "allowSyntheticDefaultImports": true, "resolveJsonModule": true, "moduleResolution": "NodeNext", - "module": "ESNext", + "module": "NodeNext", "target": "ES2022", "baseUrl": "./src", "paths": {