Skip to content

Commit

Permalink
fix: allow aborting authentication (#43)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
achingbrain authored Oct 29, 2024
1 parent 6a515bc commit 3494773
Show file tree
Hide file tree
Showing 3 changed files with 60 additions and 8 deletions.
2 changes: 1 addition & 1 deletion examples/peer-id-auth/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
32 changes: 27 additions & 5 deletions src/auth/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,29 @@ 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<Response> }
import type { AbortOptions } from '@multiformats/multiaddr'

interface tokenInfo {
creationTime: Date
bearer: string
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<string, tokenInfo>() // A map from hostname to token
Expand Down Expand Up @@ -49,7 +63,10 @@ export class ClientAuth {
return `${PeerIDAuthScheme} bearer="${token.bearer}"`
}

public async authenticateServer (fetch: Fetch, hostname: string, authEndpointURI: string): Promise<PeerId> {
public async authenticateServer (authEndpointURI: string | URL, options?: AuthenticateServerOptions): Promise<PeerId> {
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) {
Expand All @@ -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')
Expand Down Expand Up @@ -102,7 +123,8 @@ export class ClientAuth {
const resp2 = await fetch(authEndpointURI, {
headers: {
Authorization: authenticateSelfHeaders
}
},
signal: options?.signal
})

// Verify the server's signature
Expand Down
34 changes: 32 additions & 2 deletions test/auth/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> => {
const fetch = async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
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<Response> => {
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'
Expand Down

0 comments on commit 3494773

Please sign in to comment.