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..97124a1 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 `.host` + * property of `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.host + 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..ba05ffd 100644 --- a/test/auth/index.spec.ts +++ b/test/auth/index.spec.ts @@ -26,16 +26,46 @@ 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 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) + + 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'