Skip to content
This repository has been archived by the owner on Dec 14, 2020. It is now read-only.

Commit

Permalink
Implement SASL OAuth2 implementation (#193)
Browse files Browse the repository at this point in the history
Fix #190: Implement SASL XOAuth2 mechanism
  • Loading branch information
k-wall authored and vcabbage committed Dec 5, 2019
1 parent c5a6279 commit 5a75e78
Show file tree
Hide file tree
Showing 9 changed files with 444 additions and 2 deletions.
5 changes: 3 additions & 2 deletions conn.go
Original file line number Diff line number Diff line change
Expand Up @@ -644,8 +644,9 @@ func (c *conn) writeFrame(fr frame) error {
}

// validate the frame isn't exceeding peer's max frame size
if uint64(c.txBuf.len()) > uint64(c.peerMaxFrameSize) {
return errorErrorf("frame larger than peer's max frame size")
requiredFrameSize := c.txBuf.len()
if uint64(requiredFrameSize) > uint64(c.peerMaxFrameSize) {
return errorErrorf("%T frame size %d larger than peer's max frame size", fr, requiredFrameSize, c.peerMaxFrameSize)
}

// write to network
Expand Down
4 changes: 4 additions & 0 deletions decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ func parseFrameBody(r *buffer) (frameBody, error) {
t := new(saslMechanisms)
err := t.unmarshal(r)
return t, err
case typeCodeSASLChallenge:
t := new(saslChallenge)
err := t.unmarshal(r)
return t, err
case typeCodeSASLOutcome:
t := new(saslOutcome)
err := t.unmarshal(r)
Expand Down
4 changes: 4 additions & 0 deletions fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ func FuzzUnmarshal(data []byte) int {
new(*saslCode),
new(saslMechanisms),
new(*saslMechanisms),
new(saslChallenge),
new(*saslChallenge),
new(saslResponse),
new(*saslResponse),
new(saslOutcome),
new(*saslOutcome),
new(Message),
Expand Down
Binary file added fuzz/marshal/corpus/saslChallenge.bin
Binary file not shown.
Binary file added fuzz/marshal/corpus/saslResponse.bin
Binary file not shown.
6 changes: 6 additions & 0 deletions marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,12 @@ var (
&saslMechanisms{
Mechanisms: []symbol{"FOO", "BAR", "BAZ"},
},
&saslChallenge{
Challenge: []byte("BAR\x00CHALLENGE\x00"),
},
&saslResponse{
Response: []byte("BAR\x00RESPONSE\x00"),
},
&saslOutcome{
Code: codeSASLSysPerm,
AdditionalData: []byte("here's some info for you..."),
Expand Down
125 changes: 125 additions & 0 deletions sasl.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package amqp

import (
"fmt"
)

// SASL Codes
const (
codeSASLOK saslCode = iota // Connection authentication succeeded.
Expand All @@ -13,6 +17,7 @@ const (
const (
saslMechanismPLAIN symbol = "PLAIN"
saslMechanismANONYMOUS symbol = "ANONYMOUS"
saslMechanismXOAUTH2 symbol = "XOAUTH2"
)

type saslCode uint8
Expand Down Expand Up @@ -92,3 +97,123 @@ func ConnSASLAnonymous() ConnOption {
return nil
}
}

// ConnSASLXOAUTH2 enables SASL XOAUTH2 authentication for the connection.
//
// The saslMaxFrameSizeOverride parameter allows the limit that governs the maximum frame size this client will allow
// itself to generate to be raised for the sasl-init frame only. Set this when the size of the size of the SASL XOAUTH2
// initial client response (which contains the username and bearer token) would otherwise breach the 512 byte min-max-frame-size
// (http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#definition-MIN-MAX-FRAME-SIZE). Pass -1
// to keep the default.
//
// SASL XOAUTH2 transmits the bearer in plain text and should only be used
// on TLS/SSL enabled connection.
func ConnSASLXOAUTH2(username, bearer string, saslMaxFrameSizeOverride uint32) ConnOption {
return func(c *conn) error {
// make handlers map if no other mechanism has
if c.saslHandlers == nil {
c.saslHandlers = make(map[symbol]stateFunc)
}

response, err := saslXOAUTH2InitialResponse(username, bearer)
if err != nil {
return err
}

handler := saslXOAUTH2Handler{
conn: c,
maxFrameSizeOverride: saslMaxFrameSizeOverride,
response: response,
}
// add the handler the the map
c.saslHandlers[saslMechanismXOAUTH2] = handler.init
return nil
}
}

type saslXOAUTH2Handler struct {
conn *conn
maxFrameSizeOverride uint32
response []byte
errorResponse []byte // https://developers.google.com/gmail/imap/xoauth2-protocol#error_response
}

func (s saslXOAUTH2Handler) init() stateFunc {
originalPeerMaxFrameSize := s.conn.peerMaxFrameSize
if s.maxFrameSizeOverride > s.conn.peerMaxFrameSize {
s.conn.peerMaxFrameSize = s.maxFrameSizeOverride
}
s.conn.err = s.conn.writeFrame(frame{
type_: frameTypeSASL,
body: &saslInit{
Mechanism: saslMechanismXOAUTH2,
InitialResponse: s.response,
},
})
s.conn.peerMaxFrameSize = originalPeerMaxFrameSize
if s.conn.err != nil {
return nil
}

return s.step
}

func (s saslXOAUTH2Handler) step() stateFunc {
// read challenge or outcome frame
fr, err := s.conn.readFrame()
if err != nil {
s.conn.err = err
return nil
}

switch v := fr.body.(type) {
case *saslOutcome:
// check if auth succeeded
if v.Code != codeSASLOK {
s.conn.err = errorErrorf("SASL XOAUTH2 auth failed with code %#00x: %s : %s",
v.Code, v.AdditionalData, s.errorResponse)
return nil
}

// return to c.negotiateProto
s.conn.saslComplete = true
return s.conn.negotiateProto
case *saslChallenge:
if s.errorResponse == nil {
s.errorResponse = v.Challenge

// The SASL protocol requires clients to send an empty response to this challenge.
s.conn.err = s.conn.writeFrame(frame{
type_: frameTypeSASL,
body: &saslResponse{
Response: []byte{},
},
})
return s.step
} else {
s.conn.err = errorErrorf("SASL XOAUTH2 unexpected additional error response received during "+
"exchange. Initial error response: %s, additional response: %s", s.errorResponse, v.Challenge)
return nil
}
default:
s.conn.err = errorErrorf("unexpected frame type %T", fr.body)
return nil
}
}

func saslXOAUTH2InitialResponse(username string, bearer string) ([]byte, error) {
if len(bearer) == 0 {
return []byte{}, fmt.Errorf("unacceptable bearer token")
}
for _, char := range bearer {
if char < '\x20' || char > '\x7E' {
return []byte{}, fmt.Errorf("unacceptable bearer token")
}
}
for _, char := range username {
if char == '\x01' {
return []byte{}, fmt.Errorf("unacceptable username")
}
}
return []byte("user=" + username + "\x01auth=Bearer " + bearer + "\x01\x01"), nil
}
Loading

0 comments on commit 5a75e78

Please sign in to comment.