Skip to content

Commit

Permalink
Add in account scoped auth error event. If external auth, supply reas…
Browse files Browse the repository at this point in the history
…on from the callout service.

Signed-off-by: Derek Collison <derek@nats.io>
  • Loading branch information
derekcollison committed Jan 3, 2023
1 parent 2daf904 commit a63929c
Show file tree
Hide file tree
Showing 4 changed files with 234 additions and 70 deletions.
46 changes: 31 additions & 15 deletions server/auth.go
@@ -1,4 +1,4 @@
// Copyright 2012-2022 The NATS Authors
// Copyright 2012-2023 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
Expand Down Expand Up @@ -581,12 +581,20 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au

// Check if we have auth callouts enabled at the server level or in the bound account.
defer func() {
// Default reason
reason := AuthenticationViolation.String()
// No-op
if juc == nil && opts.AuthCallout == nil {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}
// We have a juc defined here, check account.
if juc != nil && !acc.hasExternalAuth() {
if !authorized {
s.sendAccountAuthErrorEvent(c, c.acc, reason)
}
return
}

Expand All @@ -608,7 +616,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
// If we are here we have an auth callout defined and we have failed auth so far
// so we will callout to our auth backend for processing.
if !skip {
authorized = s.processClientOrLeafCallout(c, opts)
authorized, reason = s.processClientOrLeafCallout(c, opts)
}
// Check if we are authorized and in the auth callout account, and if so add in deny publish permissions for the auth subject.
if authorized {
Expand All @@ -623,6 +631,14 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) (au
c.mergeDenyPermissions(pub, []string{AuthCalloutSubject})
}
c.mu.Unlock()
} else {
// If we are here we failed external authorization.
// Send an account scoped event. Server config mode acc will be nil,
// so lookup the auth callout assigned account, that is where this will be sent.
if acc == nil {
acc, _ = s.lookupAccount(opts.AuthCallout.Account)
}
s.sendAccountAuthErrorEvent(c, acc, reason)
}
}()

