From 3baa8d52a8291ce99fd1fb73c7875f287d2cd67e Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 29 Oct 2024 17:30:01 +0000 Subject: [PATCH 1/2] fix: allow aborting authentication Allows passing an abort signal to `authenticateServer` to give the caller control of when to give up waiting for the server response. Also allows passing a `URL` as the auth endpoint and allows overriding the hostname/fetch implementations as options instead of requiring them to be passed explicitly. --- examples/peer-id-auth/node.js | 2 +- src/auth/client.ts | 32 +++++++++++++++++++++++++++----- test/auth/index.spec.ts | 18 ++++++++++++++++-- 3 files changed, 44 insertions(+), 8 deletions(-) diff --git a/examples/peer-id-auth/node.js b/examples/peer-id-auth/node.js index 093f99e..bc811dc 100644 --- a/examples/peer-id-auth/node.js +++ b/examples/peer-id-auth/node.js @@ -15,7 +15,7 @@ const args = process.argv.slice(2) if (args.length === 1 && args[0] === 'client') { // Client mode const client = new ClientAuth(privKey) - const observedPeerID = await client.authenticateServer(fetch, 'localhost:8001', 'http://localhost:8001/auth') + const observedPeerID = await client.authenticateServer('http://localhost:8001/auth') console.log('Server ID:', observedPeerID.toString()) const authenticatedReq = new Request('http://localhost:8001/log-my-id', { diff --git a/src/auth/client.ts b/src/auth/client.ts index 685f388..43e65a3 100644 --- a/src/auth/client.ts +++ b/src/auth/client.ts @@ -3,8 +3,7 @@ import { peerIdFromPublicKey } from '@libp2p/peer-id' import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays' import { parseHeader, PeerIDAuthScheme, sign, verify } from './common.js' import type { PeerId, PrivateKey } from '@libp2p/interface' - -interface Fetch { (input: RequestInfo, init?: RequestInit): Promise } +import type { AbortOptions } from '@multiformats/multiaddr' interface tokenInfo { creationTime: Date @@ -12,6 +11,21 @@ interface tokenInfo { peer: PeerId } +export interface AuthenticateServerOptions extends AbortOptions { + /** + * The Fetch implementation to use + * + * @default globalThis.fetch + */ + fetch?: typeof globalThis.fetch + + /** + * The hostname to use - by default this will be extracted from the + * `authEndpointURI` + */ + hostname?: string +} + export class ClientAuth { key: PrivateKey tokens = new Map() // A map from hostname to token @@ -49,7 +63,10 @@ export class ClientAuth { return `${PeerIDAuthScheme} bearer="${token.bearer}"` } - public async authenticateServer (fetch: Fetch, hostname: string, authEndpointURI: string): Promise { + public async authenticateServer (authEndpointURI: string | URL, options?: AuthenticateServerOptions): Promise { + authEndpointURI = new URL(authEndpointURI) + const hostname = options?.hostname ?? authEndpointURI.hostname + if (this.tokens.has(hostname)) { const token = this.tokens.get(hostname) if (token !== undefined && Date.now() - token.creationTime.getTime() < this.tokenTTL) { @@ -70,7 +87,11 @@ export class ClientAuth { }) } - const resp = await fetch(authEndpointURI, { headers }) + const fetch = options?.fetch ?? globalThis.fetch + const resp = await fetch(authEndpointURI, { + headers, + signal: options?.signal + }) // Verify the server's challenge const authHeader = resp.headers.get('www-authenticate') @@ -102,7 +123,8 @@ export class ClientAuth { const resp2 = await fetch(authEndpointURI, { headers: { Authorization: authenticateSelfHeaders - } + }, + signal: options?.signal }) // Verify the server's signature diff --git a/test/auth/index.spec.ts b/test/auth/index.spec.ts index 0962c9f..9e447ec 100644 --- a/test/auth/index.spec.ts +++ b/test/auth/index.spec.ts @@ -26,16 +26,30 @@ describe('HTTP Peer ID Authentication', () => { const clientAuth = new ClientAuth(clientKey) const serverAuth = new ServerAuth(serverKey, h => h === 'example.com') - const fetch = async (input: RequestInfo, init?: RequestInit): Promise => { + const fetch = async (input: string | URL | Request, init?: RequestInit): Promise => { const req = new Request(input, init) const resp = await serverAuth.httpHandler(req) return resp } - const observedServerPeerId = await clientAuth.authenticateServer(fetch, 'example.com', 'https://example.com/auth') + const observedServerPeerId = await clientAuth.authenticateServer('https://example.com/auth', { + fetch + }) expect(observedServerPeerId.equals(server)).to.be.true() }) + it('Should time out when authenticating', async () => { + const clientAuth = new ClientAuth(clientKey) + + const controller = new AbortController() + controller.abort() + + await expect(clientAuth.authenticateServer('https://example.com/auth', { + signal: controller.signal + })).to.eventually.be.rejected + .with.property('name', 'AbortError') + }) + it('Should match the test vectors', async () => { const clientKeyHex = '080112208139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394' const serverKeyHex = '0801124001010101010101010101010101010101010101010101010101010101010101018a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c' From ffab7e227c706267d9232f6daffdc73df15f9ba2 Mon Sep 17 00:00:00 2001 From: achingbrain Date: Tue, 29 Oct 2024 17:47:37 +0000 Subject: [PATCH 2/2] chore: use host property --- src/auth/client.ts | 6 +++--- test/auth/index.spec.ts | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/auth/client.ts b/src/auth/client.ts index 43e65a3..97124a1 100644 --- a/src/auth/client.ts +++ b/src/auth/client.ts @@ -20,8 +20,8 @@ export interface AuthenticateServerOptions extends AbortOptions { fetch?: typeof globalThis.fetch /** - * The hostname to use - by default this will be extracted from the - * `authEndpointURI` + * The hostname to use - by default this will be extracted from the `.host` + * property of `authEndpointURI` */ hostname?: string } @@ -65,7 +65,7 @@ export class ClientAuth { public async authenticateServer (authEndpointURI: string | URL, options?: AuthenticateServerOptions): Promise { authEndpointURI = new URL(authEndpointURI) - const hostname = options?.hostname ?? authEndpointURI.hostname + const hostname = options?.hostname ?? authEndpointURI.host if (this.tokens.has(hostname)) { const token = this.tokens.get(hostname) diff --git a/test/auth/index.spec.ts b/test/auth/index.spec.ts index 9e447ec..ba05ffd 100644 --- a/test/auth/index.spec.ts +++ b/test/auth/index.spec.ts @@ -38,6 +38,22 @@ describe('HTTP Peer ID Authentication', () => { expect(observedServerPeerId.equals(server)).to.be.true() }) + it('Should mutually authenticate with a custom port', async () => { + const clientAuth = new ClientAuth(clientKey) + const serverAuth = new ServerAuth(serverKey, h => h === 'foobar:12345') + + const fetch = async (input: string | URL | Request, init?: RequestInit): Promise => { + const req = new Request(input, init) + const resp = await serverAuth.httpHandler(req) + return resp + } + + const observedServerPeerId = await clientAuth.authenticateServer('https://foobar:12345/auth', { + fetch + }) + expect(observedServerPeerId.equals(server)).to.be.true() + }) + it('Should time out when authenticating', async () => { const clientAuth = new ClientAuth(clientKey)