mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
Along a leaf node connection, unless the system account is shared AND the JetStream domain name is identical, the default JetStream traffic (without a domain set) will be denied. As a consequence, all clients that wants to access a domain that is not the one in the server they are connected to, a domain name must be specified. Affected from this change are setups where: a leaf node had no local JetStream OR the server the leaf node connected to had no local JetStream. One of the two accounts that are connected via a leaf node remote, must have no JetStream enabled. The side that does not have JetStream enabled, will loose JetStream access and it's clients must set `nats.Domain` manually. For workarounds on how to restore the old behavior, look at: https://github.com/nats-io/nats-server/pull/2693#issuecomment-996212582 New config values added: `default_js_domain` is a mapping from account to domain, settable when JetStream is not enabled in an account. `extension_hint` are hints for non clustered server to start in clustered mode (and be usable to extend) `js_domain` is a way to set the JetStream domain to use for mqtt. Signed-off-by: Matthias Hanel <mh@synadia.com>
1163 lines
33 KiB
Go
1163 lines
33 KiB
Go
// Copyright 2012-2019 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/sha256"
|
|
"crypto/tls"
|
|
"crypto/x509/pkix"
|
|
"encoding/asn1"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/nats-io/jwt/v2"
|
|
"github.com/nats-io/nats-server/v2/internal/ldap"
|
|
"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 {
|
|
// GetOpts gets options associated with a client
|
|
GetOpts() *ClientOpts
|
|
// GetTLSConnectionState if TLS is enabled, TLS ConnectionState, nil otherwise
|
|
GetTLSConnectionState() *tls.ConnectionState
|
|
// RegisterUser optionally map a user after auth.
|
|
RegisterUser(*User)
|
|
// RemoteAddress expose the connection information of the client
|
|
RemoteAddress() net.Addr
|
|
// GetNonce is the nonce presented to the user in the INFO line
|
|
GetNonce() []byte
|
|
// Kind indicates what type of connection this is matching defined constants like CLIENT, ROUTER, GATEWAY, LEAF etc
|
|
Kind() int
|
|
}
|
|
|
|
// NkeyUser is for multiple nkey based users
|
|
type NkeyUser struct {
|
|
Nkey string `json:"user"`
|
|
Permissions *Permissions `json:"permissions,omitempty"`
|
|
Account *Account `json:"account,omitempty"`
|
|
SigningKey string `json:"signing_key,omitempty"`
|
|
AllowedConnectionTypes map[string]struct{} `json:"connection_types,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"`
|
|
AllowedConnectionTypes map[string]struct{} `json:"connection_types,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"`
|
|
}
|
|
|
|
// ResponsePermission can be used to allow responses to any reply subject
|
|
// that is received on a valid subscription.
|
|
type ResponsePermission struct {
|
|
MaxMsgs int `json:"max"`
|
|
Expires time.Duration `json:"ttl"`
|
|
}
|
|
|
|
// Permissions are the allowed subjects on a per
|
|
// publish or subscribe basis.
|
|
type Permissions struct {
|
|
Publish *SubjectPermission `json:"publish"`
|
|
Subscribe *SubjectPermission `json:"subscribe"`
|
|
Response *ResponsePermission `json:"responses,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
if p.Response != nil {
|
|
clone.Response = &ResponsePermission{
|
|
MaxMsgs: p.Response.MaxMsgs,
|
|
Expires: p.Response.Expires,
|
|
}
|
|
}
|
|
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 {
|
|
// Skip warn if using TLS certs based auth
|
|
// unless a password has been left in the config.
|
|
if u.Password == "" && s.opts.TLSMap {
|
|
continue
|
|
}
|
|
|
|
if !isBcrypt(u.Password) {
|
|
warn = true
|
|
break
|
|
}
|
|
}
|
|
if warn {
|
|
// Warning about using plaintext passwords.
|
|
s.Warnf("Plaintext passwords detected, use nkeys or bcrypt")
|
|
}
|
|
}
|
|
|
|
// If Users or Nkeys options have definitions without an account defined,
|
|
// assign them to the default global account.
|
|
// Lock should be held.
|
|
func (s *Server) assignGlobalAccountToOrphanUsers(nkeys map[string]*NkeyUser, users map[string]*User) {
|
|
for _, u := range users {
|
|
if u.Account == nil {
|
|
u.Account = s.gacc
|
|
}
|
|
}
|
|
for _, u := range nkeys {
|
|
if u.Account == nil {
|
|
u.Account = s.gacc
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the given permissions has a ResponsePermission
|
|
// set, ensure that defaults are set (if values are 0)
|
|
// and that a Publish permission is set, and Allow
|
|
// is disabled if not explicitly set.
|
|
func validateResponsePermissions(p *Permissions) {
|
|
if p == nil || p.Response == nil {
|
|
return
|
|
}
|
|
if p.Publish == nil {
|
|
p.Publish = &SubjectPermission{}
|
|
}
|
|
if p.Publish.Allow == nil {
|
|
// We turn off the blanket allow statement.
|
|
p.Publish.Allow = []string{}
|
|
}
|
|
// If there is a response permission, ensure
|
|
// that if value is 0, we set the default value.
|
|
if p.Response.MaxMsgs == 0 {
|
|
p.Response.MaxMsgs = DEFAULT_ALLOW_RESPONSE_MAX_MSGS
|
|
}
|
|
if p.Response.Expires == 0 {
|
|
p.Response.Expires = DEFAULT_ALLOW_RESPONSE_EXPIRATION
|
|
}
|
|
}
|
|
|
|
// configureAuthorization will do any setup needed for authorization.
|
|
// Lock is assumed held.
|
|
func (s *Server) configureAuthorization() {
|
|
opts := s.getOpts()
|
|
if opts == nil {
|
|
return
|
|
}
|
|
|
|
// 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 s.trustedKeys != nil {
|
|
s.info.AuthRequired = true
|
|
} else if opts.Nkeys != nil || opts.Users != nil {
|
|
s.nkeys, s.users = s.buildNkeysAndUsersFromOptions(opts.Nkeys, opts.Users)
|
|
s.info.AuthRequired = true
|
|
} else if opts.Username != "" || opts.Authorization != "" {
|
|
s.info.AuthRequired = true
|
|
} else {
|
|
s.users = nil
|
|
s.nkeys = nil
|
|
s.info.AuthRequired = false
|
|
}
|
|
|
|
// Do similar for websocket config
|
|
s.wsConfigAuth(&opts.Websocket)
|
|
// And for mqtt config
|
|
s.mqttConfigAuth(&opts.MQTT)
|
|
}
|
|
|
|
// Takes the given slices of NkeyUser and User options and build
|
|
// corresponding maps used by the server. The users are cloned
|
|
// so that server does not reference options.
|
|
// The global account is assigned to users that don't have an
|
|
// existing account.
|
|
// Server lock is held on entry.
|
|
func (s *Server) buildNkeysAndUsersFromOptions(nko []*NkeyUser, uo []*User) (map[string]*NkeyUser, map[string]*User) {
|
|
var nkeys map[string]*NkeyUser
|
|
var users map[string]*User
|
|
|
|
if nko != nil {
|
|
nkeys = make(map[string]*NkeyUser, len(nko))
|
|
for _, u := range nko {
|
|
copy := u.clone()
|
|
if u.Account != nil {
|
|
if v, ok := s.accounts.Load(u.Account.Name); ok {
|
|
copy.Account = v.(*Account)
|
|
}
|
|
}
|
|
if copy.Permissions != nil {
|
|
validateResponsePermissions(copy.Permissions)
|
|
}
|
|
nkeys[u.Nkey] = copy
|
|
}
|
|
}
|
|
if uo != nil {
|
|
users = make(map[string]*User, len(uo))
|
|
for _, u := range uo {
|
|
copy := u.clone()
|
|
if u.Account != nil {
|
|
if v, ok := s.accounts.Load(u.Account.Name); ok {
|
|
copy.Account = v.(*Account)
|
|
}
|
|
}
|
|
if copy.Permissions != nil {
|
|
validateResponsePermissions(copy.Permissions)
|
|
}
|
|
users[u.Username] = copy
|
|
}
|
|
}
|
|
s.assignGlobalAccountToOrphanUsers(nkeys, users)
|
|
return nkeys, users
|
|
}
|
|
|
|
// 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)
|
|
case LEAF:
|
|
return s.isLeafNodeAuthorized(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 {
|
|
opts := s.getOpts()
|
|
|
|
// Check custom auth first, then jwts, then nkeys, then
|
|
// multiple users with TLS map if enabled, then token,
|
|
// then single user/pass.
|
|
if opts.CustomClientAuthentication != nil && !opts.CustomClientAuthentication.Check(c) {
|
|
return false
|
|
}
|
|
|
|
if opts.CustomClientAuthentication == nil && !s.processClientOrLeafAuthentication(c, opts) {
|
|
return false
|
|
}
|
|
|
|
if c.kind == CLIENT || c.kind == LEAF {
|
|
// Generate an event if we have a system account.
|
|
s.accountConnectEvent(c)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// returns false if the client needs to be disconnected
|
|
func (c *client) matchesPinnedCert(tlsPinnedCerts PinnedCertSet) bool {
|
|
if tlsPinnedCerts == nil {
|
|
return true
|
|
}
|
|
tlsState := c.GetTLSConnectionState()
|
|
if tlsState == nil || len(tlsState.PeerCertificates) == 0 || tlsState.PeerCertificates[0] == nil {
|
|
c.Debugf("Failed pinned cert test as client did not provide a certificate")
|
|
return false
|
|
}
|
|
sha := sha256.Sum256(tlsState.PeerCertificates[0].RawSubjectPublicKeyInfo)
|
|
keyId := hex.EncodeToString(sha[:])
|
|
if _, ok := tlsPinnedCerts[keyId]; !ok {
|
|
c.Debugf("Failed pinned cert test for key id: %s", keyId)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) bool {
|
|
var (
|
|
nkey *NkeyUser
|
|
juc *jwt.UserClaims
|
|
acc *Account
|
|
user *User
|
|
ok bool
|
|
err error
|
|
ao bool // auth override
|
|
)
|
|
s.mu.Lock()
|
|
authRequired := s.info.AuthRequired
|
|
if !authRequired {
|
|
// If no auth required for regular clients, then check if
|
|
// we have an override for MQTT or Websocket clients.
|
|
switch c.clientType() {
|
|
case MQTT:
|
|
authRequired = s.mqtt.authOverride
|
|
case WS:
|
|
authRequired = s.websocket.authOverride
|
|
}
|
|
}
|
|
if !authRequired {
|
|
// TODO(dlc) - If they send us credentials should we fail?
|
|
s.mu.Unlock()
|
|
return true
|
|
}
|
|
var (
|
|
username string
|
|
password string
|
|
token string
|
|
noAuthUser string
|
|
pinnedAcounts map[string]struct{}
|
|
)
|
|
tlsMap := opts.TLSMap
|
|
if c.kind == CLIENT {
|
|
switch c.clientType() {
|
|
case MQTT:
|
|
mo := &opts.MQTT
|
|
// Always override TLSMap.
|
|
tlsMap = mo.TLSMap
|
|
// The rest depends on if there was any auth override in
|
|
// the mqtt's config.
|
|
if s.mqtt.authOverride {
|
|
noAuthUser = mo.NoAuthUser
|
|
username = mo.Username
|
|
password = mo.Password
|
|
token = mo.Token
|
|
ao = true
|
|
}
|
|
case WS:
|
|
wo := &opts.Websocket
|
|
// Always override TLSMap.
|
|
tlsMap = wo.TLSMap
|
|
// The rest depends on if there was any auth override in
|
|
// the websocket's config.
|
|
if s.websocket.authOverride {
|
|
noAuthUser = wo.NoAuthUser
|
|
username = wo.Username
|
|
password = wo.Password
|
|
token = wo.Token
|
|
ao = true
|
|
}
|
|
}
|
|
} else {
|
|
tlsMap = opts.LeafNode.TLSMap
|
|
}
|
|
|
|
if !ao {
|
|
noAuthUser = opts.NoAuthUser
|
|
username = opts.Username
|
|
password = opts.Password
|
|
token = opts.Authorization
|
|
}
|
|
|
|
// Check if we have trustedKeys defined in the server. If so we require a user jwt.
|
|
if s.trustedKeys != nil {
|
|
if c.opts.JWT == _EMPTY_ {
|
|
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
|
|
}
|
|
pinnedAcounts = opts.resolverPinnedAccounts
|
|
}
|
|
|
|
// Check if we have nkeys or users for client.
|
|
hasNkeys := len(s.nkeys) > 0
|
|
hasUsers := len(s.users) > 0
|
|
if hasNkeys && c.opts.Nkey != _EMPTY_ {
|
|
nkey, ok = s.nkeys[c.opts.Nkey]
|
|
if !ok || !c.connectionTypeAllowed(nkey.AllowedConnectionTypes) {
|
|
s.mu.Unlock()
|
|
return false
|
|
}
|
|
} else if hasUsers {
|
|
// Check if we are tls verify and are mapping users from the client_certificate.
|
|
if tlsMap {
|
|
authorized := checkClientTLSCertSubject(c, func(u string, certDN *ldap.DN, _ bool) (string, bool) {
|
|
// First do literal lookup using the resulting string representation
|
|
// of RDNSequence as implemented by the pkix package from Go.
|
|
if u != _EMPTY_ {
|
|
usr, ok := s.users[u]
|
|
if !ok || !c.connectionTypeAllowed(usr.AllowedConnectionTypes) {
|
|
return _EMPTY_, false
|
|
}
|
|
user = usr
|
|
return usr.Username, true
|
|
}
|
|
|
|
if certDN == nil {
|
|
return _EMPTY_, false
|
|
}
|
|
|
|
// Look through the accounts for a DN that is equal to the one
|
|
// presented by the certificate.
|
|
dns := make(map[*User]*ldap.DN)
|
|
for _, usr := range s.users {
|
|
if !c.connectionTypeAllowed(usr.AllowedConnectionTypes) {
|
|
continue
|
|
}
|
|
// TODO: Use this utility to make a full validation pass
|
|
// on start in case tlsmap feature is being used.
|
|
inputDN, err := ldap.ParseDN(usr.Username)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if inputDN.Equal(certDN) {
|
|
user = usr
|
|
return usr.Username, true
|
|
}
|
|
|
|
// In case it did not match exactly, then collect the DNs
|
|
// and try to match later in case the DN was reordered.
|
|
dns[usr] = inputDN
|
|
}
|
|
|
|
// Check in case the DN was reordered.
|
|
for usr, inputDN := range dns {
|
|
if inputDN.RDNsMatch(certDN) {
|
|
user = usr
|
|
return usr.Username, true
|
|
}
|
|
}
|
|
return _EMPTY_, false
|
|
})
|
|
if !authorized {
|
|
s.mu.Unlock()
|
|
return false
|
|
}
|
|
if c.opts.Username != _EMPTY_ {
|
|
s.Warnf("User %q found in connect proto, but user required from cert", c.opts.Username)
|
|
}
|
|
// Already checked that the client didn't send a user in connect
|
|
// but we set it here to be able to identify it in the logs.
|
|
c.opts.Username = user.Username
|
|
} else {
|
|
if (c.kind == CLIENT || c.kind == LEAF) && noAuthUser != _EMPTY_ &&
|
|
c.opts.Username == _EMPTY_ && c.opts.Password == _EMPTY_ && c.opts.Token == _EMPTY_ {
|
|
if u, exists := s.users[noAuthUser]; exists {
|
|
c.mu.Lock()
|
|
c.opts.Username = u.Username
|
|
c.opts.Password = u.Password
|
|
c.mu.Unlock()
|
|
}
|
|
}
|
|
if c.opts.Username != _EMPTY_ {
|
|
user, ok = s.users[c.opts.Username]
|
|
if !ok || !c.connectionTypeAllowed(user.AllowedConnectionTypes) {
|
|
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 {
|
|
allowedConnTypes, err := convertAllowedConnectionTypes(juc.AllowedConnectionTypes)
|
|
if err != nil {
|
|
// We got an error, which means some connection types were unknown. As long as
|
|
// a valid one is returned, we proceed with auth. If not, we have to reject.
|
|
// In other words, suppose that JWT allows "WEBSOCKET" in the array. No error
|
|
// is returned and allowedConnTypes will contain "WEBSOCKET" only.
|
|
// Client will be rejected if not a websocket client, or proceed with rest of
|
|
// auth if it is.
|
|
// Now suppose JWT allows "WEBSOCKET, MQTT" and say MQTT is not known by this
|
|
// server. In this case, allowedConnTypes would contain "WEBSOCKET" and we
|
|
// would get `err` indicating that "MQTT" is an unknown connection type.
|
|
// If a websocket client connects, it should still be allowed, since after all
|
|
// the admin wanted to allow websocket and mqtt connection types.
|
|
// However, say that the JWT only allows "MQTT" (and again suppose this server
|
|
// does not know about MQTT connection type), then since the allowedConnTypes
|
|
// map would be empty (no valid types found), and since empty means allow-all,
|
|
// then we should reject because the intent was to allow connections for this
|
|
// user only as an MQTT client.
|
|
c.Debugf("%v", err)
|
|
if len(allowedConnTypes) == 0 {
|
|
return false
|
|
}
|
|
err = nil
|
|
}
|
|
if !c.connectionTypeAllowed(allowedConnTypes) {
|
|
c.Debugf("Connection type not allowed")
|
|
return false
|
|
}
|
|
issuer := juc.Issuer
|
|
if juc.IssuerAccount != _EMPTY_ {
|
|
issuer = juc.IssuerAccount
|
|
}
|
|
if pinnedAcounts != nil {
|
|
if _, ok := pinnedAcounts[issuer]; !ok {
|
|
c.Debugf("Account %s not listed as operator pinned account", issuer)
|
|
atomic.AddUint64(&s.pinnedAccFail, 1)
|
|
return false
|
|
}
|
|
}
|
|
if acc, err = s.LookupAccount(issuer); acc == nil {
|
|
c.Debugf("Account JWT lookup error: %v", err)
|
|
return false
|
|
}
|
|
if !s.isTrustedIssuer(acc.Issuer) {
|
|
c.Debugf("Account JWT not signed by trusted operator")
|
|
return false
|
|
}
|
|
if scope, ok := acc.hasIssuer(juc.Issuer); !ok {
|
|
c.Debugf("User JWT issuer is not known")
|
|
return false
|
|
} else if scope != nil {
|
|
if err := scope.ValidateScopedSigner(juc); err != nil {
|
|
c.Debugf("User JWT is not valid: %v", err)
|
|
return false
|
|
} else if uSc, ok := scope.(*jwt.UserScope); !ok {
|
|
c.Debugf("User JWT is not valid")
|
|
return false
|
|
} else {
|
|
juc.UserPermissionLimits = uSc.Template
|
|
}
|
|
}
|
|
if acc.IsExpired() {
|
|
c.Debugf("Account JWT has expired")
|
|
return false
|
|
}
|
|
// skip validation of nonce when presented with a bearer token
|
|
// FIXME: if BearerToken is only for WSS, need check for server with that port enabled
|
|
if !juc.BearerToken {
|
|
// Verify the signature against the nonce.
|
|
if c.opts.Sig == _EMPTY_ {
|
|
c.Debugf("Signature missing")
|
|
return false
|
|
}
|
|
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
|
|
if err != nil {
|
|
// Allow fallback to normal base64.
|
|
sig, err = base64.StdEncoding.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
|
|
}
|
|
}
|
|
if acc.checkUserRevoked(juc.Subject, juc.IssuedAt) {
|
|
c.Debugf("User authentication revoked")
|
|
return false
|
|
}
|
|
if !validateSrc(juc, c.host) {
|
|
c.Errorf("Bad src Ip %s", c.host)
|
|
return false
|
|
}
|
|
allowNow, validFor := validateTimes(juc)
|
|
if !allowNow {
|
|
c.Errorf("Outside connect times")
|
|
return false
|
|
}
|
|
|
|
nkey = buildInternalNkeyUser(juc, allowedConnTypes, acc)
|
|
if err := c.RegisterNkeyUser(nkey); err != nil {
|
|
return false
|
|
}
|
|
|
|
// Warn about JetStream restrictions
|
|
if c.perms != nil {
|
|
deniedPub := []string{}
|
|
deniedSub := []string{}
|
|
for _, sub := range denyAllJs {
|
|
if c.perms.pub.deny != nil {
|
|
if r := c.perms.pub.deny.Match(sub); len(r.psubs)+len(r.qsubs) > 0 {
|
|
deniedPub = append(deniedPub, sub)
|
|
}
|
|
}
|
|
if c.perms.sub.deny != nil {
|
|
if r := c.perms.sub.deny.Match(sub); len(r.psubs)+len(r.qsubs) > 0 {
|
|
deniedSub = append(deniedSub, sub)
|
|
}
|
|
}
|
|
}
|
|
if len(deniedPub) > 0 || len(deniedSub) > 0 {
|
|
c.Noticef("Connected %s has JetStream denied on pub: %v sub: %v", c.kindString(), deniedPub, deniedSub)
|
|
}
|
|
}
|
|
|
|
// Hold onto the user's public key.
|
|
c.mu.Lock()
|
|
c.pubKey = juc.Subject
|
|
c.tags = juc.Tags
|
|
c.nameTag = juc.Name
|
|
c.mu.Unlock()
|
|
|
|
// Check if we need to set an auth timer if the user jwt expires.
|
|
c.setExpiration(juc.Claims(), validFor)
|
|
|
|
acc.mu.RLock()
|
|
c.Debugf("Authenticated JWT: %s %q (claim-name: %q, claim-tags: %q) "+
|
|
"signed with %q by Account %q (claim-name: %q, claim-tags: %q) signed with %q has mappings %t accused %p",
|
|
c.kindString(), juc.Subject, juc.Name, juc.Tags, juc.Issuer, issuer, acc.nameTag, acc.tags, acc.Issuer, acc.hasMappings(), acc)
|
|
acc.mu.RUnlock()
|
|
return true
|
|
}
|
|
|
|
if nkey != nil {
|
|
if c.opts.Sig == _EMPTY_ {
|
|
c.Debugf("Signature missing")
|
|
return false
|
|
}
|
|
sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig)
|
|
if err != nil {
|
|
// Allow fallback to normal base64.
|
|
sig, err = base64.StdEncoding.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
|
|
}
|
|
if err := c.RegisterNkeyUser(nkey); 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
|
|
}
|
|
|
|
if c.kind == CLIENT {
|
|
if token != _EMPTY_ {
|
|
return comparePasswords(token, c.opts.Token)
|
|
} else if username != _EMPTY_ {
|
|
if username != c.opts.Username {
|
|
return false
|
|
}
|
|
return comparePasswords(password, c.opts.Password)
|
|
}
|
|
} else if c.kind == LEAF {
|
|
// There is no required username/password to connect and
|
|
// there was no u/p in the CONNECT or none that matches the
|
|
// know users. Register the leaf connection with global account
|
|
// or the one specified in config (if provided).
|
|
return s.registerLeafWithAccount(c, opts.LeafNode.Account)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func getTLSAuthDCs(rdns *pkix.RDNSequence) string {
|
|
dcOID := asn1.ObjectIdentifier{0, 9, 2342, 19200300, 100, 1, 25}
|
|
dcs := []string{}
|
|
for _, rdn := range *rdns {
|
|
if len(rdn) == 0 {
|
|
continue
|
|
}
|
|
for _, atv := range rdn {
|
|
value, ok := atv.Value.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
if atv.Type.Equal(dcOID) {
|
|
dcs = append(dcs, "DC="+value)
|
|
}
|
|
}
|
|
}
|
|
return strings.Join(dcs, ",")
|
|
}
|
|
|
|
type tlsMapAuthFn func(string, *ldap.DN, bool) (string, bool)
|
|
|
|
func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool {
|
|
tlsState := c.GetTLSConnectionState()
|
|
if tlsState == nil {
|
|
c.Debugf("User required in cert, no TLS connection state")
|
|
return false
|
|
}
|
|
if len(tlsState.PeerCertificates) == 0 {
|
|
c.Debugf("User required in cert, no peer certificates found")
|
|
return false
|
|
}
|
|
cert := tlsState.PeerCertificates[0]
|
|
if len(tlsState.PeerCertificates) > 1 {
|
|
c.Debugf("Multiple peer certificates found, selecting first")
|
|
}
|
|
|
|
hasSANs := len(cert.DNSNames) > 0
|
|
hasEmailAddresses := len(cert.EmailAddresses) > 0
|
|
hasSubject := len(cert.Subject.String()) > 0
|
|
hasURIs := len(cert.URIs) > 0
|
|
if !hasEmailAddresses && !hasSubject && !hasURIs {
|
|
c.Debugf("User required in cert, none found")
|
|
return false
|
|
}
|
|
|
|
switch {
|
|
case hasEmailAddresses:
|
|
for _, u := range cert.EmailAddresses {
|
|
if match, ok := fn(u, nil, false); ok {
|
|
c.Debugf("Using email found in cert for auth [%q]", match)
|
|
return true
|
|
}
|
|
}
|
|
fallthrough
|
|
case hasSANs:
|
|
for _, u := range cert.DNSNames {
|
|
if match, ok := fn(u, nil, true); ok {
|
|
c.Debugf("Using SAN found in cert for auth [%q]", match)
|
|
return true
|
|
}
|
|
}
|
|
fallthrough
|
|
case hasURIs:
|
|
for _, u := range cert.URIs {
|
|
if match, ok := fn(u.String(), nil, false); ok {
|
|
c.Debugf("Using URI found in cert for auth [%q]", match)
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use the string representation of the full RDN Sequence including
|
|
// the domain components in case there are any.
|
|
rdn := cert.Subject.ToRDNSequence().String()
|
|
|
|
// Match using the raw subject to avoid ignoring attributes.
|
|
// https://github.com/golang/go/issues/12342
|
|
dn, err := ldap.FromRawCertSubject(cert.RawSubject)
|
|
if err == nil {
|
|
if match, ok := fn("", dn, false); ok {
|
|
c.Debugf("Using DistinguishedNameMatch for auth [%q]", match)
|
|
return true
|
|
}
|
|
c.Debugf("DistinguishedNameMatch could not be used for auth [%q]", rdn)
|
|
}
|
|
|
|
var rdns pkix.RDNSequence
|
|
if _, err := asn1.Unmarshal(cert.RawSubject, &rdns); err == nil {
|
|
// If found domain components then include roughly following
|
|
// the order from https://tools.ietf.org/html/rfc2253
|
|
//
|
|
// NOTE: The original sequence from string representation by ToRDNSequence does not follow
|
|
// the correct ordering, so this addition ofdomainComponents would likely be deprecated in
|
|
// another release in favor of using the correct ordered as parsed by the go-ldap library.
|
|
//
|
|
dcs := getTLSAuthDCs(&rdns)
|
|
if len(dcs) > 0 {
|
|
u := strings.Join([]string{rdn, dcs}, ",")
|
|
if match, ok := fn(u, nil, false); ok {
|
|
c.Debugf("Using RDNSequence for auth [%q]", match)
|
|
return true
|
|
}
|
|
c.Debugf("RDNSequence could not be used for auth [%q]", u)
|
|
}
|
|
}
|
|
|
|
// If no match, then use the string representation of the RDNSequence
|
|
// from the subject without the domainComponents.
|
|
if match, ok := fn(rdn, nil, false); ok {
|
|
c.Debugf("Using certificate subject for auth [%q]", match)
|
|
return true
|
|
}
|
|
|
|
c.Debugf("User in cert [%q], not found", rdn)
|
|
return false
|
|
}
|
|
|
|
func dnsAltNameLabels(dnsAltName string) []string {
|
|
return strings.Split(strings.ToLower(dnsAltName), ".")
|
|
}
|
|
|
|
// Check DNS name according to https://tools.ietf.org/html/rfc6125#section-6.4.1
|
|
func dnsAltNameMatches(dnsAltNameLabels []string, urls []*url.URL) bool {
|
|
URLS:
|
|
for _, url := range urls {
|
|
if url == nil {
|
|
continue URLS
|
|
}
|
|
hostLabels := strings.Split(strings.ToLower(url.Hostname()), ".")
|
|
// Following https://tools.ietf.org/html/rfc6125#section-6.4.3, should not => will not, may => will not
|
|
// The wilcard * never matches multiple label and only matches the left most label.
|
|
if len(hostLabels) != len(dnsAltNameLabels) {
|
|
continue URLS
|
|
}
|
|
i := 0
|
|
// only match wildcard on left most label
|
|
if dnsAltNameLabels[0] == "*" {
|
|
i++
|
|
}
|
|
for ; i < len(dnsAltNameLabels); i++ {
|
|
if dnsAltNameLabels[i] != hostLabels[i] {
|
|
continue URLS
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
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()
|
|
|
|
// Check custom auth first, then TLS map if enabled
|
|
// then single user/pass.
|
|
if s.opts.CustomRouterAuthentication != nil {
|
|
return s.opts.CustomRouterAuthentication.Check(c)
|
|
}
|
|
|
|
if opts.Cluster.TLSMap || opts.Cluster.TLSCheckKnownURLs {
|
|
return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) {
|
|
if user == "" {
|
|
return "", false
|
|
}
|
|
if opts.Cluster.TLSCheckKnownURLs && isDNSAltName {
|
|
if dnsAltNameMatches(dnsAltNameLabels(user), opts.Routes) {
|
|
return "", true
|
|
}
|
|
}
|
|
if opts.Cluster.TLSMap && opts.Cluster.Username == user {
|
|
return "", true
|
|
}
|
|
return "", false
|
|
})
|
|
}
|
|
|
|
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()
|
|
|
|
// Check whether TLS map is enabled, otherwise use single user/pass.
|
|
if opts.Gateway.TLSMap || opts.Gateway.TLSCheckKnownURLs {
|
|
return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) {
|
|
if user == "" {
|
|
return "", false
|
|
}
|
|
if opts.Gateway.TLSCheckKnownURLs && isDNSAltName {
|
|
labels := dnsAltNameLabels(user)
|
|
for _, gw := range opts.Gateway.Gateways {
|
|
if gw != nil && dnsAltNameMatches(labels, gw.URLs) {
|
|
return "", true
|
|
}
|
|
}
|
|
}
|
|
if opts.Gateway.TLSMap && opts.Gateway.Username == user {
|
|
return "", true
|
|
}
|
|
return "", false
|
|
})
|
|
}
|
|
|
|
if opts.Gateway.Username == "" {
|
|
return true
|
|
}
|
|
|
|
if opts.Gateway.Username != c.opts.Username {
|
|
return false
|
|
}
|
|
return comparePasswords(opts.Gateway.Password, c.opts.Password)
|
|
}
|
|
|
|
func (s *Server) registerLeafWithAccount(c *client, account string) bool {
|
|
var err error
|
|
acc := s.globalAccount()
|
|
if account != _EMPTY_ {
|
|
acc, err = s.lookupAccount(account)
|
|
if err != nil {
|
|
s.Errorf("authentication of user %q failed, unable to lookup account %q: %v",
|
|
c.opts.Username, account, err)
|
|
return false
|
|
}
|
|
}
|
|
if err = c.registerWithAccount(acc); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isLeafNodeAuthorized will check for auth for an inbound leaf node connection.
|
|
func (s *Server) isLeafNodeAuthorized(c *client) bool {
|
|
opts := s.getOpts()
|
|
|
|
isAuthorized := func(username, password, account string) bool {
|
|
if username != c.opts.Username {
|
|
return false
|
|
}
|
|
if !comparePasswords(password, c.opts.Password) {
|
|
return false
|
|
}
|
|
return s.registerLeafWithAccount(c, account)
|
|
}
|
|
|
|
// If leafnodes config has an authorization{} stanza, this takes precedence.
|
|
// The user in CONNECT must match. We will bind to the account associated
|
|
// with that user (from the leafnode's authorization{} config).
|
|
if opts.LeafNode.Username != _EMPTY_ {
|
|
return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account)
|
|
} else if len(opts.LeafNode.Users) > 0 {
|
|
if opts.LeafNode.TLSMap {
|
|
var user *User
|
|
found := checkClientTLSCertSubject(c, func(u string, _ *ldap.DN, _ bool) (string, bool) {
|
|
// This is expected to be a very small array.
|
|
for _, usr := range opts.LeafNode.Users {
|
|
if u == usr.Username {
|
|
user = usr
|
|
return u, true
|
|
}
|
|
}
|
|
return _EMPTY_, false
|
|
})
|
|
if !found {
|
|
return false
|
|
}
|
|
if c.opts.Username != _EMPTY_ {
|
|
s.Warnf("User %q found in connect proto, but user required from cert", c.opts.Username)
|
|
}
|
|
c.opts.Username = user.Username
|
|
// EMPTY will result in $G
|
|
accName := _EMPTY_
|
|
if user.Account != nil {
|
|
accName = user.Account.GetName()
|
|
}
|
|
// This will authorize since are using an existing user,
|
|
// but it will also register with proper account.
|
|
return isAuthorized(user.Username, user.Password, accName)
|
|
}
|
|
|
|
// This is expected to be a very small array.
|
|
for _, u := range opts.LeafNode.Users {
|
|
if u.Username == c.opts.Username {
|
|
var accName string
|
|
if u.Account != nil {
|
|
accName = u.Account.Name
|
|
}
|
|
return isAuthorized(u.Username, u.Password, accName)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// We are here if we accept leafnode connections without any credentials.
|
|
|
|
// Still, if the CONNECT has some user info, we will bind to the
|
|
// user's account or to the specified default account (if provided)
|
|
// or to the global account.
|
|
return s.isClientAuthorized(c)
|
|
}
|
|
|
|
// Support for bcrypt stored passwords and tokens.
|
|
var validBcryptPrefix = regexp.MustCompile(`^\$2[abxy]\$\d{2}\$.*`)
|
|
|
|
// isBcrypt checks whether the given password or token is bcrypted.
|
|
func isBcrypt(password string) bool {
|
|
if strings.HasPrefix(password, "$") {
|
|
return validBcryptPrefix.MatchString(password)
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func validateAuth(o *Options) error {
|
|
if err := validatePinnedCerts(o.TLSPinnedCerts); err != nil {
|
|
return err
|
|
}
|
|
for _, u := range o.Users {
|
|
if err := validateAllowedConnectionTypes(u.AllowedConnectionTypes); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, u := range o.Nkeys {
|
|
if err := validateAllowedConnectionTypes(u.AllowedConnectionTypes); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return validateNoAuthUser(o, o.NoAuthUser)
|
|
}
|
|
|
|
func validateAllowedConnectionTypes(m map[string]struct{}) error {
|
|
for ct := range m {
|
|
ctuc := strings.ToUpper(ct)
|
|
switch ctuc {
|
|
case jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket,
|
|
jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeLeafnodeWS,
|
|
jwt.ConnectionTypeMqtt, jwt.ConnectionTypeMqttWS:
|
|
default:
|
|
return fmt.Errorf("unknown connection type %q", ct)
|
|
}
|
|
if ctuc != ct {
|
|
delete(m, ct)
|
|
m[ctuc] = struct{}{}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateNoAuthUser(o *Options, noAuthUser string) error {
|
|
if noAuthUser == "" {
|
|
return nil
|
|
}
|
|
if len(o.TrustedOperators) > 0 {
|
|
return fmt.Errorf("no_auth_user not compatible with Trusted Operator")
|
|
}
|
|
if o.Users == nil {
|
|
return fmt.Errorf(`no_auth_user: "%s" present, but users are not defined`, noAuthUser)
|
|
}
|
|
for _, u := range o.Users {
|
|
if u.Username == noAuthUser {
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf(
|
|
`no_auth_user: "%s" not present as user in authorization block or account configuration`,
|
|
noAuthUser)
|
|
}
|