@@ -2,16 +2,17 @@ import { publicKeyFromProtobuf, publicKeyToProtobuf } from '@libp2p/crypto/keys'
22import { peerIdFromPublicKey } from '@libp2p/peer-id'
33import { toString as uint8ArrayToString , fromString as uint8ArrayFromString } from 'uint8arrays'
44import { parseHeader , PeerIDAuthScheme , sign , verify } from './common.js'
5+ import { BadResponseError , InvalidPeerError , InvalidSignatureError , MissingAuthHeaderError } from './errors.js'
56import type { PeerId , PrivateKey } from '@libp2p/interface'
67import type { AbortOptions } from '@multiformats/multiaddr'
78
8- interface tokenInfo {
9+ export interface TokenInfo {
910 creationTime : Date
1011 bearer : string
1112 peer : PeerId
1213}
1314
14- export interface AuthenticateServerOptions extends AbortOptions {
15+ export interface AuthenticatedFetchOptions extends RequestInit {
1516 /**
1617 * The Fetch implementation to use
1718 *
@@ -24,11 +25,38 @@ export interface AuthenticateServerOptions extends AbortOptions {
2425 * property of `authEndpointURI`
2526 */
2627 hostname ?: string
28+
29+ /**
30+ * A function to verify the peer ID of the server. This function
31+ * will be called after the server has authenticated itself.
32+ * If the function returns false, the request will be aborted.
33+ */
34+ verifyPeer ?( peerId : PeerId , options : AbortOptions ) : boolean | Promise < boolean >
35+ }
36+
37+ export interface AuthenticateServerOptions extends AbortOptions {
38+ /**
39+ * The Fetch implementation to use
40+ *
41+ * @default globalThis.fetch
42+ */
43+ fetch ?: AuthenticatedFetchOptions [ 'fetch' ]
44+
45+ /**
46+ * The hostname to use - by default this will be extracted from the `.host`
47+ * property of `authEndpointURI`
48+ */
49+ hostname ?: AuthenticatedFetchOptions [ 'hostname' ]
50+ }
51+
52+ interface DoAuthenticatedFetchOptions {
53+ fetch ?: AuthenticatedFetchOptions [ 'fetch' ]
54+ hostname ?: AuthenticatedFetchOptions [ 'hostname' ]
2755}
2856
2957export class ClientAuth {
3058 key : PrivateKey
31- tokens = new Map < string , tokenInfo > ( ) // A map from hostname to token
59+ tokens = new Map < string , TokenInfo > ( ) // A map from hostname to token
3260 tokenTTL = 60 * 60 * 1000 // 1 hour
3361
3462 constructor ( key : PrivateKey , opts ?: { tokenTTL ?: number } ) {
@@ -51,7 +79,7 @@ export class ClientAuth {
5179 return `${ PeerIDAuthScheme } ${ encodedParams } `
5280 }
5381
54- public bearerAuthHeader ( hostname : string ) : string | undefined {
82+ public bearerAuthHeaderWithPeer ( hostname : string ) : { 'authorization' : string , peer : PeerId } | undefined {
5583 const token = this . tokens . get ( hostname )
5684 if ( token == null ) {
5785 return undefined
@@ -60,17 +88,48 @@ export class ClientAuth {
6088 this . tokens . delete ( hostname )
6189 return undefined
6290 }
63- return `${ PeerIDAuthScheme } bearer="${ token . bearer } "`
91+ return { authorization : `${ PeerIDAuthScheme } bearer="${ token . bearer } "` , peer : token . peer }
6492 }
6593
66- public async authenticateServer ( authEndpointURI : string | URL , options ?: AuthenticateServerOptions ) : Promise < PeerId > {
67- authEndpointURI = new URL ( authEndpointURI )
94+ public bearerAuthHeader ( hostname : string ) : string | undefined {
95+ return this . bearerAuthHeaderWithPeer ( hostname ) ?. authorization
96+ }
97+
98+ /**
99+ * authenticatedFetch is like `fetch`, but it also handles HTTP Peer ID
100+ * authentication with the server.
101+ *
102+ * If we have not seen the server before, verifyPeer will be called to check
103+ * if we want to make the request to the server with the given peer id. This
104+ * happens after we've authenticated the server.
105+ */
106+ public async authenticatedFetch ( request : string | URL | Request , options ?: AuthenticatedFetchOptions ) : Promise < Response & { peer : PeerId } > {
107+ const { fetch, hostname, verifyPeer, ...requestOpts } = options ?? { }
108+ let req : Request
109+ if ( request instanceof Request && Object . keys ( requestOpts ) . length === 0 ) {
110+ req = request
111+ } else {
112+ req = new Request ( request , requestOpts )
113+ }
114+ const verifyPeerWithDefault = verifyPeer ?? ( ( ) => true )
115+
116+ const { response, peer } = await this . doAuthenticatedFetch ( req , verifyPeerWithDefault , { fetch, hostname } )
117+
118+ const responseWithPeer : Response & { peer : PeerId } = response as Response & { peer : PeerId }
119+ responseWithPeer . peer = peer
120+ return responseWithPeer
121+ }
122+
123+ private async doAuthenticatedFetch ( request : Request , verifyPeer : ( server : PeerId , options : AbortOptions ) => boolean | Promise < boolean > , options ?: DoAuthenticatedFetchOptions ) : Promise < { peer : PeerId , response : Response } > {
124+ const authEndpointURI = new URL ( request . url )
68125 const hostname = options ?. hostname ?? authEndpointURI . host
126+ const fetch = options ?. fetch ?? globalThis . fetch
69127
70128 if ( this . tokens . has ( hostname ) ) {
71- const token = this . tokens . get ( hostname )
72- if ( token !== undefined && Date . now ( ) - token . creationTime . getTime ( ) < this . tokenTTL ) {
73- return token . peer
129+ const token = this . bearerAuthHeaderWithPeer ( hostname )
130+ if ( token !== undefined ) {
131+ request . headers . set ( 'Authorization' , token . authorization )
132+ return { peer : token . peer , response : await fetch ( request ) }
74133 } else {
75134 this . tokens . delete ( hostname )
76135 }
@@ -87,16 +146,15 @@ export class ClientAuth {
87146 } )
88147 }
89148
90- const fetch = options ?. fetch ?? globalThis . fetch
91149 const resp = await fetch ( authEndpointURI , {
92150 headers,
93- signal : options ? .signal
151+ signal : request . signal
94152 } )
95153
96154 // Verify the server's challenge
97155 const authHeader = resp . headers . get ( 'www-authenticate' )
98156 if ( authHeader == null ) {
99- throw new Error ( 'No auth header' )
157+ throw new MissingAuthHeaderError ( 'No auth header' )
100158 }
101159 const authFields = parseHeader ( authHeader )
102160 const serverPubKeyBytes = uint8ArrayFromString ( authFields [ 'public-key' ] , 'base64urlpad' )
@@ -107,7 +165,14 @@ export class ClientAuth {
107165 [ 'client-public-key' , marshaledClientPubKey ] ,
108166 [ 'challenge-server' , challengeServer ] ] , uint8ArrayFromString ( authFields . sig , 'base64urlpad' ) )
109167 if ( ! valid ) {
110- throw new Error ( 'Invalid signature' )
168+ throw new InvalidSignatureError ( 'Invalid signature' )
169+ }
170+
171+ const serverPublicKey = publicKeyFromProtobuf ( serverPubKeyBytes )
172+ const serverID = peerIdFromPublicKey ( serverPublicKey )
173+
174+ if ( ! await verifyPeer ( serverID , { signal : request . signal } ) ) {
175+ throw new InvalidPeerError ( 'Id check failed' )
111176 }
112177
113178 const sig = await sign ( this . key , PeerIDAuthScheme , [
@@ -120,31 +185,30 @@ export class ClientAuth {
120185 sig : uint8ArrayToString ( sig , 'base64urlpad' )
121186 } )
122187
123- const resp2 = await fetch ( authEndpointURI , {
124- headers : {
125- Authorization : authenticateSelfHeaders
126- } ,
127- signal : options ?. signal
128- } )
188+ request . headers . set ( 'Authorization' , authenticateSelfHeaders )
189+ const resp2 = await fetch ( request )
190+
191+ if ( ! resp2 . ok ) {
192+ throw new BadResponseError ( `Unexpected status code ${ resp . status } ` )
193+ }
129194
130- // Verify the server's signature
131195 const serverAuthHeader = resp2 . headers . get ( 'Authentication-Info' )
132196 if ( serverAuthHeader == null ) {
133- throw new Error ( 'No server auth header' )
134- }
135- if ( resp2 . status !== 200 ) {
136- throw new Error ( 'Unexpected status code' )
197+ throw new MissingAuthHeaderError ( 'No server auth header' )
137198 }
138199
139200 const serverAuthFields = parseHeader ( serverAuthHeader )
140- const serverPublicKey = publicKeyFromProtobuf ( serverPubKeyBytes )
141- const serverID = peerIdFromPublicKey ( serverPublicKey )
142201 this . tokens . set ( hostname , {
143202 peer : serverID ,
144203 creationTime : new Date ( ) ,
145204 bearer : serverAuthFields . bearer
146205 } )
147206
148- return serverID
207+ return { peer : serverID , response : resp2 }
208+ }
209+
210+ public async authenticateServer ( authEndpointURI : string | URL , options ?: AuthenticateServerOptions ) : Promise < PeerId > {
211+ const req = new Request ( authEndpointURI , { signal : options ?. signal } )
212+ return ( await this . authenticatedFetch ( req , options ) ) . peer
149213 }
150214}
0 commit comments