Skip to content

Commit e88d48a

Browse files
dominicbarnesachille-rousselcmaher
authored
SASL Mechanism: AWS MSK IAM (making requested edits) (#798)
Co-authored-by: Achille <[email protected]> Co-authored-by: Christian Maher <[email protected]>
1 parent 2e02f37 commit e88d48a

File tree

5 files changed

+308
-3
lines changed

5 files changed

+308
-3
lines changed

.circleci/config.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,20 @@ jobs:
3434
- checkout
3535
- restore_cache:
3636
key: kafka-go-mod-{{ checksum "go.sum" }}-1
37-
- run: go mod download
37+
- run:
38+
name: Download dependencies
39+
command: go mod download
3840
- save_cache:
3941
key: kafka-go-mod-{{ checksum "go.sum" }}-1
4042
paths:
4143
- /go/pkg/mod
42-
- run: go test -race -cover ./...
44+
- run:
45+
name: Test kafka-go
46+
command: go test -race -cover ./...
47+
- run:
48+
name: Test kafka-go/sasl/aws_msk_iam
49+
working_directory: ./sasl/aws_msk_iam
50+
command: go test -race -cover ./...
4351

4452
# Starting at version 0.11, the kafka features and configuration remained
4553
# mostly stable, so we can use this CI job configuration as template for other
@@ -219,7 +227,7 @@ jobs:
219227
- 9093:9093
220228
environment: *environment
221229
steps: *steps
222-
230+
223231
workflows:
224232
version: 2
225233
run:

sasl/aws_msk_iam/go.mod

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
module github.com/segmentio/kafka-go/sasl/aws_msk_iam
2+
3+
go 1.15
4+
5+
require (
6+
github.com/aws/aws-sdk-go v1.41.3
7+
github.com/segmentio/kafka-go v0.4.24
8+
)

sasl/aws_msk_iam/go.sum

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
github.com/aws/aws-sdk-go v1.41.3 h1:deglLZ1jjHdhkd6Rbad1MZM4gL+1pfnTfjuFk6CGJFM=
2+
github.com/aws/aws-sdk-go v1.41.3/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
3+
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
4+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5+
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw=
6+
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
7+
github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
8+
github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
9+
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
10+
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
11+
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
12+
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
13+
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
14+
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
15+
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
16+
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
17+
github.com/klauspost/compress v1.9.8 h1:VMAMUUOh+gaxKTMk+zqbjsSjsIcUcL/LF4o63i82QyA=
18+
github.com/klauspost/compress v1.9.8/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
19+
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
20+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
21+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
22+
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
23+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
24+
github.com/pierrec/lz4 v2.6.0+incompatible h1:Ix9yFKn1nSPBLFl/yZknTp8TU5G4Ps0JDmguYK6iH1A=
25+
github.com/pierrec/lz4 v2.6.0+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
26+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
27+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
28+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
29+
github.com/segmentio/kafka-go v0.4.24 h1:R3tYSYxyLK3SknDIU15LtpDdq59gRg2/J0GKhDFXrBQ=
30+
github.com/segmentio/kafka-go v0.4.24/go.mod h1:XzMcoMjSzDGHcIwpWUI7GB43iKZ2fTVmryPSGLf/MPg=
31+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
32+
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
33+
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
34+
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c h1:u40Z8hqBAAQyv+vATcGgV0YCnDjqSL7/q/JyPhhJSPk=
35+
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
36+
github.com/xdg/stringprep v1.0.0 h1:d9X0esnoa3dFsV0FG35rAT0RIhYFlPq7MiP+DW89La0=
37+
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
38+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
39+
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284 h1:rlLehGeYg6jfoyz/eDqDU1iRXLKfR42nnNh57ytKEWo=
40+
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
41+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
42+
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
43+
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
44+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
45+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
46+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
47+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
48+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
49+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
50+
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
51+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
52+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
53+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
54+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
55+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
56+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
57+
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
58+
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
59+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
60+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

sasl/aws_msk_iam/msk_iam.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package aws_msk_iam
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"net/http"
9+
"net/url"
10+
"runtime"
11+
"strings"
12+
"time"
13+
14+
sigv4 "github.com/aws/aws-sdk-go/aws/signer/v4"
15+
"github.com/segmentio/kafka-go/sasl"
16+
)
17+
18+
const (
19+
// These constants come from https://github.com/aws/aws-msk-iam-auth#details and
20+
// https://github.com/aws/aws-msk-iam-auth/blob/main/src/main/java/software/amazon/msk/auth/iam/internals/AWS4SignedPayloadGenerator.java.
21+
signVersion = "2020_10_22"
22+
signService = "kafka-cluster"
23+
signAction = "kafka-cluster:Connect"
24+
signVersionKey = "version"
25+
signHostKey = "host"
26+
signUserAgentKey = "user-agent"
27+
signActionKey = "action"
28+
queryActionKey = "Action"
29+
)
30+
31+
var signUserAgent = fmt.Sprintf("kafka-go/sasl/aws_msk_iam/%s", runtime.Version())
32+
33+
// Mechanism implements sasl.Mechanism for the AWS_MSK_IAM mechanism, based on the official java implementation:
34+
// https://github.com/aws/aws-msk-iam-auth
35+
type Mechanism struct {
36+
// The sigv4.Signer to use when signing the request. Required.
37+
Signer *sigv4.Signer
38+
// The region where the msk cluster is hosted, e.g. "us-east-1". Required.
39+
Region string
40+
// The time the request is planned for. Optional, defaults to time.Now() at time of authentication.
41+
SignTime time.Time
42+
// The duration for which the presigned request is active. Optional, defaults to 5 minutes.
43+
Expiry time.Duration
44+
}
45+
46+
func (m *Mechanism) Name() string {
47+
return "AWS_MSK_IAM"
48+
}
49+
50+
// Start produces the authentication values required for AWS_MSK_IAM. It produces the following json as a byte array,
51+
// making use of the aws-sdk to produce the signed output.
52+
// {
53+
// "version" : "2020_10_22",
54+
// "host" : "<broker host>",
55+
// "user-agent": "<user agent string from the client>",
56+
// "action": "kafka-cluster:Connect",
57+
// "x-amz-algorithm" : "<algorithm>",
58+
// "x-amz-credential" : "<clientAWSAccessKeyID>/<date in yyyyMMdd format>/<region>/kafka-cluster/aws4_request",
59+
// "x-amz-date" : "<timestamp in yyyyMMdd'T'HHmmss'Z' format>",
60+
// "x-amz-security-token" : "<clientAWSSessionToken if any>",
61+
// "x-amz-signedheaders" : "host",
62+
// "x-amz-expires" : "<expiration in seconds>",
63+
// "x-amz-signature" : "<AWS SigV4 signature computed by the client>"
64+
// }
65+
func (m *Mechanism) Start(ctx context.Context) (sess sasl.StateMachine, ir []byte, err error) {
66+
saslMeta := sasl.MetadataFromContext(ctx)
67+
if saslMeta == nil {
68+
return nil, nil, errors.New("missing sasl metadata")
69+
}
70+
71+
query := url.Values{
72+
queryActionKey: {signAction},
73+
}
74+
75+
signUrl := url.URL{
76+
Scheme: "kafka",
77+
Host: saslMeta.Host,
78+
Path: "/",
79+
RawQuery: query.Encode(),
80+
}
81+
82+
req, err := http.NewRequest("GET", signUrl.String(), nil)
83+
if err != nil {
84+
return nil, nil, err
85+
}
86+
87+
signTime := m.SignTime
88+
if signTime.IsZero() {
89+
signTime = time.Now()
90+
}
91+
92+
expiry := m.Expiry
93+
if expiry == 0 {
94+
expiry = 5 * time.Minute
95+
}
96+
97+
header, err := m.Signer.Presign(req, nil, signService, m.Region, expiry, signTime)
98+
if err != nil {
99+
return nil, nil, err
100+
}
101+
signedMap := map[string]string{
102+
signVersionKey: signVersion,
103+
signHostKey: signUrl.Host,
104+
signUserAgentKey: signUserAgent,
105+
signActionKey: signAction,
106+
}
107+
// The protocol requires lowercase keys.
108+
for key, vals := range header {
109+
signedMap[strings.ToLower(key)] = vals[0]
110+
}
111+
for key, vals := range req.URL.Query() {
112+
signedMap[strings.ToLower(key)] = vals[0]
113+
}
114+
115+
signedJson, err := json.Marshal(signedMap)
116+
return m, signedJson, err
117+
}
118+
119+
func (m *Mechanism) Next(ctx context.Context, challenge []byte) (bool, []byte, error) {
120+
// After the initial step, the authentication is complete
121+
// kafka will return error if it rejected the credentials, so we'll only
122+
// arrive here on success.
123+
return true, nil, nil
124+
}