Expand Down Expand Up @@ -1106,7 +1122,7 @@ func checkClientTLSCertSubject(c *client, fn tlsMapAuthFn) bool {
// https://github.com/golang/go/issues/12342
dn, err := ldap.FromRawCertSubject(cert.RawSubject)
if err == nil {
if match, ok := fn("", dn, false); ok {
if match, ok := fn(_EMPTY_, dn, false); ok {
c.Debugf("Using DistinguishedNameMatch for auth [%q]", match)
return true
}
Expand Down Expand Up @@ -1189,22 +1205,22 @@ func (s *Server) isRouterAuthorized(c *client) bool {

if opts.Cluster.TLSMap || opts.Cluster.TLSCheckKnownURLs {
return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) {
if user == "" {
return "", false
if user == _EMPTY_ {
return _EMPTY_, false
}
if opts.Cluster.TLSCheckKnownURLs && isDNSAltName {
if dnsAltNameMatches(dnsAltNameLabels(user), opts.Routes) {
return "", true
return _EMPTY_, true
}
}
if opts.Cluster.TLSMap && opts.Cluster.Username == user {
return "", true
return _EMPTY_, true
}
return "", false
return _EMPTY_, false
})
}

if opts.Cluster.Username == "" {
if opts.Cluster.Username == _EMPTY_ {
return true
}

Expand All @@ -1225,25 +1241,25 @@ func (s *Server) isGatewayAuthorized(c *client) bool {
// 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 user == _EMPTY_ {
return _EMPTY_, false
}
if opts.Gateway.TLSCheckKnownURLs && isDNSAltName {
labels := dnsAltNameLabels(user)
for _, gw := range opts.Gateway.Gateways {
if gw != nil && dnsAltNameMatches(labels, gw.URLs) {
return "", true
return _EMPTY_, true
}
}
}
if opts.Gateway.TLSMap && opts.Gateway.Username == user {
return "", true
return _EMPTY_, true
}
return "", false
return _EMPTY_, false
})
}

if opts.Gateway.Username == "" {
if opts.Gateway.Username == _EMPTY_ {
return true
}

Expand Down
80 changes: 41 additions & 39 deletions server/auth_callout.go
@@ -1,4 +1,4 @@
// Copyright 2022 The NATS Authors
// Copyright 2022-2023 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
Expand All @@ -17,6 +17,7 @@ import (
"bytes"
"crypto/tls"
"encoding/pem"
"fmt"
"time"

"github.com/nats-io/jwt/v2"
Expand All @@ -30,7 +31,7 @@ const (
)

// Process a callout on this client's behalf.
func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool) {
func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorized bool, errStr string) {
isOperatorMode := len(opts.TrustedKeys) > 0

var acc *Account
Expand All @@ -39,8 +40,9 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
var err error
acc, err = s.LookupAccount(aname)
if err != nil {
s.Warnf("No valid account %q for auth callout request: %v", aname, err)
return false
errStr = fmt.Sprintf("No valid account %q for auth callout request: %v", aname, err)
s.Warnf(errStr)
return false, errStr
}
} else {
acc = c.acc
Expand All @@ -67,15 +69,14 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
pub, _ := ukp.PublicKey()

reply := s.newRespInbox()
respCh := make(chan bool, 1)
respCh := make(chan string, 1)

processReply := func(_ *subscription, rc *client, racc *Account, subject, reply string, rmsg []byte) {
_, msg := rc.msgParts(rmsg)
// This signals not authorized.
// Since this is an account subscription will always have "\r\n".
if len(msg) <= LEN_CR_LF {
s.Warnf("Auth callout violation: %q on account %q", "no reason supplied", racc.Name)
respCh <- false
respCh <- fmt.Sprintf("Auth callout violation: %q on account %q", "no reason supplied", racc.Name)
return
}
// Strip trailing CRLF.
Expand All @@ -86,25 +87,24 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
var err error
msg, err = xkp.Open(msg, pubAccXKey)
if err != nil {
s.Warnf("Error decrypting auth callout response on account %q: %v", racc.Name, err)
respCh <- false
respCh <- fmt.Sprintf("Error decrypting auth callout response on account %q: %v", racc.Name, err)
return
}
}

arc, err := jwt.DecodeAuthorizationResponseClaims(string(msg))
if err != nil {
s.Warnf("Error decoding auth callout response on account %q: %v", racc.Name, err)
respCh <- false
respCh <- fmt.Sprintf("Error decoding auth callout response on account %q: %v", racc.Name, err)
return
}

// FIXME(dlc) - push error through here.
if arc.Error != nil || arc.User == nil {
if arc.Error != nil {
s.Warnf("Auth callout violation: %q on account %q", arc.Error.Description, racc.Name)
respCh <- fmt.Sprintf("Auth callout violation: %q on account %q", arc.Error.Description, racc.Name)
} else {
respCh <- fmt.Sprintf("Auth callout violation: no user returned on account %q", racc.Name)
}
respCh <- false
return
}

Expand All @@ -119,42 +119,40 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
// By default issuer needs to match server config or the requesting account in operator mode.
if arc.Issuer != issuer {
if !isOperatorMode {
s.Warnf("Wrong issuer for auth callout response on account %q, expected %q got %q", racc.Name, issuer, arc.Issuer)
respCh <- false
respCh <- fmt.Sprintf("Wrong issuer for auth callout response on account %q, expected %q got %q", racc.Name, issuer, arc.Issuer)
return
} else if !acc.isAllowedAcount(arc.Issuer) {
s.Warnf("Account %q not permitted as valid account option for auth callout on %q for account %q", arc.Issuer, issuer, racc.Name)
respCh <- false
respCh <- fmt.Sprintf("Account %q not permitted as valid account option for auth callout for account %q",
arc.Issuer, racc.Name)
return
}
}

// Require the response to have pinned the audience to this server.
if arc.Audience != s.info.ID {
s.Warnf("Wrong server audience received for auth callout response on account %q, expected %q got %q", racc.Name, s.info.ID, arc.Audience)
respCh <- false
respCh <- fmt.Sprintf("Wrong server audience received for auth callout response on account %q, expected %q got %q",
racc.Name, s.info.ID, arc.Audience)
return
}

juc := arc.User
// Make sure that the user is what we requested.
if juc.Subject != pub {
s.Warnf("Expected authorized user of %q but got %q on account %q", pub, juc.Subject, racc.Name)
respCh <- false
respCh <- fmt.Sprintf("Expected authorized user of %q but got %q on account %q", pub, juc.Subject, racc.Name)
return
}

allowNow, validFor := validateTimes(juc)
if !allowNow {
c.Errorf("Outside connect times")
respCh <- false
respCh <- fmt.Sprintf("Authorized user on account %q outside of valid connect times", racc.Name)
return
}
allowedConnTypes, err := convertAllowedConnectionTypes(juc.AllowedConnectionTypes)
if err != nil {
c.Debugf("%v", err)
if len(allowedConnTypes) == 0 {
respCh <- false
respCh <- fmt.Sprintf("Authorized user on account %q using invalid connection type", racc.Name)
return
}
}
Expand All @@ -164,23 +162,20 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
if aname := juc.Audience; aname != _EMPTY_ {
targetAcc, err = s.LookupAccount(aname)
if err != nil {
s.Warnf("No valid account %q for auth callout response on account %q: %v", aname, racc.Name, err)
respCh <- false
respCh <- fmt.Sprintf("No valid account %q for auth callout response on account %q: %v", aname, racc.Name, err)
return
}
// In operator mode make sure this account matches the issuer.
if isOperatorMode && aname != arc.Issuer {
s.Warnf("Account %q does not match issuer %q", aname, juc.Issuer)
respCh <- false
respCh <- fmt.Sprintf("Account %q does not match issuer %q", aname, juc.Issuer)
return
}
}

// Build internal user and bind to the targeted account.
nkuser := buildInternalNkeyUser(juc, allowedConnTypes, targetAcc)
if err := c.RegisterNkeyUser(nkuser); err != nil {
s.Warnf("Could not register auth callout user: %v", err)
respCh <- false
respCh <- fmt.Sprintf("Could not register auth callout user: %v", err)
return
}

Expand All @@ -198,13 +193,14 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
// Check if we need to set an auth timer if the user jwt expires.
c.setExpiration(juc.Claims(), validFor)

respCh <- true
respCh <- _EMPTY_
}

sub, err := acc.subscribeInternal(reply, processReply)
if err != nil {
s.Warnf("Error setting up reply subscription for auth request: %v", err)
return false
errStr = fmt.Sprintf("Error setting up reply subscription for auth request: %v", err)
s.Warnf(errStr)
return false, errStr
}
defer acc.unsubscribeInternal(sub)

Expand Down Expand Up @@ -283,8 +279,9 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize

b, err := claim.Encode(s.kp)
if err != nil {
s.Warnf("Error encoding auth request claim on account %q: %v", acc.Name, err)
return false
errStr = fmt.Sprintf("Error encoding auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
req := []byte(b)
var hdr map[string]string
Expand All @@ -293,8 +290,9 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
if xkp != nil {
req, err = xkp.Seal([]byte(req), pubAccXKey)
if err != nil {
s.Warnf("Error encrypting auth request claim on account %q: %v", acc.Name, err)
return false
errStr = fmt.Sprintf("Error encrypting auth request claim on account %q: %v", acc.Name, err)
s.Warnf(errStr)
return false, errStr
}
hdr = map[string]string{AuthRequestXKeyHeader: xkey}
}
Expand All @@ -303,12 +301,16 @@ func (s *Server) processClientOrLeafCallout(c *client, opts *Options) (authorize
s.sendInternalAccountMsgWithReply(acc, AuthCalloutSubject, reply, hdr, req, false)

select {
case authorized = <-respCh:
case errStr = <-respCh:
if authorized = errStr == _EMPTY_; !authorized {
s.Warnf(errStr)

This comment has been minimized.

Copy link
@ripienaar

ripienaar Jan 3, 2023

Contributor

nice these will help a lot

}
case <-time.After(authTimeout):
s.Debugf("Authorization callout response not received in time on account %q", acc.Name)
errStr = fmt.Sprintf("Authorization callout response not received in time on account %q", acc.Name)
s.Debugf(errStr)
}

return authorized
return authorized, errStr
}

// Fill in client information for the request.
Expand Down

0 comments on commit a63929c

Please sign in to comment.