Skip to content

Commit

Permalink
AuthCallout request should include TLS data when client is NATS WS cl…
Browse files Browse the repository at this point in the history
…ient (#4544)

Make sure the client handshake flag is set when TLS handshake is made as
part of WebSocket connection/upgrade (notionally HTTPS) rather than as
part of the NATS protocol TLS initiation chain. AuthCallout tests the
flag when building the data for the AuthCallout service request.

Added AuthCallout unit test for NATS WS client auth that requires the
TLS data.
  • Loading branch information
derekcollison committed Sep 15, 2023
2 parents 0af378c + aed9944 commit a5344c0
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 9 deletions.
103 changes: 103 additions & 0 deletions server/auth_callout_test.go
Expand Up @@ -152,6 +152,30 @@ func (at *authTest) Connect(clientOptions ...nats.Option) *nats.Conn {
return conn
}

func (at *authTest) WSNewClient(clientOptions ...nats.Option) (*nats.Conn, error) {
pi := at.srv.PortsInfo(10 * time.Millisecond)
require_False(at.t, pi == nil)

// test cert is SAN to DNS localhost, not local IPs returned by server in test environments
wssUrl := strings.Replace(pi.WebSocket[0], "127.0.0.1", "localhost", 1)

// Seeing 127.0.1.1 in some test environments...
wssUrl = strings.Replace(wssUrl, "127.0.1.1", "localhost", 1)

conn, err := nats.Connect(wssUrl, clientOptions...)
if err != nil {
return nil, err
}
at.clients = append(at.clients, conn)
return conn, nil
}

func (at *authTest) WSConnect(clientOptions ...nats.Option) *nats.Conn {
conn, err := at.WSNewClient(clientOptions...)
require_NoError(at.t, err)
return conn
}

func (at *authTest) RequireConnectError(clientOptions ...nats.Option) {
_, err := at.NewClient(clientOptions...)
require_Error(at.t, err)
Expand Down Expand Up @@ -1423,3 +1447,82 @@ func TestAuthCalloutOperator_AnyAccount(t *testing.T) {
userInfo = response.Data.(*UserInfo)
require_Equal(t, userInfo.Account, bpk)
}

func TestAuthCalloutWSClientTLSCerts(t *testing.T) {
conf := `
server_name: T
listen: "localhost:-1"
tls {
cert_file = "../test/configs/certs/tlsauth/server.pem"
key_file = "../test/configs/certs/tlsauth/server-key.pem"
ca_file = "../test/configs/certs/tlsauth/ca.pem"
verify = true
}
websocket: {
listen: "localhost:-1"
tls {
cert_file = "../test/configs/certs/tlsauth/server.pem"
key_file = "../test/configs/certs/tlsauth/server-key.pem"
ca_file = "../test/configs/certs/tlsauth/ca.pem"
verify = true
}
}
accounts {
AUTH { users [ {user: "auth", password: "pwd"} ] }
FOO {}
}
authorization {
timeout: 1s
auth_callout {
# Needs to be a public account nkey, will work for both server config and operator mode.
issuer: "ABJHLOVMPA4CI6R5KLNGOB4GSLNIY7IOUPAJC4YFNDLQVIOBYQGUWVLA"
account: AUTH
auth_users: [ auth ]
}
}
`
handler := func(m *nats.Msg) {
user, si, ci, _, ctls := decodeAuthRequest(t, m.Data)
require_Equal(t, si.Name, "T")
require_Equal(t, ci.Host, "127.0.0.1")
require_NotEqual(t, ctls, nil)
// Zero since we are verified and will be under verified chains.
require_Equal(t, len(ctls.Certs), 0)
require_Equal(t, len(ctls.VerifiedChains), 1)
// Since we have a CA.
require_Equal(t, len(ctls.VerifiedChains[0]), 2)
blk, _ := pem.Decode([]byte(ctls.VerifiedChains[0][0]))
cert, err := x509.ParseCertificate(blk.Bytes)
require_NoError(t, err)
if strings.HasPrefix(cert.Subject.String(), "CN=example.com") {
// Override blank name here, server will substitute.
ujwt := createAuthUser(t, user, "dlc", "FOO", "", nil, 0, nil)
m.Respond(serviceResponse(t, user, si.ID, ujwt, "", 0))
}
}

ac := NewAuthTest(t, conf, handler,
nats.UserInfo("auth", "pwd"),
nats.ClientCert("../test/configs/certs/tlsauth/client2.pem", "../test/configs/certs/tlsauth/client2-key.pem"),
nats.RootCAs("../test/configs/certs/tlsauth/ca.pem"))
defer ac.Cleanup()

// Will use client cert to determine user.
nc := ac.WSConnect(
nats.ClientCert("../test/configs/certs/tlsauth/client2.pem", "../test/configs/certs/tlsauth/client2-key.pem"),
nats.RootCAs("../test/configs/certs/tlsauth/ca.pem"),
)

resp, err := nc.Request(userDirectInfoSubj, nil, time.Second)
require_NoError(t, err)
response := ServerAPIResponse{Data: &UserInfo{}}
err = json.Unmarshal(resp.Data, &response)
require_NoError(t, err)
userInfo := response.Data.(*UserInfo)

require_Equal(t, userInfo.UserID, "dlc")
require_Equal(t, userInfo.Account, "FOO")
}
13 changes: 7 additions & 6 deletions server/monitor.go
Expand Up @@ -561,12 +561,13 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool)
// Exclude clients that are still doing handshake so we don't block in
// ConnectionState().
if client.flags.isSet(handshakeComplete) && nc != nil {
conn := nc.(*tls.Conn)
cs := conn.ConnectionState()
ci.TLSVersion = tlsVersion(cs.Version)
ci.TLSCipher = tlsCipher(cs.CipherSuite)
if auth && len(cs.PeerCertificates) > 0 {
ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates)
if conn, ok := nc.(*tls.Conn); ok {
cs := conn.ConnectionState()
ci.TLSVersion = tlsVersion(cs.Version)
ci.TLSCipher = tlsCipher(cs.CipherSuite)
if auth && len(cs.PeerCertificates) > 0 {
ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates)
}
}
}

Expand Down
8 changes: 5 additions & 3 deletions server/websocket.go
Expand Up @@ -1234,12 +1234,14 @@ func (s *Server) createWSClient(conn net.Conn, ws *websocket) *client {
return nil
}
s.clients[c.cid] = c

// Websocket clients do TLS in the websocket http server.
// So no TLS here...
s.mu.Unlock()

c.mu.Lock()
// Websocket clients do TLS in the websocket http server.
// So no TLS initiation here...
if _, ok := conn.(*tls.Conn); ok {
c.flags.set(handshakeComplete)
}

if c.isClosed() {
c.mu.Unlock()
Expand Down

0 comments on commit a5344c0

Please sign in to comment.