Skip to content

Commit 3494773

Browse files
authored
fix: allow aborting authentication (#43)
* 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. * chore: use host property
1 parent 6a515bc commit 3494773

File tree

3 files changed

+60
-8
lines changed

3 files changed

+60
-8
lines changed

examples/peer-id-auth/node.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const args = process.argv.slice(2)
1515
if (args.length === 1 && args[0] === 'client') {
1616
// Client mode
1717
const client = new ClientAuth(privKey)
18-
const observedPeerID = await client.authenticateServer(fetch, 'localhost:8001', 'http://localhost:8001/auth')
18+
const observedPeerID = await client.authenticateServer('http://localhost:8001/auth')
1919
console.log('Server ID:', observedPeerID.toString())
2020

2121
const authenticatedReq = new Request('http://localhost:8001/log-my-id', {

src/auth/client.ts

+27-5
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,29 @@ import { peerIdFromPublicKey } from '@libp2p/peer-id'
33
import { toString as uint8ArrayToString, fromString as uint8ArrayFromString } from 'uint8arrays'
44
import { parseHeader, PeerIDAuthScheme, sign, verify } from './common.js'
55
import type { PeerId, PrivateKey } from '@libp2p/interface'
6-
7-
interface Fetch { (input: RequestInfo, init?: RequestInit): Promise<Response> }
6+
import type { AbortOptions } from '@multiformats/multiaddr'
87

98
interface tokenInfo {
109
creationTime: Date
1110
bearer: string
1211
peer: PeerId
1312
}
1413

14+
export interface AuthenticateServerOptions extends AbortOptions {
15+
/**
16+
* The Fetch implementation to use
17+
*
18+
* @default globalThis.fetch
19+
*/
20+
fetch?: typeof globalThis.fetch
21+
22+
/**
23+
* The hostname to use - by default this will be extracted from the `.host`
24+
* property of `authEndpointURI`
25+
*/
26+
hostname?: string
27+
}
28+
1529
export class ClientAuth {
1630
key: PrivateKey
1731
tokens = new Map<string, tokenInfo>() // A map from hostname to token
@@ -49,7 +63,10 @@ export class ClientAuth {
4963
return `${PeerIDAuthScheme} bearer="${token.bearer}"`
5064
}
5165

52-
public async authenticateServer (fetch: Fetch, hostname: string, authEndpointURI: string): Promise<PeerId> {
66+
public async authenticateServer (authEndpointURI: string | URL, options?: AuthenticateServerOptions): Promise<PeerId> {
67+
authEndpointURI = new URL(authEndpointURI)
68+
const hostname = options?.hostname ?? authEndpointURI.host
69+
5370
if (this.tokens.has(hostname)) {
5471
const token = this.tokens.get(hostname)
5572
if (token !== undefined && Date.now() - token.creationTime.getTime() < this.tokenTTL) {
@@ -70,7 +87,11 @@ export class ClientAuth {
7087
})
7188
}
7289

73-
const resp = await fetch(authEndpointURI, { headers })
90+
const fetch = options?.fetch ?? globalThis.fetch
91+
const resp = await fetch(authEndpointURI, {
92+
headers,
93+
signal: options?.signal
94+
})
7495

7596
// Verify the server's challenge
7697
const authHeader = resp.headers.get('www-authenticate')
@@ -102,7 +123,8 @@ export class ClientAuth {
102123
const resp2 = await fetch(authEndpointURI, {
103124
headers: {
104125
Authorization: authenticateSelfHeaders
105-
}
126+
},
127+
signal: options?.signal
106128
})
107129

108130
// Verify the server's signature

test/auth/index.spec.ts

+32-2
Original file line numberDiff line numberDiff line change
@@ -26,16 +26,46 @@ describe('HTTP Peer ID Authentication', () => {
2626
const clientAuth = new ClientAuth(clientKey)
2727
const serverAuth = new ServerAuth(serverKey, h => h === 'example.com')
2828

29-
const fetch = async (input: RequestInfo, init?: RequestInit): Promise<Response> => {
29+
const fetch = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
3030
const req = new Request(input, init)
3131
const resp = await serverAuth.httpHandler(req)
3232
return resp
3333
}
3434

35-
const observedServerPeerId = await clientAuth.authenticateServer(fetch, 'example.com', 'https://example.com/auth')
35+
const observedServerPeerId = await clientAuth.authenticateServer('https://example.com/auth', {
36+
fetch
37+
})
3638
expect(observedServerPeerId.equals(server)).to.be.true()
3739
})
3840

41+
it('Should mutually authenticate with a custom port', async () => {
42+
const clientAuth = new ClientAuth(clientKey)
43+
const serverAuth = new ServerAuth(serverKey, h => h === 'foobar:12345')
44+
45+
const fetch = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
46+
const req = new Request(input, init)
47+
const resp = await serverAuth.httpHandler(req)
48+
return resp
49+
}
50+
51+
const observedServerPeerId = await clientAuth.authenticateServer('https://foobar:12345/auth', {
52+
fetch
53+
})
54+
expect(observedServerPeerId.equals(server)).to.be.true()
55+
})
56+
57+
it('Should time out when authenticating', async () => {
58+
const clientAuth = new ClientAuth(clientKey)
59+
60+
const controller = new AbortController()
61+
controller.abort()
62+
63+
await expect(clientAuth.authenticateServer('https://example.com/auth', {
64+
signal: controller.signal
65+
})).to.eventually.be.rejected
66+
.with.property('name', 'AbortError')
67+
})
68+
3969
it('Should match the test vectors', async () => {
4070
const clientKeyHex = '080112208139770ea87d175f56a35466c34c7ecccb8d8a91b4ee37a25df60f5b8fc9b394'
4171
const serverKeyHex = '0801124001010101010101010101010101010101010101010101010101010101010101018a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c'

0 commit comments

Comments
 (0)