mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
Changed account lookup and validation failures to be more understandable by users. Changed limits to be -1 for unlimited to match jwt pkg. The limits changed exposed problems with options holding real objects causing issues with reload tests under race mode. Longer term this code should be reworked such that options only hold config data, not real structs, etc. Signed-off-by: Derek Collison <derek@nats.io>
471 lines
12 KiB
Go
471 lines
12 KiB
Go
// Copyright 2012-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 (
|
|
"crypto/tls"
|
|
"encoding/base64"
|
|
"strings"
|
|
|
|
"github.com/nats-io/jwt"
|
|
"github.com/nats-io/nkeys"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// Authentication is an interface for implementing authentication
|
|
type Authentication interface {
|
|
// Check if a client is authorized to connect
|
|
Check(c ClientAuthentication) bool
|
|
}
|
|
|
|
// ClientAuthentication is an interface for client authentication
|
|
type ClientAuthentication interface {
|
|
// Get options associated with a client
|
|
GetOpts() *clientOpts
|
|
// If TLS is enabled, TLS ConnectionState, nil otherwise
|
|
GetTLSConnectionState() *tls.ConnectionState
|
|
// Optionally map a user after auth.
|
|
RegisterUser(*User)
|
|
}
|
|
|
|
// NkeyUser is for multiple nkey based users
|
|
type NkeyUser struct {
|
|
Nkey string `json:"user"`
|
|
Permissions *Permissions `json:"permissions,omitempty"`
|
|
Account *Account `json:"account,omitempty"`
|
|
}
|
|
|
|
// User is for multiple accounts/users.
|
|
type User struct {
|
|
Username string `json:"user"`
|
|
Password string `json:"password"`
|
|
Permissions *Permissions `json:"permissions,omitempty"`
|
|
Account *Account `json:"account,omitempty"`
|
|
}
|
|
|
|
// clone performs a deep copy of the User struct, returning a new clone with
|
|
// all values copied.
|
|
func (u *User) clone() *User {
|
|
if u == nil {
|
|
return nil
|
|
}
|
|
clone := &User{}
|
|
*clone = *u
|
|
clone.Permissions = u.Permissions.clone()
|
|
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 {
|
|
Allow []string `json:"allow,omitempty"`
|
|
Deny []string `json:"deny,omitempty"`
|
|
}
|
|
|
|
// Permissions are the allowed subjects on a per
|
|
// publish or subscribe basis.
|
|
type Permissions struct {
|
|
Publish *SubjectPermission `json:"publish"`
|
|
Subscribe *SubjectPermission `json:"subscribe"`
|
|
}
|
|
|
|
// RoutePermissions are similar to user permissions
|
|
// but describe what a server can import/export from and to
|
|
// another server.
|
|
type RoutePermissions struct {
|
|
Import *SubjectPermission `json:"import"`
|
|
Export *SubjectPermission `json:"export"`
|
|
}
|
|
|
|
// GatewayPermissions are similar to RoutePermissions
|
|
type GatewayPermissions = RoutePermissions
|
|
|
|
// clone will clone a RoutePermissions object
|
|
func (rp *RoutePermissions) clone() *RoutePermissions {
|
|
if rp == nil {
|
|
return nil
|
|
}
|
|
clone := &RoutePermissions{}
|
|
if rp.Import != nil {
|
|
clone.Import = rp.Import.clone()
|
|
}
|
|
if rp.Export != nil {
|
|
clone.Export = rp.Export.clone()
|
|
}
|
|
return clone
|
|
}
|
|
|
|
// clone will clone an individual subject permission.
|
|
func (p *SubjectPermission) clone() *SubjectPermission {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
clone := &SubjectPermission{}
|
|
if p.Allow != nil {
|
|
clone.Allow = make([]string, len(p.Allow))
|
|
copy(clone.Allow, p.Allow)
|
|
}
|
|
if p.Deny != nil {
|
|
clone.Deny = make([]string, len(p.Deny))
|
|
copy(clone.Deny, p.Deny)
|
|
}
|
|
return clone
|
|
}
|
|
|
|
// clone performs a deep copy of the Permissions struct, returning a new clone
|
|
// with all values copied.
|
|
func (p *Permissions) clone() *Permissions {
|
|
if p == nil {
|
|
return nil
|
|
}
|
|
clone := &Permissions{}
|
|
if p.Publish != nil {
|
|
clone.Publish = p.Publish.clone()
|
|
}
|
|
if p.Subscribe != nil {
|
|
clone.Subscribe = p.Subscribe.clone()
|
|
}
|
|
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.")
|
|
}
|
|
}
|
|
|
|
// If opts.Users or opts.Nkeys have definitions without an account
|
|
// defined assign them to the default global account.
|
|
// Lock should be held.
|
|
func (s *Server) assignGlobalAccountToOrphanUsers() {
|
|
for _, u := range s.users {
|
|
if u.Account == nil {
|
|
u.Account = s.gacc
|
|
}
|
|
}
|
|
for _, u := range s.nkeys {
|
|
if u.Account == nil {
|
|
u.Account = s.gacc
|
|
}
|
|
}
|
|
}
|
|
|
|
// configureAuthorization will do any setup needed for authorization.
|
|
// Lock is assumed held.
|
|
func (s *Server) configureAuthorization() {
|
|
if s.opts == nil {
|
|
return
|
|
}
|
|
|
|
// Snapshot server options.
|
|
opts := s.getOpts()
|
|
|
|
// Check for multiple users first
|
|
// This just checks and sets up the user map if we have multiple users.
|
|
if opts.CustomClientAuthentication != nil {
|
|
s.info.AuthRequired = true
|
|
} else if len(s.trustedKeys) > 0 {
|
|
s.info.AuthRequired = true
|
|
} 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 {
|
|
copy := u.clone()
|
|
if u.Account != nil {
|
|
copy.Account = s.accounts[u.Account.Name]
|
|
}
|
|
s.nkeys[u.Nkey] = copy
|
|
}
|
|
}
|
|
if opts.Users != nil {
|
|
s.users = make(map[string]*User)
|
|
for _, u := range opts.Users {
|
|
copy := u.clone()
|
|
if u.Account != nil {
|
|
copy.Account = s.accounts[u.Account.Name]
|
|
}
|
|
s.users[u.Username] = copy
|
|
}
|
|
}
|
|
s.assignGlobalAccountToOrphanUsers()
|
|
s.info.AuthRequired = true
|
|
} else if opts.Username != "" || opts.Authorization != "" {
|
|
s.info.AuthRequired = true
|
|
} else {
|
|
s.users = nil
|
|
s.info.AuthRequired = false
|
|
}
|
|
}
|
|
|
|
// checkAuthentication will check based on client type and
|
|
// return boolean indicating if client is authorized.
|
|
func (s *Server) checkAuthentication(c *client) bool {
|
|
switch c.kind {
|
|
case CLIENT:
|
|
return s.isClientAuthorized(c)
|
|
case ROUTER:
|
|
return s.isRouterAuthorized(c)
|
|
case GATEWAY:
|
|
return s.isGatewayAuthorized(c)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// isClientAuthorized will check the client against the proper authorization method and data.
|
|
// This could be nkey, token, or username/password based.
|
|
func (s *Server) isClientAuthorized(c *client) bool {
|
|
// 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 jwts, then nkeys, then multiple users, then token, then single user/pass.
|
|
if customClientAuthentication != nil {
|
|
return customClientAuthentication.Check(c)
|
|
}
|
|
|
|
// Grab under lock but process after.
|
|
var (
|
|
nkey *NkeyUser
|
|
juc *jwt.UserClaims
|
|
acc *Account
|
|
user *User
|
|
ok bool
|
|
err error
|
|
)
|
|
|
|
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 trustedKeys defined in the server. If so we require a user jwt.
|
|
if s.trustedKeys != nil {
|
|
if c.opts.JWT == "" {
|
|
s.mu.Unlock()
|
|
c.Debugf("Authentication requires a user JWT")
|
|
return false
|
|
}
|
|
// So we have a valid user jwt here.
|
|
juc, err = jwt.DecodeUserClaims(c.opts.JWT)
|
|
if err != nil {
|
|
s.mu.Unlock()
|
|
c.Debugf("User JWT not valid: %v", err)
|
|
return false
|
|
}
|
|
vr := jwt.CreateValidationResults()
|
|
juc.Validate(vr)
|
|
if vr.IsBlocking(true) {
|
|
s.mu.Unlock()
|
|
c.Debugf("User JWT no longer valid: %+v", vr)
|
|
return false
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
|
|
// If we have a jwt and a userClaim, make sure we have the Account, etc associated.
|
|
// We need to look up the account. This will use an account resolver if one is present.
|
|
if juc != nil {
|
|
if acc, _ = s.LookupAccount(juc.Issuer); acc == nil {
|
|
c.Debugf("Account JWT can not be found")
|
|
return false
|
|
}
|
|
if !s.isTrustedIssuer(acc.Issuer) {
|
|
c.Debugf("Account JWT not signed by trusted operator")
|
|
return false
|
|
}
|
|
if acc.IsExpired() {
|
|
c.Debugf("Account JWT has expired")
|
|
return false
|
|
}
|
|
// Verify the signature against the nonce.
|
|
if c.opts.Sig == "" {
|
|
c.Debugf("Signature missing")
|
|
return false
|
|
}
|
|
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
|
|
if err != nil {
|
|
c.Debugf("Signature not valid base64")
|
|
return false
|
|
}
|
|
pub, err := nkeys.FromPublicKey(juc.Subject)
|
|
if err != nil {
|
|
c.Debugf("User nkey not valid: %v", err)
|
|
return false
|
|
}
|
|
if err := pub.Verify(c.nonce, sig); err != nil {
|
|
c.Debugf("Signature not verified")
|
|
return false
|
|
}
|
|
nkey = buildInternalNkeyUser(juc, acc)
|
|
c.RegisterNkeyUser(nkey)
|
|
|
|
// Generate an event if we have a system account.
|
|
s.accountConnectEvent(c)
|
|
|
|
// Check if we need to set an auth timer if the user jwt expires.
|
|
c.checkExpiration(juc.Claims())
|
|
return true
|
|
}
|
|
|
|
if nkey != nil {
|
|
if c.opts.Sig == "" {
|
|
c.Debugf("Signature missing")
|
|
return false
|
|
}
|
|
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
|
|
if err != nil {
|
|
c.Debugf("Signature not valid base64")
|
|
return false
|
|
}
|
|
pub, err := nkeys.FromPublicKey(c.opts.Nkey)
|
|
if err != nil {
|
|
c.Debugf("User nkey not valid: %v", err)
|
|
return false
|
|
}
|
|
if err := pub.Verify(c.nonce, sig); err != nil {
|
|
c.Debugf("Signature not verified")
|
|
return false
|
|
}
|
|
c.RegisterNkeyUser(nkey)
|
|
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
|
|
}
|
|
|
|
if authorization != "" {
|
|
return comparePasswords(authorization, c.opts.Authorization)
|
|
} else if username != "" {
|
|
if username != c.opts.Username {
|
|
return false
|
|
}
|
|
return comparePasswords(password, c.opts.Password)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// checkRouterAuth checks optional router authorization which can be nil or username/password.
|
|
func (s *Server) isRouterAuthorized(c *client) bool {
|
|
// Snapshot server options.
|
|
opts := s.getOpts()
|
|
|
|
if s.opts.CustomRouterAuthentication != nil {
|
|
return s.opts.CustomRouterAuthentication.Check(c)
|
|
}
|
|
|
|
if opts.Cluster.Username == "" {
|
|
return true
|
|
}
|
|
|
|
if opts.Cluster.Username != c.opts.Username {
|
|
return false
|
|
}
|
|
if !comparePasswords(opts.Cluster.Password, c.opts.Password) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isGatewayAuthorized checks optional gateway authorization which can be nil or username/password.
|
|
func (s *Server) isGatewayAuthorized(c *client) bool {
|
|
// Snapshot server options.
|
|
opts := s.getOpts()
|
|
if opts.Gateway.Username == "" {
|
|
return true
|
|
}
|
|
if opts.Gateway.Username != c.opts.Username {
|
|
return false
|
|
}
|
|
return comparePasswords(opts.Gateway.Password, c.opts.Password)
|
|
}
|
|
|
|
// Support for bcrypt stored passwords and tokens.
|
|
const bcryptPrefix = "$2a$"
|
|
|
|
// isBcrypt checks whether the given password or token is bcrypted.
|
|
func isBcrypt(password string) bool {
|
|
return strings.HasPrefix(password, bcryptPrefix)
|
|
}
|
|
|
|
func comparePasswords(serverPassword, clientPassword string) bool {
|
|
// Check to see if the server password is a bcrypt hash
|
|
if isBcrypt(serverPassword) {
|
|
if err := bcrypt.CompareHashAndPassword([]byte(serverPassword), []byte(clientPassword)); err != nil {
|
|
return false
|
|
}
|
|
} else if serverPassword != clientPassword {
|
|
return false
|
|
}
|
|
return true
|
|
}
|