callout

package module
v0.2.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 2, 2026 License: Apache-2.0 Imports: 12 Imported by: 1

README

Callout.go

Coverage Status License Apache 2 Go Report Card

This library implements a small framework for writing AuthCallout services for NATS.

An AuthCallout is a service that generates an authorization for a user based on an external criteria. There are five aspects to a callout:

  1. Receiving an authorization request
  2. Decoding and validating an authorization request
  3. Generating a user JWT based on the authorization request information
  4. Packaging an authorization response
  5. Sending the authorization response to the server

With the exception of step #3, all the other operations are completely boilerplate. While not complicated, it requires a careful understanding of the callout process and the features it provides to make the callout process secure.

The callout library simply requires a custom function implementation for step #3 (generating an user JWT), and handles all the other minutia automatically.

The library is implemented using the NATS services framework which enables you to easily horizontally scale the service to meet your cluster's demand.

High-level Overview

A callout simply issues a user JWT from a request from the server. So at it's most basic it is really just a request that receives an encoded jwt.AuthorizationRequestClaims which contains a jwt.AuthorizationRequest.

The server may have transmitted the request encrypted if the callout configuration specifies an encryption public key.

After possibly decrypting the request and decoding the jwt.AuthorizationRequestClaims, some checks are performed on the request. if its valid, and use the data provided in the jwt.AuthorizationRequest. The request provides all the connection options specified by the client as well as TLS information, and as additional information from the server. Of importance is the UserNKey, which specifies the ID that must be assigned to the user if the authorization succeeds. Typically, clients encode additional information into the information in the token field.

Limits and permissions are then translated into a jwt.UserClaims which describes the limits and permissions for the user within NATS.

Next, the callout service generates a jwt.AuthorizationResponseClaims which embeds a jwt.AuthorizationResponse which either includes the generated JWT token (a string) or an error message. If an error message is set, this message will be printed by the NATS server, but NOT transmitted to the user. Reasoning for rejecting a user shouldn't be forwarded. Note that typically if the user is rejected, the best practice is to drop the request after logging a message. This will timeout the authorization request and reject the user. The timeout introduces a delay that to slow down users that are rejected.

Finally, if the callout is using encryption, it must encrypt the encoded jwt.AuthorizationResponseClaim using the server's public key, and sending the response back to the server. The server in turn, validates the response and if all looks good, uses the generated user JWT as the permissions to assign to the user connecting it to NATS.

Simplest Callout

This example uses the following server configuration:

authorization:{
    users:[
        { user: auth, password: pwd }
    ]
    auth_callout:{
        # users mapped here, bypass the callout, any other users
        # will result in a request to the callout.
        auth_users:[auth]
        # in this type of configuration, the issuer for jwt.UserClaims
        # as well as the jwt.AuthorizationResponseClaim must be signed
        # with the private Account key matching the public key listed
        # as the issuer
        issuer: AAB35RZ7HJSICG7D4IGPYO3CTQPWWGULJXZYD45QAWUKTXJYDI6EO7MV
    }
}

To create a callout, you would connect to the NATS server using the auth user and create a subscription to $SYS.REQ.USER.AUTH, and process the request as above. You can examine the source in this project to see the nuances.

The Callout Implementation

If you are using the callout library, the process is greatly simplified:

// parse the private key
akp, _ := nkeys.FromSeed([]byte("SAAHZHKC43PG6B6EP3LZ7HB3HB3JD25GSJRV5LFZE2A6XFT57SDFRSEI4E"))

// a function that creates the users
authorizer := func(req *jwt.AuthorizationRequest) (string, error) {
	// peek at the req for information - for brevity
	// in the example, we simply allow them in
	
	// use the server specified user nkey
    uc := jwt.NewUserClaims(req.UserNkey)
	// put the user in the global account
    uc.Audience = "$G"
	// add whatever permissions you need
    uc.Sub.Allow.Add("_INBOX.>")
	// perhaps add an expiration to the JWT
    uc.Expires = time.Now().Unix() + 90
    return uc.Encode(akp)
}

// create a connection using the callout user
nc, _ := nats.Connect("nats://127.0.0.1", nats.UserInfo("auth", "pwd"))

// configure the authorization service with the connection, the function that 
// generates users, and the key to use to issue the jwt.AuthorizationResponseClaims
svc, err := NewAuthorizationService(serviceConn, Authorizer(authorizer), ResponseSignerKey(akp))
// done!
Adding Encryption

AuthorizationRequests can be encrypted. Encrypting ensures that requests are readable only to the owner of the specified encryption key, and that responses are only readable to the server that sent the request.

Here's the same server configuration, but this time enabling encryption:

