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/AuthIdentityToken.ts b/src/client/handlers/AuthIdentityToken.ts new file mode 100644 index 000000000..9c81a20fc --- /dev/null +++ b/src/client/handlers/AuthIdentityToken.ts @@ -0,0 +1,38 @@ +import type { + ClientRPCRequestParams, + ClientRPCResponseResult, + IdentityResponseData, + 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 AuthIdentityToken extends UnaryHandler< + { + keyRing: KeyRing; + }, + ClientRPCRequestParams, + ClientRPCResponseResult +> { + public handle = async (): Promise => { + const { keyRing }: { keyRing: KeyRing } = this.container; + 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 AuthIdentityToken; diff --git a/src/client/handlers/AuthSignToken.ts b/src/client/handlers/AuthSignToken.ts deleted file mode 100644 index 4a38eb484..000000000 --- a/src/client/handlers/AuthSignToken.ts +++ /dev/null @@ -1,59 +0,0 @@ -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, - ClientRPCResponseResult -> { - public handle = async ( - input: ClientRPCRequestParams, - ): 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, - nodeId: nodesUtils.encodeNodeId(keyRing.getNodeId()), - }; - const outgoingToken = - Token.fromPayload(outgoingTokenPayload); - outgoingToken.signWithPrivateKey(keyRing.keyPair); - return outgoingToken.toEncoded(); - }; -} - -export default AuthSignToken; 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 e9900e89d..bef9ee91b 100644 --- a/src/client/types.ts +++ b/src/client/types.ts @@ -108,18 +108,10 @@ 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; + jti: string; + exp: number; + iss: NodeIdEncoded; }; type TokenIdentityResponse = SignedTokenEncoded; @@ -422,9 +414,7 @@ export type { ClaimIdMessage, ClaimNodeMessage, TokenMessage, - IdentityRequestData, IdentityResponseData, - TokenIdentityRequest, TokenIdentityResponse, NodeIdMessage, AddressMessage, diff --git a/tests/client/handlers/auth.test.ts b/tests/client/handlers/auth.test.ts index 37115248d..58b277613 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 '#client/types.js'; import type { TLSConfig } from '#network/types.js'; import fs from 'node:fs'; import path from 'node:path'; @@ -10,17 +7,17 @@ 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'; 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, [ +describe('authIdentityToken', () => { + const logger = new Logger('authIdentityToken test', LogLevel.WARN, [ new StreamHandler( formatting.format`${formatting.level}:${formatting.keys}:${formatting.msg}`, ), @@ -33,7 +30,7 @@ describe('authSignToken', () => { let clientService: ClientService; let webSocketClient: WebSocketClient; let rpcClient: RPCClient<{ - authSignToken: typeof authSignToken; + authIdentityToken: typeof authIdentityToken; }>; beforeEach(async () => { @@ -56,7 +53,7 @@ describe('authSignToken', () => { }); await clientService.start({ manifest: { - authSignToken: new AuthSignToken({ + authIdentityToken: new AuthIdentityToken({ keyRing, }), }, @@ -72,7 +69,7 @@ describe('authSignToken', () => { }); rpcClient = new RPCClient({ manifest: { - authSignToken, + authIdentityToken, }, streamFactory: () => webSocketClient.connection.newStream(), toError: networkUtils.toError, @@ -91,44 +88,14 @@ 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.authIdentityToken({}); 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.iss).toBe(encodedNodeId); + expect(decodedToken.payload.exp).toBeDefined(); + expect(decodedToken.payload.jti).toBeDefined(); }); }); 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": {