Skip to content

Commit 3e51326

Browse files
authored
feat(libp2phttp): More ergonomic auth (#3188)
1 parent 93a1d3f commit 3e51326

File tree

6 files changed

+344
-14
lines changed

6 files changed

+344
-14
lines changed

p2p/http/auth/auth_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func TestMutualAuth(t *testing.T) {
137137
req.Host = "example.com"
138138
serverID, resp, err = clientAuth.AuthenticatedDo(client, req)
139139
require.NotEmpty(t, req.Header.Get("Authorization"))
140+
require.True(t, HasAuthHeader(req))
140141
require.NoError(t, err)
141142
require.Equal(t, expectedServerID, serverID)
142143
require.NotZero(t, clientAuth.tm.tokenMap["example.com"])

p2p/http/auth/client.go

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,25 @@ type ClientPeerIDAuth struct {
2020
tm tokenMap
2121
}
2222

23+
type clientAsRoundTripper struct {
24+
*http.Client
25+
}
26+
27+
func (c clientAsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
28+
return c.Client.Do(req)
29+
}
30+
2331
// AuthenticatedDo is like http.Client.Do, but it does the libp2p peer ID auth
2432
// handshake if needed.
2533
//
2634
// It is recommended to pass in an http.Request with `GetBody` set, so that this
2735
// method can retry sending the request in case a previously used token has
2836
// expired.
2937
func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Request) (peer.ID, *http.Response, error) {
38+
return a.AuthenticateWithRoundTripper(clientAsRoundTripper{client}, req)
39+
}
40+
41+
func (a *ClientPeerIDAuth) AuthenticateWithRoundTripper(rt http.RoundTripper, req *http.Request) (peer.ID, *http.Response, error) {
3042
hostname := req.Host
3143
ti, hasToken := a.tm.get(hostname, a.TokenTTL)
3244
handshake := handshake.PeerIDAuthHandshakeClient{
@@ -36,7 +48,7 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Reques
3648

3749
if hasToken {
3850
// We have a token. Attempt to use that, but fallback to server initiated challenge if it fails.
39-
peer, resp, err := a.doWithToken(client, req, ti)
51+
peer, resp, err := a.doWithToken(rt, req, ti)
4052
switch {
4153
case err == nil:
4254
return peer, resp, nil
@@ -62,7 +74,7 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Reques
6274
handshake.SetInitiateChallenge()
6375
}
6476

65-
serverPeerID, resp, err := a.runHandshake(client, req, clearBody(req), &handshake)
77+
serverPeerID, resp, err := a.runHandshake(rt, req, clearBody(req), &handshake)
6678
if err != nil {
6779
return "", nil, fmt.Errorf("failed to run handshake: %w", err)
6880
}
@@ -74,7 +86,12 @@ func (a *ClientPeerIDAuth) AuthenticatedDo(client *http.Client, req *http.Reques
7486
return serverPeerID, resp, nil
7587
}
7688

77-
func (a *ClientPeerIDAuth) runHandshake(client *http.Client, req *http.Request, b bodyMeta, hs *handshake.PeerIDAuthHandshakeClient) (peer.ID, *http.Response, error) {
89+
func (a *ClientPeerIDAuth) HasToken(hostname string) bool {
90+
_, hasToken := a.tm.get(hostname, a.TokenTTL)
91+
return hasToken
92+
}
93+
94+
func (a *ClientPeerIDAuth) runHandshake(rt http.RoundTripper, req *http.Request, b bodyMeta, hs *handshake.PeerIDAuthHandshakeClient) (peer.ID, *http.Response, error) {
7895
maxSteps := 5 // Avoid infinite loops in case of buggy handshake. Shouldn't happen.
7996
var resp *http.Response
8097

@@ -92,7 +109,7 @@ func (a *ClientPeerIDAuth) runHandshake(client *http.Client, req *http.Request,
92109
b.setBody(req)
93110
}
94111

95-
resp, err = client.Do(req)
112+
resp, err = rt.RoundTrip(req)
96113
if err != nil {
97114
return "", nil, err
98115
}
@@ -119,10 +136,10 @@ func (a *ClientPeerIDAuth) runHandshake(client *http.Client, req *http.Request,
119136

120137
var errTokenRejected = errors.New("token rejected")
121138

122-
func (a *ClientPeerIDAuth) doWithToken(client *http.Client, req *http.Request, ti tokenInfo) (peer.ID, *http.Response, error) {
139+
func (a *ClientPeerIDAuth) doWithToken(rt http.RoundTripper, req *http.Request, ti tokenInfo) (peer.ID, *http.Response, error) {
123140
// Try to make the request with the token
124141
req.Header.Set("Authorization", ti.token)
125-
resp, err := client.Do(req)
142+
resp, err := rt.RoundTrip(req)
126143
if err != nil {
127144
return "", nil, err
128145
}

p2p/http/auth/server.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"errors"
88
"hash"
99
"net/http"
10+
"strings"
1011
"sync"
1112
"time"
1213

@@ -60,6 +61,10 @@ type ServerPeerIDAuth struct {
6061
// scheme. If a Next handler is set, it will be called on authenticated
6162
// requests.
6263
func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
64+
a.ServeHTTPWithNextHandler(w, r, a.Next)
65+
}
66+
67+
func (a *ServerPeerIDAuth) ServeHTTPWithNextHandler(w http.ResponseWriter, r *http.Request, next func(peer.ID, http.ResponseWriter, *http.Request)) {
6368
a.initHmac.Do(func() {
6469
if a.HmacKey == nil {
6570
key := make([]byte, 32)
@@ -130,7 +135,7 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
130135
TokenTTL: a.TokenTTL,
131136
Hmac: hmac,
132137
}
133-
hs.Run()
138+
_ = hs.Run() // First run will never err
134139
hs.SetHeader(w.Header())
135140
w.WriteHeader(http.StatusUnauthorized)
136141

@@ -149,9 +154,16 @@ func (a *ServerPeerIDAuth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
149154
return
150155
}
151156

152-
if a.Next == nil {
157+
if next == nil {
153158
w.WriteHeader(http.StatusOK)
154159
return
155160
}
156-
a.Next(peer, w, r)
161+
next(peer, w, r)
162+
}
163+
164+
// HasAuthHeader checks if the HTTP request contains an Authorization header
165+
// that starts with the PeerIDAuthScheme prefix.
166+
func HasAuthHeader(r *http.Request) bool {
167+
h := r.Header.Get("Authorization")
168+
return h != "" && strings.HasPrefix(h, handshake.PeerIDAuthScheme)
157169
}

p2p/http/example_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,86 @@ import (
88
"net/http"
99
"regexp"
1010
"strings"
11+
"time"
1112

1213
"github.com/libp2p/go-libp2p"
14+
"github.com/libp2p/go-libp2p/core/crypto"
1315
"github.com/libp2p/go-libp2p/core/peer"
1416
libp2phttp "github.com/libp2p/go-libp2p/p2p/http"
17+
httpauth "github.com/libp2p/go-libp2p/p2p/http/auth"
1518
ma "github.com/multiformats/go-multiaddr"
1619
)
1720

21+
func ExampleHost_authenticatedHTTP() {
22+
clientKey, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 0)
23+
if err != nil {
24+
log.Fatal(err)
25+
}
26+
client := libp2phttp.Host{
27+
ClientPeerIDAuth: &httpauth.ClientPeerIDAuth{
28+
TokenTTL: time.Hour,
29+
PrivKey: clientKey,
30+
},
31+
}
32+
33+
serverKey, _, err := crypto.GenerateKeyPair(crypto.Ed25519, 0)
34+
if err != nil {
35+
log.Fatal(err)
36+
}
37+
server := libp2phttp.Host{
38+
ServerPeerIDAuth: &httpauth.ServerPeerIDAuth{
39+
PrivKey: serverKey,
40+
// No TLS for this example. In practice you want to use TLS.
41+
NoTLS: true,
42+
ValidHostnameFn: func(hostname string) bool {
43+
return strings.HasPrefix(hostname, "127.0.0.1")
44+
},
45+
TokenTTL: time.Hour,
46+
},
47+
// No TLS for this example. In practice you want to use TLS.
48+
InsecureAllowHTTP: true,
49+
ListenAddrs: []ma.Multiaddr{ma.StringCast("/ip4/127.0.0.1/tcp/0/http")},
50+
}
51+
52+
observedClientID := ""
53+
server.SetHTTPHandler("/echo-id", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
54+
observedClientID = libp2phttp.ClientPeerID(r).String()
55+
w.WriteHeader(http.StatusOK)
56+
}))
57+
58+
go server.Serve()
59+
defer server.Close()
60+
61+
expectedServerID, err := peer.IDFromPrivateKey(serverKey)
62+
if err != nil {
63+
log.Fatal(err)
64+
}
65+
66+
httpClient := http.Client{Transport: &client}
67+
url := fmt.Sprintf("multiaddr:%s/p2p/%s/http-path/echo-id", server.Addrs()[0], expectedServerID)
68+
resp, err := httpClient.Get(url)
69+
if err != nil {
70+
log.Fatal(err)
71+
}
72+
resp.Body.Close()
73+
74+
expectedClientID, err := peer.IDFromPrivateKey(clientKey)
75+
if err != nil {
76+
log.Fatal(err)
77+
}
78+
if observedClientID != expectedClientID.String() {
79+
log.Fatal("observedClientID does not match expectedClientID")
80+
}
81+
82+
observedServerID := libp2phttp.ServerPeerID(resp)
83+
if observedServerID != expectedServerID {
84+
log.Fatal("observedServerID does not match expectedServerID")
85+
}
86+
87+
fmt.Println("Successfully authenticated HTTP request")
88+
// Output: Successfully authenticated HTTP request
89+
}
90+
1891
func ExampleHost_withAStockGoHTTPClient() {
1992
server := libp2phttp.Host{
2093
InsecureAllowHTTP: true, // For our example, we'll allow insecure HTTP

0 commit comments

Comments
 (0)