authorization:{
    users:[
        { user: auth, password: pwd }
    ]
    auth_callout:{
        auth_users:[auth]
        issuer: AAB35RZ7HJSICG7D4IGPYO3CTQPWWGULJXZYD45QAWUKTXJYDI6EO7MV
        # Specifying the public curve key, the server will send authorization
        # requests encrypted so that the public key specified can read them.
        # Likewise the server will expect the response to be encrypted on the
        # public key it specifies in the request.
        xkey: XBCW4J63ZDLH54GKXJLBJQOWXEWPIYXY23HBMWL5LX6U24FW3C6U2UUL
    }
}

On the callout side, the only additional requirement is to specify the option EncryptionKey:

xkey, _ := nkeys.CreateCurveKeys()
svc, err := NewAuthorizationService(serviceConn,
    Authorizer(authorizer), ResponseSignerKey(akp), EncryptionKey(xkey))

The library will ensure that both the server and service are using encryption, and that the keys assets from the request are encrypted by the sending server, and will encrypt the AuthorizationResponse.

Delegated Authentication

Delegated Authentication increases the complixity a bit more. When using

A more complicated example using delegated authentication can be found here.

WebSocket

When using websockets, the websocket configuration on the server can specify

  • jwt_cookie (only if using delegated auth), user_cookie, pass_cookie, token_cookie, these options specify the name of a cookie that is mapped to the connect option of the same name. Note that because of CORS, the cookies will have to be Secure and SameSite (HttpOnly is good too) (at least for browsers). This enables websockets on a browser to the connection options injected on HTTP server.
FAQ
Delegating callout across leaf node connections

It is possible in principle for a leaf node to let an agent in the hub reply to an authentication request. Note that this is potentially unsafe, and therefore the $SYS.REQ.USER.AUTH subject is blocked by default on leaf node connections.

Specifically, if the account on the hub has auth callout enabled, it will block $SYS.REQ.USER.AUTH from leaf nodes.

This block cannot be disabled, but it can be circumvented by:

  • Connecting the auth callout accounts on the leaves to a dedicated non-auth-callout-enabled account on the hub.
  • Using import/export on the leaf node to export (and optionally rename) $SYS.REQ.USER.AUTH before routing to the hub.

More Examples TBD (look at the source Luke)

Documentation

Index

Constants

View Source
const (
	NatsServerXKeyHeader   = "Nats-Server-Xkey"
	ExpectedAudience       = "nats-authorization-request"
	SysRequestUserAuthSubj = "$SYS.REQ.USER.AUTH"
)

Variables

View Source
var (
	// ErrAbortRequest is an Error that signals for operation to abort.
	// Aborted operations don't respond to the callout request. Reason for
	// aborting is to allow the NATS server to delay the connection error
	// response and thus delay any possible denial-of-service attack.
	ErrAbortRequest = errors.New("abort request")
	// ErrAuthorizerRequired indicates that an authorizer must be provided for
	// the operation to proceed.
	ErrAuthorizerRequired = errors.New("authorizer is required")
	// ErrBadCalloutOption indicates an invalid or incompatible configuration
	// option was provided to NewAuthorizationService.
	ErrBadCalloutOption = errors.New("bad options")
	// ErrService represents a general service error that may occur during
	// operation execution.
	ErrService = errors.New("service error")
	// ErrRejectedAuth indicates that an authorization request was explicitly
	// rejected, typically due to invalid credentials or other authorization
	// failures.
	ErrRejectedAuth = errors.New("rejected authorization request")
	// ErrWorkerChannelFull indicates the async worker channel is at capacity
	// and the request was dropped.
	ErrWorkerChannelFull = errors.New("async worker channel full")
)

Functions

This section is empty.

Types

type AuthorizationService

type AuthorizationService struct {
	Service micro.Service
	// contains filtered or unexported fields
}

AuthorizationService is a service that handles user authorization requests and processes JWT-based authentication. It uses options to specify configuration such as authorizers, response signers, encryption keys, and logging. The service can operate with synchronous or asynchronous request handling using worker channels.

func NewAuthorizationService

func NewAuthorizationService(
	nc *nats.Conn, opts ...Option,
) (*AuthorizationService, error)

NewAuthorizationService initializes and returns a new instance of AuthorizationService with the provided options. It sets up the service, including logging, error handling, and request-processing functionality.

Errors may occur during initialization due to missing or incompatible options.

Returns an AuthorizationService instance and an error, if any occurred.

func (*AuthorizationService) AsyncWorkerHandler

func (c *AuthorizationService) AsyncWorkerHandler(msg micro.Request)

AsyncWorkerHandler enqueues an incoming micro.Request to the workers channel for asynchronous processing.

func (*AuthorizationService) ServiceHandler

func (c *AuthorizationService) ServiceHandler(msg micro.Request)

ServiceHandler processes incoming authorization micro.Request messages, decodes and validates them, and generates appropriate responses or errors based on the authorization outcome.

func (*AuthorizationService) Stop

func (c *AuthorizationService) Stop() error

Stop gracefully shuts down the AuthorizationService by stopping its underlying service and closing worker channels. Note that the connection provided to the service when created is not closed as it is not created by the service.

