Skip to content

Commit

Permalink
Merge 129866b into 5bfbc8b
Browse files Browse the repository at this point in the history
  • Loading branch information
derekcollison committed Sep 10, 2018
2 parents 5bfbc8b + 129866b commit d47ee3e
Show file tree
Hide file tree
Showing 9 changed files with 720 additions and 64 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -5,6 +5,7 @@ go:
- 1.11
install:
- go get github.com/nats-io/go-nats
- go get github.com/nats-io/nkeys
- go get github.com/mattn/goveralls
- go get github.com/wadey/gocovmerge
- go get -u honnef.co/go/tools/cmd/megacheck
Expand Down
145 changes: 117 additions & 28 deletions server/auth.go
Expand Up @@ -15,9 +15,11 @@ package server

import (
"crypto/tls"
"encoding/base64"
"fmt"
"strings"

"github.com/nats-io/nkeys"
"golang.org/x/crypto/bcrypt"
)

Expand All @@ -37,6 +39,12 @@ type ClientAuthentication interface {
RegisterUser(*User)
}

// Nkey is for multiple nkey based users
type NkeyUser struct {
Nkey string `json:"user"`
Permissions *Permissions `json:"permissions"`
}

// User is for multiple accounts/users.
type User struct {
Username string `json:"user"`
Expand All @@ -56,6 +64,18 @@ func (u *User) clone() *User {
return clone
}

// clone performs a deep copy of the NkeyUser struct, returning a new clone with
// all values copied.
func (n *NkeyUser) clone() *NkeyUser {
if n == nil {
return nil
}
clone := &NkeyUser{}
*clone = *n
clone.Permissions = n.Permissions.clone()
return clone
}

// SubjectPermission is an individual allow and deny struct for publish
// and subscribe authorizations.
type SubjectPermission struct {
Expand Down Expand Up @@ -111,6 +131,25 @@ func (p *Permissions) clone() *Permissions {
return clone
}

// checkAuthforWarnings will look for insecure settings and log concerns.
// Lock is assumed held.
func (s *Server) checkAuthforWarnings() {
warn := false
if s.opts.Password != "" && !isBcrypt(s.opts.Password) {
warn = true
}
for _, u := range s.users {
if !isBcrypt(u.Password) {
warn = true
break
}
}
if warn {
// Warning about using plaintext passwords.
s.Warnf("Plaintext passwords detected. Use Nkeys or Bcrypt passwords in config files.")
}
}

// configureAuthorization will do any setup needed for authorization.
// Lock is assumed held.
func (s *Server) configureAuthorization() {
Expand All @@ -125,10 +164,19 @@ func (s *Server) configureAuthorization() {
// This just checks and sets up the user map if we have multiple users.
if opts.CustomClientAuthentication != nil {
s.info.AuthRequired = true
} else if opts.Users != nil {
s.users = make(map[string]*User)
for _, u := range opts.Users {
s.users[u.Username] = u
} else if opts.Nkeys != nil || opts.Users != nil {
// Support both at the same time.
if opts.Nkeys != nil {
s.nkeys = make(map[string]*NkeyUser)
for _, u := range opts.Nkeys {
s.nkeys[u.Nkey] = u
}
}
if opts.Users != nil {
s.users = make(map[string]*User)
for _, u := range opts.Users {
s.users[u.Username] = u
}
}
s.info.AuthRequired = true
} else if opts.Username != "" || opts.Authorization != "" {
Expand All @@ -152,50 +200,91 @@ func (s *Server) checkAuthorization(c *client) bool {
}
}

// hasUsers leyt's us know if we have a users array.
func (s *Server) hasUsers() bool {
s.mu.Lock()
hu := s.users != nil
s.mu.Unlock()
return hu
}

// isClientAuthorized will check the client against the proper authorization method and data.
// This could be token or username/password based.
// This could be nkey, token, or username/password based.
func (s *Server) isClientAuthorized(c *client) bool {
// Snapshot server options.
opts := s.getOpts()
// Snapshot server options by hand and only grab what we really need.
s.optsMu.RLock()
customClientAuthentication := s.opts.CustomClientAuthentication
authorization := s.opts.Authorization
username := s.opts.Username
password := s.opts.Password
s.optsMu.RUnlock()

// Check custom auth first, then nkeys, then multiple users, then token, then single user/pass.
if customClientAuthentication != nil {
return customClientAuthentication.Check(c)
}

// Check custom auth first, then multiple users, then token, then single user/pass.
if opts.CustomClientAuthentication != nil {
return opts.CustomClientAuthentication.Check(c)
} else if s.hasUsers() {
s.mu.Lock()
user, ok := s.users[c.opts.Username]
var nkey *NkeyUser
var user *User
var ok bool

s.mu.Lock()
authRequired := s.info.AuthRequired
if !authRequired {
// TODO(dlc) - If they send us credentials should we fail?
s.mu.Unlock()
return true
}

// Check if we have nkeys or users for client.
hasNkeys := s.nkeys != nil
hasUsers := s.users != nil
if hasNkeys && c.opts.Nkey != "" {
nkey, ok = s.nkeys[c.opts.Nkey]
if !ok {
s.mu.Unlock()
return false
}
} else if hasUsers && c.opts.Username != "" {
user, ok = s.users[c.opts.Username]
if !ok {
s.mu.Unlock()
return false
}
}
s.mu.Unlock()

// Verify the signature against the nonce.
if nkey != nil {
if c.opts.Sig == "" {
return false
}
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
if err != nil {
return false
}
pub, err := nkeys.FromPublicKey(c.opts.Nkey)
if err != nil {
return false
}
if err := pub.Verify(c.nonce, sig); err != nil {
return false
}
return true
}

if user != nil {
ok = comparePasswords(user.Password, c.opts.Password)
// If we are authorized, register the user which will properly setup any permissions
// for pub/sub authorizations.
if ok {
c.RegisterUser(user)
}
return ok
}

} else if opts.Authorization != "" {
return comparePasswords(opts.Authorization, c.opts.Authorization)

} else if opts.Username != "" {
if opts.Username != c.opts.Username {
if authorization != "" {
return comparePasswords(authorization, c.opts.Authorization)
} else if username != "" {
if username != c.opts.Username {
return false
}
return comparePasswords(opts.Password, c.opts.Password)
return comparePasswords(password, c.opts.Password)
}

return true
return false
}

// checkRouterAuth checks optional router authorization which can be nil or username/password.
Expand Down
39 changes: 35 additions & 4 deletions server/client.go
Expand Up @@ -21,6 +21,7 @@ import (
"io"
"math/rand"
"net"
"regexp"
"sync"
"sync/atomic"
"time"
Expand Down Expand Up @@ -137,6 +138,7 @@ type client struct {
cid uint64
opts clientOpts
start time.Time
nonce []byte
nc net.Conn
ncs string
out outbound
Expand Down Expand Up @@ -246,9 +248,11 @@ type clientOpts struct {
Verbose bool `json:"verbose"`
Pedantic bool `json:"pedantic"`
TLSRequired bool `json:"tls_required"`
Authorization string `json:"auth_token"`
Username string `json:"user"`
Password string `json:"pass"`
Nkey string `json:"nkey,omitempty"`
Sig string `json:"sig,omitempty"`
Authorization string `json:"auth_token,omitempty"`
Username string `json:"user,omitempty"`
Password string `json:"pass,omitempty"`
Name string `json:"name"`
Lang string `json:"lang"`
Version string `json:"version"`
Expand Down Expand Up @@ -701,8 +705,35 @@ func (c *client) processErr(errStr string) {
c.closeConnection(ParseError)
}

// Password pattern matcher.
var passPat = regexp.MustCompile(`"?\s*pass\S*\s*"?\s*[:=]\s*("?[^\s,}$]*)`)

// This will remove any notion of passwords from trace messages
// for logging.
func removePassFromTrace(arg []byte) []byte {

if !bytes.Contains(arg, []byte("pass")) {
return arg
}
m := passPat.FindAllSubmatch(arg, -1)
if len(m) == 0 {
return arg
}

for _, match := range m {
if len(match) != 2 {
continue
}
arg = bytes.Replace(arg, match[1], []byte("[REDACTED]"), 1)

}
return arg
}

func (c *client) processConnect(arg []byte) error {
c.traceInOp("CONNECT", arg)
if c.trace {
c.traceInOp("CONNECT", removePassFromTrace(arg))
}

c.mu.Lock()
// If we can't stop the timer because the callback is in progress...
Expand Down
51 changes: 51 additions & 0 deletions server/log_test.go
Expand Up @@ -14,6 +14,7 @@
package server

import (
"bytes"
"fmt"
"io/ioutil"
"os"
Expand Down Expand Up @@ -181,3 +182,53 @@ func TestReOpenLogFile(t *testing.T) {
t.Fatalf("New message was not appended after file was re-opened, got: %v", string(buf))
}
}

func TestNoPasswordsFromConnectTrace(t *testing.T) {
opts := DefaultOptions()
opts.NoLog = false
opts.Trace = true
opts.Username = "derek"
opts.Password = "s3cr3t"

s := &Server{opts: opts}
dl := &DummyLogger{}
s.SetLogger(dl, false, true)

_ = s.logging.logger.(*DummyLogger)
if s.logging.trace != 1 {
t.Fatalf("Expected trace 1, received value %d\n", s.logging.trace)
}
defer s.SetLogger(nil, false, false)

c, _, _ := newClientForServer(s)

connectOp := []byte("CONNECT {\"user\":\"derek\",\"pass\":\"s3cr3t\"}\r\n")
err := c.parse(connectOp)
if err != nil {
t.Fatalf("Received error: %v\n", err)
}

dl.Lock()
hasPass := strings.Contains(dl.msg, "s3cr3t")
dl.Unlock()

if hasPass {
t.Fatalf("Password detected in log output: %s", dl.msg)
}
}

func TestRemovePassFromTrace(t *testing.T) {
pass := []byte("s3cr3t")
check := func(r []byte) {
t.Helper()
if bytes.Contains(r, pass) {
t.Fatalf("Found password in %q", r)
}
}
check(removePassFromTrace([]byte("CONNECT {\"user\":\"derek\",\"pass\":\"s3cr3t\"}\r\n")))
check(removePassFromTrace([]byte("CONNECT {\"user\":\"derek\",\"pass\": \"s3cr3t\"}\r\n")))
check(removePassFromTrace([]byte("CONNECT {\"user\":\"derek\",\"pass\": \"s3cr3t\" }\r\n")))
check(removePassFromTrace([]byte("CONNECT {\"password\":\"s3cr3t\",}\r\n")))
check(removePassFromTrace([]byte("CONNECT {pass:s3cr3t\r\n")))
check(removePassFromTrace([]byte("CONNECT {pass:s3cr3t , password = s3cr3t}")))
}
39 changes: 39 additions & 0 deletions server/nkey.go
@@ -0,0 +1,39 @@
// Copyright 2018 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package server

import (
"encoding/base64"
)

// Raw length of the nonce challenge
const (
nonceRawLen = 16
nonceLen = 22 // base64.RawURLEncoding.EncodedLen(nonceRawLen)
)

// nonceRequired tells us if we should send a nonce.
// Assumes server lock is held
func (s *Server) nonceRequired() bool {
return len(s.opts.Nkeys) > 0
}

// Generate a nonce for INFO challenge.
// Assumes server lock is held
func (s *Server) generateNonce(n []byte) {
var raw [nonceRawLen]byte
data := raw[:]
s.prand.Read(data)
base64.RawURLEncoding.Encode(n, data)
}

0 comments on commit d47ee3e

Please sign in to comment.