sasl: mellium.im/sasl Index | Examples | Files

package sasl

import "mellium.im/sasl"

Package sasl implements the Simple Authentication and Security Layer (SASL) as defined by RFC 4422.

Most users of this package will only need to create a Negotiator using NewClient or NewServer and call its Step method repeatedly. Authors implementing SASL mechanisms other than the builtin ones will want to create a Mechanism struct which will likely use the other methods on the Negotiator.

Be advised: This API is still unstable and is subject to change.

Code:

const (
    username = "miranda"
    password = "pencil"
)

creds := sasl.Credentials(func() ([]byte, []byte, []byte) {
    return []byte(username), []byte(password), []byte{}
})

server := sasl.NewServer(sasl.Plain, func(n *sasl.Negotiator) bool {
    user, pass, ident := n.Credentials()
    // In a real auth system you might want to consider a constant time
    // comparison and this would probably involve hashing and a database lookup.
    if len(ident) == 0 && string(user) == username && string(pass) == password {
        fmt.Println("auth success!")
        return true
    }
    fmt.Println("auth failed!")
    return false
}, creds)

client := sasl.NewClient(sasl.Plain, sasl.Credentials(func() ([]byte, []byte, []byte) {
    // In a real auth system this would probably be user input.
    return []byte(username), []byte("password!"), []byte{}
}))

_, resp, err := client.Step(nil)
if err != nil {
    fmt.Println(err)
    return
}

// Normally the response would come from the network, not from a client on the
// same machine.
_, resp, err = server.Step(resp)
if err != sasl.ErrAuthn {
    fmt.Println(err)
    return
}

Output:

auth failed!

Code:

const (
    username = "miranda"
    password = "pencil"
)

creds := sasl.Credentials(func() ([]byte, []byte, []byte) {
    // In a real auth system this would probably be user input.
    return []byte(username), []byte(password), []byte{}
})

server := sasl.NewServer(sasl.Plain, func(n *sasl.Negotiator) bool {
    user, pass, ident := n.Credentials()
    // In a real auth system you might want to consider a constant time
    // comparison and this would probably involve hashing and a database lookup.
    if len(ident) == 0 && string(user) == username && string(pass) == password {
        fmt.Println("auth success!")
        return true
    }
    fmt.Println("auth failed!")
    return false
}, creds)

client := sasl.NewClient(sasl.Plain, creds)

_, resp, err := client.Step(nil)
if err != nil {
    fmt.Println(err)
    return
}

// Normally the response would come from the network, not from a client on the
// same machine.
_, resp, err = server.Step(resp)
if err != nil {
    fmt.Println(err)
    return
}

Output:

auth success!

Code:

package main

import (
    "bytes"
    "fmt"

    "mellium.im/sasl"
)

// A custom SASL Mechanism that implements XOAUTH2:
// https://developers.google.com/gmail/xoauth2_protocol
var xoauth2 = sasl.Mechanism{
    Name: "XOAUTH2",
    Start: func(m *sasl.Negotiator) (bool, []byte, interface{}, error) {
        // Start is called only by clients and returns the client first message.

        username, password, _ := m.Credentials()

        payload := []byte(`user=`)
        payload = append(payload, username...)
        payload = append(payload, '\x01')
        payload = append(payload, []byte(`auth=Bearer `)...)
        payload = append(payload, password...)
        payload = append(payload, '\x01', '\x01')

        return false, payload, nil, nil
    },
    Next: func(m *sasl.Negotiator, challenge []byte, _ interface{}) (bool, []byte, interface{}, error) {
        // Next is called by both clients and servers and must be able to generate
        // and handle every challenge except for the client first message which is
        // generated (but not handled by) by Start.

        state := m.State()

        // If we're a client or a server that's past the AuthTextSent step, we
        // should never actually hit this step for the XOAUTH2 mechanism so return
        // an error.
        if state&sasl.Receiving != sasl.Receiving || state&sasl.StepMask != sasl.AuthTextSent {
            return false, nil, nil, sasl.ErrTooManySteps
        }

        parts := bytes.Split(challenge, []byte{1})
        if len(parts) != 3 {
            return false, nil, nil, sasl.ErrInvalidChallenge
        }
        user := bytes.TrimPrefix([]byte("user="), parts[0])
        if len(user) == len(parts[0]) {
            return false, nil, nil, sasl.ErrInvalidChallenge
        }
        pass := bytes.TrimPrefix([]byte("Auth=Bearer "), parts[1])
        if len(pass) == len(parts[1]) {
            return false, nil, nil, sasl.ErrInvalidChallenge
        }
        if len(parts[2]) > 0 {
            return false, nil, nil, sasl.ErrInvalidChallenge
        }

        if m.Permissions(sasl.Credentials(func() ([]byte, []byte, []byte) {
            return user, pass, nil
        })) {
            return false, nil, nil, nil
        }
        return false, nil, nil, sasl.ErrAuthn
    },
}