type AuthorizerFn

type AuthorizerFn func(req *jwt.AuthorizationRequest) (string, error)

AuthorizerFn is a callback to AuthorizationService returns a user JWT

type ErrCallbackFn

type ErrCallbackFn func(err error)

ErrCallbackFn this is an optional callback that gets invoked whenever the AuthorizerFn returns an error, this is useful for tests

type InvalidUserCallbackFn

type InvalidUserCallbackFn func(jwt string, err error)

InvalidUserCallbackFn is a function type invoked when a user JWT validation fails, providing the JWT and the error details.

type Option

type Option func(*Options) error

Option is a function type used to configure the AuthorizationService options

func AsyncWorkers

func AsyncWorkers(n int) Option

AsyncWorkers sets the number of asynchronous workers that will be used the AuthorizationService.

func Authorizer

func Authorizer(fn AuthorizerFn) Option

Authorizer sets a custom authorization function (AuthorizerFn) for signing and handling user JWTs in the service configuration.

func EncryptionKey

func EncryptionKey(kp nkeys.KeyPair) Option

EncryptionKey sets the encryption key for the service, requiring it to be a curve seed. Returns an error for invalid keys.

func ErrCallback

func ErrCallback(fn ErrCallbackFn) Option

ErrCallback sets a callback function to handle errors returned by the AuthorizerFn, useful for logging or testing.

func InvalidUser

func InvalidUser(cb InvalidUserCallbackFn) Option

InvalidUser configures a callback function invoked when a user JWT validation fails, passing the JWT and error details.

func Logger

func Logger(l natsserver.Logger) Option

Logger sets the custom logger for the AuthorizationService.

func Name

func Name(n string) Option

Name sets the name of the service. This value must not contain spaces or dots of it may be rejected by the micro.Service backing the AuthorizationService.

func ResponseSigner

func ResponseSigner(fn ResponseSignerFn) Option

ResponseSigner sets a custom ResponseSignerFn to handle the signing of AuthorizationResponseClaims in the service options.

func ResponseSignerIssuer

func ResponseSignerIssuer(pub string) Option

ResponseSignerIssuer configures the issuer for the response signer using an account public key or seed. Returns an error if the provided key is invalid or not associated with an account.

func ResponseSignerKey

func ResponseSignerKey(kp nkeys.KeyPair) Option

ResponseSignerKey sets the response signer key to be used for signing authorization responses in the authorization service. The key pair must be an account private key, otherwise an error is returned.

func ServiceEndpoints

func ServiceEndpoints(n int) Option

ServiceEndpoints configures the number of service endpoints for the AuthorizationService.

type Options

type Options struct {
	// Name for the AuthorizationService cannot have spaces, etc, as this is
	// the name that the actual micro.Service will use.
	Name string
	// Authorizer function that processes authorization request and issues user
	// JWT
	Authorizer AuthorizerFn
	// ResponseSigner is a function that performs the signing of the
	// jwt.AuthorizationResponseClaim
	ResponseSigner ResponseSignerFn
	// ResponseSigner is the key that will be used to sign the
	// jwt.AuthorizationResponseClaim
	ResponseSignerKey nkeys.KeyPair
	// ResponseSigner is the key that ID of the account issuing the
	// jwt.AuthorizationResponseClaim if not set, ResponseSigner is the account
	ResponseSignerIssuer string
	// EncryptionKey is an optional configuration that must be provided if the
	// callout is configured to use encryption.
	EncryptionKey nkeys.KeyPair
	// Logger for the service process
	Logger natsserver.Logger
	// InvalidUser when set user JWTs are validated if error notified via the
	// callback
	InvalidUser InvalidUserCallbackFn
	// ErrCallback is an optional callback invoked whenever AuthorizerFn
	// returns an error, useful for handling test errors.
	ErrCallback ErrCallbackFn
	// ServiceEndpoints sets the number of endpoints available for the service
	// to handle requests.
	ServiceEndpoints int
	// AsyncWorkers specifies the number of workers used for asynchronous task
	// processing.
	AsyncWorkers int
}

Options defines a configuration struct for handling authorization and signing of user JWTs within a service.

type RequestContextError

type RequestContextError struct {
	UserID string
	Reason error
}

RequestContextError represents an error in the context of handling a user request, including the user ID and the cause.

func (RequestContextError) Error

func (r RequestContextError) Error() string

func (RequestContextError) Unwrap

func (r RequestContextError) Unwrap() error

func (RequestContextError) User

func (r RequestContextError) User() string

type ResponseSignerFn

type ResponseSignerFn func(*jwt.AuthorizationResponseClaims) (string, error)

ResponseSignerFn allows externalizing the signing to a different workflow where the callout doesn't directly sign the jwt.AuthorizationResponseClaims but instead forwards the request to some other mechanism

Directories

Path Synopsis
examples
delegated command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL