Skip to content

Commit 3fd94ec

Browse files
committed
Fix PAT auth: Bearer-only, no API key/app key headers (CRED-2147)
PAT auth is a standalone auth path — when a PAT is configured, the client sends only Authorization: Bearer <token> with no DD-API-KEY or DD-APPLICATION-KEY headers. This is mutually exclusive with the existing API key + app key flow. Changes: - Add ContextPATKey context variable to store PAT separately - SetAuthKeys: when PAT is present, send only Authorization: Bearer and skip all other auth headers (apiKeyAuth, appKeyAuth) - NewDefaultContext: store DD_PAT env var in ContextPATKey (not appKeyAuth) - Update template (.j2) to match - Update unit tests to verify Bearer-only behavior (no API key headers) - Add mock-server test confirming full HTTP flow sends correct headers Verified against staging: Bearer-only auth returns 200 on /api/v2/users and /api/v2/current_user.
1 parent 57ebae9 commit 3fd94ec

File tree

6 files changed

+141
-9
lines changed

6 files changed

+141
-9
lines changed

.generator/src/generator/templates/configuration.j2

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,9 @@ func NewDefaultContext(ctx context.Context) context.Context {
400400
}
401401

402402
{%- endfor %}
403+
if pat, ok := os.LookupEnv("DD_PAT"); ok {
404+
ctx = context.WithValue(ctx, ContextPATKey, pat)
405+
}
403406
ctx = context.WithValue(
404407
ctx,
405408
ContextAPIKeys,

api/datadog/client.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,21 @@ func UseDelegatedTokenAuth(ctx context.Context, headerParams *map[string]string,
8686
}
8787

8888
// SetAuthKeys sets the appropriate values in the headers parameter.
89+
// When a PAT (Personal Access Token) is present in the context, only the
90+
// Authorization: Bearer header is sent. No DD-API-KEY or DD-APPLICATION-KEY
91+
// headers are included — PAT auth is a standalone auth path.
8992
func SetAuthKeys(ctx context.Context, headerParams *map[string]string, keys ...[2]string) {
90-
if ctx != nil {
91-
for _, key := range keys {
92-
if auth, ok := ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
93-
if apiKey, ok := auth[key[0]]; ok {
94-
(*headerParams)[key[1]] = apiKey.Key
95-
}
93+
if ctx == nil {
94+
return
95+
}
96+
if pat, ok := ctx.Value(ContextPATKey).(string); ok {
97+
(*headerParams)[authorizationHeader] = fmt.Sprintf(bearerTokenFormat, pat)
98+
return
99+
}
100+
for _, key := range keys {
101+
if auth, ok := ctx.Value(ContextAPIKeys).(map[string]APIKey); ok {
102+
if apiKey, ok := auth[key[0]]; ok {
103+
(*headerParams)[key[1]] = apiKey.Key
96104
}
97105
}
98106
}

api/datadog/configuration.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ var (
6262

6363
// ContextOperationServerVariables overrides a server configuration variables using operation specific values.
6464
ContextOperationServerVariables = contextKey("serverOperationVariables")
65+
66+
// ContextPATKey holds the Personal Access Token for Bearer auth.
67+
ContextPATKey = contextKey("patKey")
6568
)
6669

6770
// BasicAuth provides basic http authentication to a request passed via context using ContextBasicAuth.
@@ -1064,6 +1067,9 @@ func NewDefaultContext(ctx context.Context) context.Context {
10641067
if apiKey, ok := os.LookupEnv("DD_APP_KEY"); ok {
10651068
keys["appKeyAuth"] = APIKey{Key: apiKey}
10661069
}
1070+
if pat, ok := os.LookupEnv("DD_PAT"); ok {
1071+
ctx = context.WithValue(ctx, ContextPATKey, pat)
1072+
}
10671073
ctx = context.WithValue(
10681074
ctx,
10691075
ContextAPIKeys,

tests/api/client_test.go

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@ package api
33
import (
44
"context"
55
"fmt"
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
"time"
10+
611
"github.com/DataDog/datadog-api-client-go/v2/api/datadog"
12+
"github.com/DataDog/datadog-api-client-go/v2/api/datadogV2"
713
"github.com/DataDog/datadog-api-client-go/v2/tests/api/mocks"
814
"github.com/golang/mock/gomock"
915
"github.com/stretchr/testify/assert"
10-
"testing"
11-
"time"
16+
"github.com/stretchr/testify/require"
1217
)
1318

1419
const FAKE_TOKEN = "fake-token"
@@ -155,3 +160,103 @@ func TestApiAppKeyAuthenticate(t *testing.T) {
155160
assert.Equal(t, headers["DD-API-KEY"], apiKey)
156161
assert.Equal(t, headers["DD-APPLICATION-KEY"], appKey)
157162
}
163+
164+
func TestPATAuthentication(t *testing.T) {
165+
pat := "ddapp_test-pat-token"
166+
keys := map[string]datadog.APIKey{
167+
"apiKeyAuth": {Key: "api-key"},
168+
}
169+
testAuthCtx := context.WithValue(
170+
context.Background(),
171+
datadog.ContextAPIKeys,
172+
keys,
173+
)
174+
testAuthCtx = context.WithValue(testAuthCtx, datadog.ContextPATKey, pat)
175+
176+
headers := map[string]string{}
177+
datadog.SetAuthKeys(
178+
testAuthCtx,
179+
&headers,
180+
[2]string{"apiKeyAuth", "DD-API-KEY"},
181+
[2]string{"appKeyAuth", "DD-APPLICATION-KEY"},
182+
)
183+
assert.Equal(t, "Bearer ddapp_test-pat-token", headers["Authorization"])
184+
assert.Empty(t, headers["DD-API-KEY"], "PAT auth should not send DD-API-KEY")
185+
assert.Empty(t, headers["DD-APPLICATION-KEY"], "PAT auth should not send DD-APPLICATION-KEY")
186+
}
187+
188+
func TestNewDefaultContextWithPAT(t *testing.T) {
189+
t.Setenv("DD_API_KEY", "test-api-key")
190+
t.Setenv("DD_PAT", "ddapp_test-pat-token")
191+
192+
ctx := datadog.NewDefaultContext(context.Background())
193+
keys := ctx.Value(datadog.ContextAPIKeys).(map[string]datadog.APIKey)
194+
assert.Equal(t, "test-api-key", keys["apiKeyAuth"].Key)
195+
pat := ctx.Value(datadog.ContextPATKey).(string)
196+
assert.Equal(t, "ddapp_test-pat-token", pat)
197+
}
198+
199+
func TestNewDefaultContextPATOverridesAppKey(t *testing.T) {
200+
t.Setenv("DD_API_KEY", "test-api-key")
201+
t.Setenv("DD_APP_KEY", "test-app-key")
202+
t.Setenv("DD_PAT", "ddapp_test-pat-token")
203+
204+
ctx := datadog.NewDefaultContext(context.Background())
205+
// App key is still stored, but PAT takes precedence in SetAuthKeys
206+
keys := ctx.Value(datadog.ContextAPIKeys).(map[string]datadog.APIKey)
207+
assert.Equal(t, "test-app-key", keys["appKeyAuth"].Key)
208+
pat := ctx.Value(datadog.ContextPATKey).(string)
209+
assert.Equal(t, "ddapp_test-pat-token", pat)
210+
211+
// Verify PAT is used as Bearer — no API key or app key headers
212+
headers := map[string]string{}
213+
datadog.SetAuthKeys(
214+
ctx,
215+
&headers,
216+
[2]string{"apiKeyAuth", "DD-API-KEY"},
217+
[2]string{"appKeyAuth", "DD-APPLICATION-KEY"},
218+
)
219+
assert.Equal(t, "Bearer ddapp_test-pat-token", headers["Authorization"])
220+
assert.Empty(t, headers["DD-API-KEY"], "PAT auth should not send DD-API-KEY")
221+
assert.Empty(t, headers["DD-APPLICATION-KEY"], "PAT auth should not send DD-APPLICATION-KEY")
222+
}
223+
224+
func TestNewDefaultContextAppKeyWithoutPAT(t *testing.T) {
225+
t.Setenv("DD_API_KEY", "test-api-key")
226+
t.Setenv("DD_APP_KEY", "test-app-key")
227+
228+
ctx := datadog.NewDefaultContext(context.Background())
229+
keys := ctx.Value(datadog.ContextAPIKeys).(map[string]datadog.APIKey)
230+
assert.Equal(t, "test-app-key", keys["appKeyAuth"].Key)
231+
assert.Nil(t, ctx.Value(datadog.ContextPATKey))
232+
}
233+
234+
func TestPATBearerHeaderSentViaHTTPClient(t *testing.T) {
235+
pat := "ddapp_test-token"
236+
237+
var capturedHeaders http.Header
238+
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
239+
capturedHeaders = r.Header.Clone()
240+
w.Header().Set("Content-Type", "application/json")
241+
w.WriteHeader(200)
242+
fmt.Fprint(w, `{"data":[],"meta":{"page":{"total_count":0,"total_filtered_count":0}}}`)
243+
}))
244+
defer ts.Close()
245+
246+
ctx := context.Background()
247+
ctx = context.WithValue(ctx, datadog.ContextPATKey, pat)
248+
249+
cfg := datadog.NewConfiguration()
250+
cfg.Servers = datadog.ServerConfigurations{{URL: ts.URL}}
251+
client := datadog.NewAPIClient(cfg)
252+
usersAPI := datadogV2.NewUsersApi(client)
253+
254+
_, httpResp, err := usersAPI.ListUsers(ctx, *datadogV2.NewListUsersOptionalParameters())
255+
require.NoError(t, err)
256+
defer httpResp.Body.Close()
257+
258+
assert.Equal(t, 200, httpResp.StatusCode)
259+
assert.Equal(t, "Bearer "+pat, capturedHeaders.Get("Authorization"))
260+
assert.Empty(t, capturedHeaders.Get("DD-API-KEY"), "PAT auth should not send DD-API-KEY")
261+
assert.Empty(t, capturedHeaders.Get("DD-APPLICATION-KEY"), "PAT auth should not send DD-APPLICATION-KEY")
262+
}

tests/go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
module github.com/DataDog/datadog-api-client-go/v2/tests
22

3-
go 1.22
3+
go 1.23.0
4+
45
toolchain go1.23.7
56

67
require (

tests/go.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ github.com/tinylib/msgp v1.1.6 h1:i+SbKraHhnrf9M5MYmvQhFnbLhAXSDWF8WWsuyRdocw=
114114
github.com/tinylib/msgp v1.1.6/go.mod h1:75BAfg2hauQhs3qedfdDZmWAPcFMAvJE5b9rGOMufyw=
115115
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
116116
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
117+
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
117118
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
118119
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
119120
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@@ -123,6 +124,7 @@ golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvx
123124
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
124125
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
125126
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
127+
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
126128
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
127129
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
128130
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -132,6 +134,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
132134
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
133135
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
134136
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
137+
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
135138
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
136139
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
137140
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -142,15 +145,20 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
142145
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
143146
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
144147
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
148+
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
145149
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
146150
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
147151
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
148152
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
149153
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
150154
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
155+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
151156
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
157+
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
158+
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
152159
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
153160
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
161+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
154162
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
155163
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
156164
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -167,6 +175,7 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
167175
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
168176
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
169177
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
178+
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
170179
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
171180
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
172181
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

0 commit comments

Comments
 (0)