func main() {
    c := sasl.NewClient(
        xoauth2,
        sasl.Credentials(func() ([]byte, []byte, []byte) {
            return []byte("someuser@example.com"), []byte("vF9dft4qmTc2Nvb3RlckBhdHRhdmlzdGEuY29tCg=="), []byte{}
        }),
    )

    // This is the first step and we haven't received any challenge from the
    // server yet.
    more, resp, _ := c.Step(nil)
    fmt.Printf("%v %s", more, bytes.Replace(resp, []byte{1}, []byte{' '}, -1))

}

Index

Examples

Package Files

doc.go mechanism.go negotiator.go nonce.go options.go plain.go scram.go xor_amd64.go

Variables

var (
    ErrInvalidState     = errors.New("Invalid state")
    ErrInvalidChallenge = errors.New("Invalid or missing challenge")
    ErrAuthn            = errors.New("Authentication error")
    ErrTooManySteps     = errors.New("Step called too many times")
)

Define common errors used by SASL mechanisms and negotiators.

type Mechanism Uses

type Mechanism struct {
    Name  string
    Start func(n *Negotiator) (more bool, resp []byte, cache interface{}, err error)
    Next  func(n *Negotiator, challenge []byte, data interface{}) (more bool, resp []byte, cache interface{}, err error)
}

Mechanism represents a SASL mechanism that can be used by a Client or Server to perform the actual negotiation. Base64 encoding the final challenges and responses should not be performed by the mechanism.

Mechanisms must be stateless and may be shared between goroutines. When a mechanism needs to store state between the different steps it can return anything that it needs to store and the value will be cached by the negotiator and passed in as the data parameter when the next challenge is received.

var (
    // Plain is a Mechanism that implements the PLAIN authentication mechanism
    // as defined by RFC 4616.
    Plain Mechanism = plain

    // ScramSha256Plus is a Mechanism that implements the SCRAM-SHA-256-PLUS
    // authentication mechanism defined in RFC 7677. The only supported channel
    // binding type is tls-unique as defined in RFC 5929.
    ScramSha256Plus Mechanism = scram("SCRAM-SHA-256-PLUS", sha256.New)

    // ScramSha256 is a Mechanism that implements the SCRAM-SHA-256
    // authentication mechanism defined in RFC 7677.
    ScramSha256 Mechanism = scram("SCRAM-SHA-256", sha256.New)

    // ScramSha1Plus is a Mechanism that implements the SCRAM-SHA-1-PLUS
    // authentication mechanism defined in RFC 5802. The only supported channel
    // binding type is tls-unique as defined in RFC 5929.
    ScramSha1Plus Mechanism = scram("SCRAM-SHA-1-PLUS", sha1.New)

    // ScramSha1 is a Mechanism that implements the SCRAM-SHA-1 authentication
    // mechanism defined in RFC 5802.
    ScramSha1 Mechanism = scram("SCRAM-SHA-1", sha1.New)
)