sasl/aws_msk_iam/msk_iam_test.go

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package aws_msk_iam
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"testing"
8+
"time"
9+
10+
"github.com/segmentio/kafka-go/sasl"
11+
12+
"github.com/aws/aws-sdk-go/aws/credentials"
13+
sigv4 "github.com/aws/aws-sdk-go/aws/signer/v4"
14+
)
15+
16+
const (
17+
accessKeyId = "ACCESS_KEY"
18+
secretAccessKey = "SECRET_KEY"
19+
)
20+
21+
// using a fixed time allows the signature to be verifiable in a test
22+
var signTime = time.Date(2021, 10, 14, 13, 5, 0, 0, time.UTC)
23+
24+
func TestAwsMskIamMechanism(t *testing.T) {
25+
tests := []struct {
26+
description string
27+
ctx func() context.Context
28+
shouldFail bool
29+
}{
30+
{
31+
description: "with metadata",
32+
ctx: func() context.Context {
33+
return sasl.WithMetadata(context.Background(), &sasl.Metadata{
34+
Host: "localhost",
35+
Port: 9092,
36+
})
37+
},
38+
},
39+
{
40+
description: "without metadata",
41+
ctx: func() context.Context {
42+
return context.Background()
43+
},
44+
shouldFail: true,
45+
},
46+
}
47+
48+
for _, tt := range tests {
49+
t.Run(tt.description, func(t *testing.T) {
50+
ctx := tt.ctx()
51+
52+
creds := credentials.NewStaticCredentials(accessKeyId, secretAccessKey, "")
53+
mskMechanism := &Mechanism{
54+
Signer: sigv4.NewSigner(creds),
55+
Region: "us-east-1",
56+
SignTime: signTime,
57+
}
58+
59+
sess, auth, err := mskMechanism.Start(ctx)
60+
if tt.shouldFail { // if error is expected
61+
if err == nil { // but we don't find one
62+
t.Fatal("error expected")
63+
} else { // but we do find one
64+
return // return early since the remaining assertions are irrelevant
65+
}
66+
} else { // if error is not expected (typical)
67+
if err != nil { // but we do find one
68+
t.Fatal(err)
69+
}
70+
}
71+
72+
if sess != mskMechanism {
73+
t.Error(
74+
"Unexpected session",
75+
"expected", mskMechanism,
76+
"got", sess,
77+
)
78+
}
79+
80+
expectedMap := map[string]string{
81+
"version": "2020_10_22",
82+
"action": "kafka-cluster:Connect",
83+
"host": "localhost",
84+
"user-agent": signUserAgent,
85+
"x-amz-algorithm": "AWS4-HMAC-SHA256",
86+
"x-amz-credential": "ACCESS_KEY/20211014/us-east-1/kafka-cluster/aws4_request",
87+
"x-amz-date": "20211014T130500Z",
88+
"x-amz-expires": "300",
89+
"x-amz-signedheaders": "host",
90+
"x-amz-signature": "6b8d25f9b45b9c7db9da855a49112d80379224153a27fd279c305a5b7940d1a7",
91+
}
92+
expectedAuth, err := json.Marshal(expectedMap)
93+
if err != nil {
94+
t.Fatal(err)
95+
}
96+
97+
if !bytes.Equal(expectedAuth, auth) {
98+
t.Error("Unexpected authentication",
99+
"expected", expectedAuth,
100+
"got", auth,
101+
)
102+
}
103+
})
104+
}
105+
}

0 commit comments

Comments
 (0)