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

Commit 5a75e78

Browse files
k-wallvcabbage
authored andcommitted
Implement SASL OAuth2 implementation (#193)
Fix #190: Implement SASL XOAuth2 mechanism
1 parent c5a6279 commit 5a75e78

9 files changed

+444
-2
lines changed

conn.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -644,8 +644,9 @@ func (c *conn) writeFrame(fr frame) error {
644644
}
645645

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

651652
// write to network

decode.go

+4
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ func parseFrameBody(r *buffer) (frameBody, error) {
119119
t := new(saslMechanisms)
120120
err := t.unmarshal(r)
121121
return t, err
122+
case typeCodeSASLChallenge:
123+
t := new(saslChallenge)
124+
err := t.unmarshal(r)
125+
return t, err
122126
case typeCodeSASLOutcome:
123127
t := new(saslOutcome)
124128
err := t.unmarshal(r)

fuzz.go

+4
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ func FuzzUnmarshal(data []byte) int {
109109
new(*saslCode),
110110
new(saslMechanisms),
111111
new(*saslMechanisms),
112+
new(saslChallenge),
113+
new(*saslChallenge),
114+
new(saslResponse),
115+
new(*saslResponse),
112116
new(saslOutcome),
113117
new(*saslOutcome),
114118
new(Message),

fuzz/marshal/corpus/saslChallenge.bin

28 Bytes
Binary file not shown.

fuzz/marshal/corpus/saslResponse.bin

27 Bytes
Binary file not shown.

marshal_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,12 @@ var (
554554
&saslMechanisms{
555555
Mechanisms: []symbol{"FOO", "BAR", "BAZ"},
556556
},
557+
&saslChallenge{
558+
Challenge: []byte("BAR\x00CHALLENGE\x00"),
559+
},
560+
&saslResponse{
561+
Response: []byte("BAR\x00RESPONSE\x00"),
562+
},
557563
&saslOutcome{
558564
Code: codeSASLSysPerm,
559565
AdditionalData: []byte("here's some info for you..."),

sasl.go

+125
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
package amqp
22

3+
import (
4+
"fmt"
5+
)
6+
37
// SASL Codes
48
const (
59
codeSASLOK saslCode = iota // Connection authentication succeeded.
@@ -13,6 +17,7 @@ const (
1317
const (
1418
saslMechanismPLAIN symbol = "PLAIN"
1519
saslMechanismANONYMOUS symbol = "ANONYMOUS"
20+
saslMechanismXOAUTH2 symbol = "XOAUTH2"
1621
)
1722

1823
type saslCode uint8
@@ -92,3 +97,123 @@ func ConnSASLAnonymous() ConnOption {
9297
return nil
9398
}
9499
}
100+
101+
// ConnSASLXOAUTH2 enables SASL XOAUTH2 authentication for the connection.
102+
//
103+
// The saslMaxFrameSizeOverride parameter allows the limit that governs the maximum frame size this client will allow
104+
// itself to generate to be raised for the sasl-init frame only. Set this when the size of the size of the SASL XOAUTH2
105+
// initial client response (which contains the username and bearer token) would otherwise breach the 512 byte min-max-frame-size
106+
// (http://docs.oasis-open.org/amqp/core/v1.0/os/amqp-core-transport-v1.0-os.html#definition-MIN-MAX-FRAME-SIZE). Pass -1
107+
// to keep the default.
108+
//
109+
// SASL XOAUTH2 transmits the bearer in plain text and should only be used
110+
// on TLS/SSL enabled connection.
111+
func ConnSASLXOAUTH2(username, bearer string, saslMaxFrameSizeOverride uint32) ConnOption {
112+
return func(c *conn) error {
113+
// make handlers map if no other mechanism has
114+
if c.saslHandlers == nil {
115+
c.saslHandlers = make(map[symbol]stateFunc)
116+
}
117+
118+
response, err := saslXOAUTH2InitialResponse(username, bearer)
119+
if err != nil {
120+
return err
121+
}
122+
123+
handler := saslXOAUTH2Handler{
124+
conn: c,
125+
maxFrameSizeOverride: saslMaxFrameSizeOverride,
126+
response: response,
127+
}
128+
// add the handler the the map
129+
c.saslHandlers[saslMechanismXOAUTH2] = handler.init
130+
return nil
131+
}
132+
}
133+
134+
type saslXOAUTH2Handler struct {
135+
conn *conn
136+
maxFrameSizeOverride uint32
137+
response []byte
138+
errorResponse []byte // https://developers.google.com/gmail/imap/xoauth2-protocol#error_response
139+
}
140+
141+
func (s saslXOAUTH2Handler) init() stateFunc {
142+
originalPeerMaxFrameSize := s.conn.peerMaxFrameSize
143+
if s.maxFrameSizeOverride > s.conn.peerMaxFrameSize {
144+
s.conn.peerMaxFrameSize = s.maxFrameSizeOverride
145+
}
146+
s.conn.err = s.conn.writeFrame(frame{
147+
type_: frameTypeSASL,
148+
body: &saslInit{
149+
Mechanism: saslMechanismXOAUTH2,
150+
InitialResponse: s.response,
151+
},
152+
})
153+
s.conn.peerMaxFrameSize = originalPeerMaxFrameSize
154+
if s.conn.err != nil {
155+
return nil
156+
}
157+
158+
return s.step
159+
}
160+
161+
func (s saslXOAUTH2Handler) step() stateFunc {
162+
// read challenge or outcome frame
163+
fr, err := s.conn.readFrame()
164+
if err != nil {
165+
s.conn.err = err
166+
return nil
167+
}
168+
169+
switch v := fr.body.(type) {
170+
case *saslOutcome:
171+
// check if auth succeeded
172+
if v.Code != codeSASLOK {
173+
s.conn.err = errorErrorf("SASL XOAUTH2 auth failed with code %#00x: %s : %s",
174+
v.Code, v.AdditionalData, s.errorResponse)
175+
return nil
176+
}
177+
178+
// return to c.negotiateProto
179+
s.conn.saslComplete = true
180+
return s.conn.negotiateProto
181+
case *saslChallenge:
182+
if s.errorResponse == nil {
183+
s.errorResponse = v.Challenge
184+
185+
// The SASL protocol requires clients to send an empty response to this challenge.
186+
s.conn.err = s.conn.writeFrame(frame{
187+
type_: frameTypeSASL,
188+
body: &saslResponse{
189+
Response: []byte{},
190+
},
191+
})
192+
return s.step
193+
} else {
194+
s.conn.err = errorErrorf("SASL XOAUTH2 unexpected additional error response received during "+
195+
"exchange. Initial error response: %s, additional response: %s", s.errorResponse, v.Challenge)
196+
return nil
197+
}
198+
default:
199+
s.conn.err = errorErrorf("unexpected frame type %T", fr.body)
200+
return nil
201+
}
202+
}
203+
204+
func saslXOAUTH2InitialResponse(username string, bearer string) ([]byte, error) {
205+
if len(bearer) == 0 {
206+
return []byte{}, fmt.Errorf("unacceptable bearer token")
207+
}
208+
for _, char := range bearer {
209+
if char < '\x20' || char > '\x7E' {
210+
return []byte{}, fmt.Errorf("unacceptable bearer token")
211+
}
212+
}
213+
for _, char := range username {
214+
if char == '\x01' {
215+
return []byte{}, fmt.Errorf("unacceptable username")
216+
}
217+
}
218+
return []byte("user=" + username + "\x01auth=Bearer " + bearer + "\x01\x01"), nil
219+
}

0 commit comments

Comments
 (0)