type Negotiator Uses

type Negotiator struct {
    // contains filtered or unexported fields
}

A Negotiator represents a SASL client or server state machine that can attempt to negotiate auth. Negotiators should not be used from multiple goroutines, and must be reset between negotiation attempts.

func NewClient Uses

func NewClient(m Mechanism, opts ...Option) *Negotiator

NewClient creates a new SASL Negotiator that supports creating authentication requests using the given mechanism.

func NewServer Uses

func NewServer(m Mechanism, permissions func(*Negotiator) bool, opts ...Option) *Negotiator

NewServer creates a new SASL Negotiator that supports receiving authentication requests using the given mechanism. A nil permissions function is the same as a function that always returns false.

func (*Negotiator) Credentials Uses

func (c *Negotiator) Credentials() (username, password, identity []byte)

Credentials returns a username, and password for authentication and optional identity for authorization.

func (*Negotiator) Nonce Uses

func (c *Negotiator) Nonce() []byte

Nonce returns a unique nonce that is reset for each negotiation attempt. It is used by SASL Mechanisms and should generally not be called directly.

func (*Negotiator) Permissions Uses

func (c *Negotiator) Permissions(opts ...Option) bool

Permissions is the callback used by the server to authenticate the user.

func (*Negotiator) RemoteMechanisms Uses

func (c *Negotiator) RemoteMechanisms() []string

RemoteMechanisms is a list of mechanisms as advertised by the other side of a SASL negotiation.

func (*Negotiator) Reset Uses

func (c *Negotiator) Reset()

Reset resets the state machine to its initial state so that it can be reused in another SASL exchange.

func (*Negotiator) State Uses

func (c *Negotiator) State() State

State returns the internal state of the SASL state machine.

func (*Negotiator) Step Uses

func (c *Negotiator) Step(challenge []byte) (more bool, resp []byte, err error)

Step attempts to transition the state machine to its next state. If Step is called after a previous invocation generates an error (and the state machine has not been reset to its initial state), Step panics.

func (*Negotiator) TLSState Uses

func (c *Negotiator) TLSState() *tls.ConnectionState

TLSState is the state of any TLS connections being used to negotiate SASL (it can be used for channel binding).

type Option Uses

type Option func(*Negotiator)

An Option represents an input to a SASL state machine.

func Credentials Uses

func Credentials(f func() (Username, Password, Identity []byte)) Option

Credentials provides the negotiator with a username and password to authenticate with and (optionally) an authorization identity. Identity will normally be left empty to act as the username. The Credentials function is called lazily and may be called multiple times by the mechanism. It is not memoized by the negotiator.

func RemoteMechanisms Uses

func RemoteMechanisms(m ...string) Option

RemoteMechanisms sets a list of mechanisms supported by the remote client or server with which the state machine will be negotiating. It is used to determine if the server supports channel binding.

func TLSState Uses

func TLSState(cs tls.ConnectionState) Option

TLSState lets the state machine negotiate channel binding with a TLS session if supported by the underlying mechanism.

type State Uses

type State uint8

State represents the current state of a Negotiator. The first two bits represent the actual state of the state machine and the last 3 bits are a bitmask that define the machines behavior. The remaining bits should not be used.

const (
    Initial State = iota
    AuthTextSent
    ResponseSent
    ValidServerResponse

    // Bitmask used for extracting the step from the state byte.
    StepMask = 0x3
)

The current step of the Server or Client (represented by the first two bits of the state byte).

const (
    // RemoteCB bit is on if the remote client or server supports channel binding.
    RemoteCB State = 1 << (iota + 3)

    // Errored bit is on if the machine has errored.
    Errored

    // Receiving bit is on if the machine is a server.
    Receiving
)

Bugs

We need a way to cache the SCRAM client and server key calculations.

Package sasl imports 13 packages (graph) and is imported by 12 packages. Updated 2019-07-12. Refresh now. Tools for package owners.