Skip to content

Commit 1ec19f0

Browse files
authored
Added support for SIG4/SIGV4A querystring authentication. (#595)
1 parent 62e8fe9 commit 1ec19f0

File tree

7 files changed

+419
-18
lines changed

7 files changed

+419
-18
lines changed

aws-http-auth/internal/v4/signer.go

Lines changed: 56 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type Signer struct {
3333
Algorithm string
3434
CredentialScope string
3535
Finalizer Finalizer
36+
37+
SignatureType v4.SignatureType
3638
}
3739

3840
// Finalizer performs the final step in v4 signing, deriving a signature for
@@ -53,15 +55,26 @@ func (s *Signer) Do() error {
5355

5456
s.setRequiredHeaders()
5557

56-
canonicalRequest, signedHeaders := s.buildCanonicalRequest()
58+
// Build canonical headers first to get signedHeaders
59+
canonHeaders, signedHeaders := s.buildCanonicalHeaders()
60+
61+
if s.SignatureType == v4.SignatureTypeQueryString {
62+
s.addSignatureParametersToQuery(signedHeaders)
63+
}
64+
65+
canonicalRequest := s.buildCanonicalRequestWithHeaders(canonHeaders, signedHeaders)
5766
stringToSign := s.buildStringToSign(canonicalRequest)
5867
signature, err := s.Finalizer.SignString(stringToSign)
5968
if err != nil {
6069
return err
6170
}
6271

63-
s.Request.Header.Set("Authorization",
64-
s.buildAuthorizationHeader(signature, signedHeaders))
72+
if s.SignatureType == v4.SignatureTypeQueryString {
73+
s.addSignatureToQuery(signature, signedHeaders)
74+
} else {
75+
s.Request.Header.Set("Authorization",
76+
s.buildAuthorizationHeader(signature, signedHeaders))
77+
}
6578

6679
return nil
6780
}
@@ -108,20 +121,32 @@ func (s *Signer) setRequiredHeaders() {
108121
headers := s.Request.Header
109122

110123
s.Request.Header.Set("Host", s.Request.Host)
111-
s.Request.Header.Set("X-Amz-Date", s.Time.Format(TimeFormat))
112124

113-
if len(s.Credentials.SessionToken) > 0 {
114-
s.Request.Header.Set("X-Amz-Security-Token", s.Credentials.SessionToken)
125+
// X-Amz-Date and X-Amz-Security-Token are only set as headers when using a header signature type
126+
if s.SignatureType == v4.SignatureTypeHeader {
127+
s.Request.Header.Set("X-Amz-Date", s.Time.Format(TimeFormat))
128+
if len(s.Credentials.SessionToken) > 0 {
129+
s.Request.Header.Set("X-Amz-Security-Token", s.Credentials.SessionToken)
130+
}
131+
} else {
132+
// For query string auth, ensure these headers are not present
133+
s.Request.Header.Del("X-Amz-Date")
134+
s.Request.Header.Del("X-Amz-Security-Token")
115135
}
136+
116137
if len(s.PayloadHash) > 0 && s.Options.AddPayloadHashHeader {
117138
headers.Set("X-Amz-Content-Sha256", payloadHashString(s.PayloadHash))
118139
}
119140
}
120141

121142
func (s *Signer) buildCanonicalRequest() (string, string) {
143+
canonHeaders, signedHeaders := s.buildCanonicalHeaders()
144+
canonicalRequest := s.buildCanonicalRequestWithHeaders(canonHeaders, signedHeaders)
145+
return canonicalRequest, signedHeaders
146+
}
147+
148+
func (s *Signer) buildCanonicalRequestWithHeaders(canonHeaders, signedHeaders string) string {
122149
canonPath := s.Request.URL.EscapedPath()
123-
// https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html:
124-
// if input has no path, "/" is used
125150
if len(canonPath) == 0 {
126151
canonPath = "/"
127152
}
@@ -135,8 +160,6 @@ func (s *Signer) buildCanonicalRequest() (string, string) {
135160
}
136161
canonQuery := strings.Replace(query.Encode(), "+", "%20", -1)
137162

138-
canonHeaders, signedHeaders := s.buildCanonicalHeaders()
139-
140163
req := strings.Join([]string{
141164
s.Request.Method,
142165
canonPath,
@@ -146,7 +169,7 @@ func (s *Signer) buildCanonicalRequest() (string, string) {
146169
payloadHashString(s.PayloadHash),
147170
}, "\n")
148171

149-
return req, signedHeaders
172+
return req
150173
}
151174

152175
func (s *Signer) buildCanonicalHeaders() (canon, signed string) {
@@ -203,6 +226,28 @@ func (s *Signer) buildAuthorizationHeader(signature, headers string) string {
203226
signature)
204227
}
205228

229+
func (s *Signer) addSignatureParametersToQuery(signedHeaders string) {
230+
query := s.Request.URL.Query()
231+
query.Set("X-Amz-Algorithm", s.Algorithm)
232+
query.Set("X-Amz-Credential", s.Credentials.AccessKeyID+"/"+s.CredentialScope)
233+
query.Set("X-Amz-Date", s.Time.Format(TimeFormat))
234+
query.Set("X-Amz-SignedHeaders", signedHeaders)
235+
236+
if len(s.Credentials.SessionToken) > 0 {
237+
query.Set("X-Amz-Security-Token", s.Credentials.SessionToken)
238+
}
239+
240+
s.Request.URL.RawQuery = query.Encode()
241+
}
242+
243+
func (s *Signer) addSignatureToQuery(signature, signedHeaders string) {
244+
query := s.Request.URL.Query()
245+
query.Set("X-Amz-SignedHeaders", signedHeaders)
246+
query.Set("X-Amz-Signature", signature)
247+
248+
s.Request.URL.RawQuery = query.Encode()
249+
}
250+
206251
func payloadHashString(p []byte) string {
207252
if string(p) == "UNSIGNED-PAYLOAD" {
208253
return string(p) // sentinel, do not hex-encode

aws-http-auth/internal/v4/signer_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,117 @@ func (identityFinalizer) SignString(v string) (string, error) {
3434
return v, nil
3535
}
3636

37+
func TestQueryStringAuth_IncludesSignedHeadersInCanonicalRequest(t *testing.T) {
38+
req, err := http.NewRequest(http.MethodGet, "https://service.region.amazonaws.com/path", nil)
39+
if err != nil {
40+
t.Fatal(err)
41+
}
42+
req.URL.RawQuery = "existing=param"
43+
44+
s := &Signer{
45+
Request: req,
46+
PayloadHash: []byte("UNSIGNED-PAYLOAD"),
47+
Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
48+
Credentials: credentials.Credentials{AccessKeyID: "AKID", SecretAccessKey: "SECRET"},
49+
Algorithm: "AWS4-HMAC-SHA256",
50+
CredentialScope: "20240101/us-east-1/service/aws4_request",
51+
Finalizer: identityFinalizer{},
52+
SignatureType: v4.SignatureTypeQueryString,
53+
}
54+
55+
err = s.Do()
56+
if err != nil {
57+
t.Fatal(err)
58+
}
59+
60+
query := req.URL.Query()
61+
if !query.Has("X-Amz-SignedHeaders") {
62+
t.Error("X-Amz-SignedHeaders missing from query string")
63+
}
64+
if !query.Has("X-Amz-Algorithm") {
65+
t.Error("X-Amz-Algorithm missing from query string")
66+
}
67+
if !query.Has("X-Amz-Credential") {
68+
t.Error("X-Amz-Credential missing from query string")
69+
}
70+
if !query.Has("X-Amz-Date") {
71+
t.Error("X-Amz-Date missing from query string")
72+
}
73+
if !query.Has("X-Amz-Signature") {
74+
t.Error("X-Amz-Signature missing from query string")
75+
}
76+
if !query.Has("existing") {
77+
t.Error("existing query parameter should be preserved")
78+
}
79+
}
80+
81+
func TestQueryStringAuth_WithSessionToken(t *testing.T) {
82+
req, err := http.NewRequest(http.MethodGet, "https://service.region.amazonaws.com/path", nil)
83+
if err != nil {
84+
t.Fatal(err)
85+
}
86+
87+
s := &Signer{
88+
Request: req,
89+
PayloadHash: []byte("UNSIGNED-PAYLOAD"),
90+
Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
91+
Credentials: credentials.Credentials{AccessKeyID: "AKID", SecretAccessKey: "SECRET", SessionToken: "TOKEN123"},
92+
Algorithm: "AWS4-HMAC-SHA256",
93+
CredentialScope: "20240101/us-east-1/service/aws4_request",
94+
Finalizer: identityFinalizer{},
95+
SignatureType: v4.SignatureTypeQueryString,
96+
}
97+
98+
err = s.Do()
99+
if err != nil {
100+
t.Fatal(err)
101+
}
102+
103+
query := req.URL.Query()
104+
if !query.Has("X-Amz-Security-Token") {
105+
t.Error("X-Amz-Security-Token missing from query string")
106+
}
107+
if query.Get("X-Amz-Security-Token") != "TOKEN123" {
108+
t.Errorf("X-Amz-Security-Token = %q, want %q", query.Get("X-Amz-Security-Token"), "TOKEN123")
109+
}
110+
}
111+
112+
func TestQueryStringAuth_WithRegionSet(t *testing.T) {
113+
req, err := http.NewRequest(http.MethodGet, "https://service.region.amazonaws.com/path", nil)
114+
if err != nil {
115+
t.Fatal(err)
116+
}
117+
118+
// Pre-populate X-Amz-Region-Set as query parameter (simulating SigV4a)
119+
query := req.URL.Query()
120+
query.Set("X-Amz-Region-Set", "us-east-1,us-west-2")
121+
req.URL.RawQuery = query.Encode()
122+
123+
s := &Signer{
124+
Request: req,
125+
PayloadHash: []byte("UNSIGNED-PAYLOAD"),
126+
Time: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
127+
Credentials: credentials.Credentials{AccessKeyID: "AKID", SecretAccessKey: "SECRET"},
128+
Algorithm: "AWS4-ECDSA-P256-SHA256",
129+
CredentialScope: "20240101/service/aws4_request",
130+
Finalizer: identityFinalizer{},
131+
SignatureType: v4.SignatureTypeQueryString,
132+
}
133+
134+
err = s.Do()
135+
if err != nil {
136+
t.Fatal(err)
137+
}
138+
139+
finalQuery := req.URL.Query()
140+
if !finalQuery.Has("X-Amz-Region-Set") {
141+
t.Error("X-Amz-Region-Set missing from final query string")
142+
}
143+
if finalQuery.Get("X-Amz-Region-Set") != "us-east-1,us-west-2" {
144+
t.Errorf("X-Amz-Region-Set = %q, want %q", finalQuery.Get("X-Amz-Region-Set"), "us-east-1,us-west-2")
145+
}
146+
}
147+
37148
func TestBuildCanonicalRequest_SignedPayload(t *testing.T) {
38149
req, err := http.NewRequest(http.MethodPost,
39150
"https://service.region.amazonaws.com",

aws-http-auth/sigv4/sigv4.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717

1818
const algorithm = "AWS4-HMAC-SHA256"
1919

20+
21+
2022
// Signer signs requests with AWS Signature version 4.
2123
type Signer struct {
2224
options v4.SignerOptions
@@ -66,6 +68,9 @@ type SignRequestInput struct {
6668
// If the zero-value is given (generally by the caller not setting it), the
6769
// signer will instead use the current system clock time for the signature.
6870
Time time.Time
71+
72+
// How the signature is transmitted (header or query string).
73+
SignatureType v4.SignatureType
6974
}
7075

7176
// SignRequest signs an HTTP request with AWS Signature Version 4, modifying
@@ -107,8 +112,9 @@ func (s *Signer) SignRequest(in *SignRequestInput, opts ...v4.SignerOption) erro
107112
Credentials: in.Credentials,
108113
Options: options,
109114

110-
Algorithm: algorithm,
111-
CredentialScope: scope(tm, in.Region, in.Service),
115+
Algorithm: algorithm,
116+
CredentialScope: scope(tm, in.Region, in.Service),
117+
SignatureType: in.SignatureType,
112118
Finalizer: &finalizer{
113119
Secret: in.Credentials.SecretAccessKey,
114120
Service: in.Service,

0 commit comments

Comments
 (0)