From b6b8d6dd6ab2361f2cdfbb0553f32dc4f9d6b9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Tue, 27 Jan 2026 16:05:17 +0100 Subject: [PATCH 01/10] Fix panic --- lib/web/headless.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/web/headless.go b/lib/web/headless.go index b5c25a5f5da81..937fe679e7be3 100644 --- a/lib/web/headless.go +++ b/lib/web/headless.go @@ -24,6 +24,7 @@ import ( "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" + "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/httplib" @@ -71,9 +72,14 @@ func (h *Handler) putHeadlessState(_ http.ResponseWriter, r *http.Request, param } } - mfaResp, err := req.MFAResponse.GetOptionalMFAResponseProtoReq() - if err != nil { - return nil, trace.Wrap(err) + // MFAResponse is required only when accepting a request. + var mfaResp *proto.MFAAuthenticateResponse + if req.MFAResponse != nil { + var err error + mfaResp, err = req.MFAResponse.GetOptionalMFAResponseProtoReq() + if err != nil { + return nil, trace.Wrap(err) + } } var action types.HeadlessAuthenticationState From fd64d79e701ccfc2a0b8dbfcbc2b90beb17b4b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Tue, 27 Jan 2026 16:05:17 +0100 Subject: [PATCH 02/10] Add tests --- lib/web/headless_test.go | 318 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 318 insertions(+) create mode 100644 lib/web/headless_test.go diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go new file mode 100644 index 0000000000000..903a27df0534f --- /dev/null +++ b/lib/web/headless_test.go @@ -0,0 +1,318 @@ +/* + * Teleport + * Copyright (C) 2025 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package web + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" + + authproto "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/constants" + mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/authtest" + wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" + "github.com/gravitational/teleport/lib/client" + "github.com/gravitational/teleport/lib/services" +) + +func TestPutHeadlessState(t *testing.T) { + t.Parallel() + + ctx := context.Background() + env := newWebPack(t, 1) + proxy := env.proxies[0] + + // Create a user with appropriate roles + username := "headless-test-user" + role := services.NewPresetEditorRole() + pack := proxy.authPack(t, username, []types.Role{role}) + + // Set up WebAuthn as the second factor. + ap, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{ + Type: constants.Local, + SecondFactor: constants.SecondFactorWebauthn, + Webauthn: &types.Webauthn{ + RPID: "localhost", + }, + }) + require.NoError(t, err) + _, err = env.server.Auth().UpsertAuthPreference(ctx, ap) + require.NoError(t, err) + + // Register a WebAuthn device for the user. + userClient, err := env.server.NewClient(authtest.TestUser(username)) + require.NoError(t, err) + webauthnDev, err := authtest.RegisterTestDevice( + ctx, + userClient, + "webauthn", + authproto.DeviceType_DEVICE_TYPE_WEBAUTHN, + nil, /* authenticator */ + ) + require.NoError(t, err) + + getMFAResponse := func() *client.MFAChallengeResponse { + // Create an authentication challenge and solve it with the WebAuthn device. + chal, err := userClient.CreateAuthenticateChallenge(ctx, &authproto.CreateAuthenticateChallengeRequest{ + Request: &authproto.CreateAuthenticateChallengeRequest_ContextUser{}, + ChallengeExtensions: &mfav1.ChallengeExtensions{ + Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_HEADLESS_LOGIN, + }, + }) + require.NoError(t, err) + mfaResp, err := webauthnDev.SolveAuthn(chal) + require.NoError(t, err) + + return &client.MFAChallengeResponse{ + WebauthnResponse: wantypes.CredentialAssertionResponseFromProto(mfaResp.GetWebauthn()), + } + } + + tests := []struct { + name string + // setupHeadless creates new headless auth and returns its ID. If an actual headless auth ID + // is not needed (e.g. to test a case where the headless auth ID is incorrect), use headlessID + // instead. + setupHeadless func() string + // headlessID is used when setupHeadless is nil. + headlessID string + request client.HeadlessRequest + useMFA bool + expectedStatus int + expectedErrMsg string + }{ + { + name: "invalid headless authentication ID", + headlessID: "non-existent-id", + request: client.HeadlessRequest{ + Action: "denied", + }, + expectedStatus: 404, + expectedErrMsg: "not found", + }, + { + name: "invalid action", + setupHeadless: func() string { + sshPubKey := []byte("fake-ssh-public-key-invalid-action") + headlessID := services.NewHeadlessAuthenticationID(sshPubKey) + + ha, err := types.NewHeadlessAuthentication(username, headlessID, env.clock.Now().Add(5*time.Minute)) + require.NoError(t, err) + ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING + ha.SshPublicKey = sshPubKey + + err = env.server.Auth().UpsertHeadlessAuthentication(ctx, ha) + require.NoError(t, err) + return ha.GetName() + }, + request: client.HeadlessRequest{ + Action: "invalid-action", + }, + expectedStatus: 400, + expectedErrMsg: "unknown action invalid-action", + }, + { + name: "accept without MFA response", + setupHeadless: func() string { + sshPubKey := []byte("fake-ssh-public-key-accept-no-mfa") + headlessID := services.NewHeadlessAuthenticationID(sshPubKey) + + ha, err := types.NewHeadlessAuthentication(username, headlessID, env.clock.Now().Add(5*time.Minute)) + require.NoError(t, err) + ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING + ha.SshPublicKey = sshPubKey + + err = env.server.Auth().UpsertHeadlessAuthentication(ctx, ha) + require.NoError(t, err) + return ha.GetName() + }, + request: client.HeadlessRequest{ + Action: "accept", + }, + expectedStatus: 400, + expectedErrMsg: "expected MFA auth challenge response", + }, + { + name: "accept with MFA response", + setupHeadless: func() string { + // Create the headless authentication request. + sshPubKey := []byte("fake-ssh-public-key-accept-with-mfa") + headlessID := services.NewHeadlessAuthenticationID(sshPubKey) + + ha, err := types.NewHeadlessAuthentication(username, headlessID, env.clock.Now().Add(5*time.Minute)) + require.NoError(t, err) + ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING + ha.SshPublicKey = sshPubKey + + err = env.server.Auth().UpsertHeadlessAuthentication(ctx, ha) + require.NoError(t, err) + + return ha.GetName() + }, + useMFA: true, + request: client.HeadlessRequest{ + Action: "accept", + }, + expectedStatus: 200, + }, + { + name: "denied without MFA response", + setupHeadless: func() string { + sshPubKey := []byte("fake-ssh-public-key") + headlessID := services.NewHeadlessAuthenticationID(sshPubKey) + + ha, err := types.NewHeadlessAuthentication(username, headlessID, env.clock.Now().Add(5*time.Minute)) + require.NoError(t, err) + ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING + ha.SshPublicKey = sshPubKey + + err = env.server.Auth().UpsertHeadlessAuthentication(ctx, ha) + require.NoError(t, err) + return ha.GetName() + }, + request: client.HeadlessRequest{ + Action: "denied", + }, + expectedStatus: 200, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var headlessID string + if tt.setupHeadless != nil { + headlessID = tt.setupHeadless() + } else { + headlessID = tt.headlessID + } + + request := tt.request + if tt.useMFA { + request.MFAResponse = getMFAResponse() + } + + endpoint := pack.clt.Endpoint("webapi", "headless", headlessID) + resp, err := pack.clt.PutJSON(ctx, endpoint, request) + + require.Equal(t, tt.expectedStatus, resp.Code(), "unexpected status code") + if tt.expectedStatus == 200 { + require.NoError(t, err) + } else { + require.Error(t, err) + } + + if tt.expectedStatus != 200 { + require.ErrorContains(t, err, tt.expectedErrMsg, "unexpected error message") + return + } + + // Verify the state was updated correctly. + ha, err := env.server.Auth().GetHeadlessAuthentication(ctx, username, headlessID) + require.NoError(t, err) + + var expectedState types.HeadlessAuthenticationState + switch tt.request.Action { + case "accept": + expectedState = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED + case "denied": + expectedState = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED + } + require.Equal(t, expectedState, ha.State) + }) + } +} + +func TestGetHeadless(t *testing.T) { + t.Parallel() + + ctx := context.Background() + env := newWebPack(t, 1) + proxy := env.proxies[0] + + username := "headless-get-user" + role := services.NewPresetEditorRole() + pack := proxy.authPack(t, username, []types.Role{role}) + + tests := []struct { + name string + // setupHeadless creates new headless auth and returns its ID. If an actual headless auth ID + // is not needed (e.g. to test a case where the headless auth ID is incorrect), use headlessID + // instead. + setupHeadless func() string + // headlessID is used when setupHeadless is nil. + headlessID string + expectedStatus int + expectedErrMsg string + }{ + { + name: "get existing headless authentication", + setupHeadless: func() string { + sshPubKey := []byte("fake-ssh-public-key-get") + headlessID := services.NewHeadlessAuthenticationID(sshPubKey) + + ha, err := types.NewHeadlessAuthentication(username, headlessID, env.clock.Now().Add(5*time.Minute)) + require.NoError(t, err) + ha.State = types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_PENDING + ha.SshPublicKey = sshPubKey + + err = env.server.Auth().UpsertHeadlessAuthentication(ctx, ha) + require.NoError(t, err) + return ha.GetName() + }, + expectedStatus: 200, + }, + { + name: "non-existent headless authentication", + headlessID: "non-existent-id", + expectedStatus: 400, + expectedErrMsg: "requested invalid headless session", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var headlessID string + if tt.setupHeadless != nil { + headlessID = tt.setupHeadless() + } else { + headlessID = tt.headlessID + } + + endpoint := pack.clt.Endpoint("webapi", "headless", headlessID) + resp, err := pack.clt.Get(ctx, endpoint, nil) + + require.Equal(t, tt.expectedStatus, resp.Code(), "unexpected status code") + + if tt.expectedStatus != 200 { + require.ErrorContains(t, err, tt.expectedErrMsg, "unexpected error message") + return + } + + var ha types.HeadlessAuthentication + err = json.Unmarshal(resp.Bytes(), &ha) + require.NoError(t, err) + require.Equal(t, headlessID, ha.Metadata.Name) + }) + } +} From a63209c8c10fceea37e6ba8bbed0a966e73102f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 28 Jan 2026 15:36:26 +0100 Subject: [PATCH 03/10] Use `t.Context` instead of `context.Background` --- lib/web/headless_test.go | 32 +++++++++++++++----------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go index 903a27df0534f..1ad7d39d3f8ca 100644 --- a/lib/web/headless_test.go +++ b/lib/web/headless_test.go @@ -39,7 +39,6 @@ import ( func TestPutHeadlessState(t *testing.T) { t.Parallel() - ctx := context.Background() env := newWebPack(t, 1) proxy := env.proxies[0] @@ -57,14 +56,14 @@ func TestPutHeadlessState(t *testing.T) { }, }) require.NoError(t, err) - _, err = env.server.Auth().UpsertAuthPreference(ctx, ap) + _, err = env.server.Auth().UpsertAuthPreference(t.Context(), ap) require.NoError(t, err) // Register a WebAuthn device for the user. userClient, err := env.server.NewClient(authtest.TestUser(username)) require.NoError(t, err) webauthnDev, err := authtest.RegisterTestDevice( - ctx, + t.Context(), userClient, "webauthn", authproto.DeviceType_DEVICE_TYPE_WEBAUTHN, @@ -74,7 +73,7 @@ func TestPutHeadlessState(t *testing.T) { getMFAResponse := func() *client.MFAChallengeResponse { // Create an authentication challenge and solve it with the WebAuthn device. - chal, err := userClient.CreateAuthenticateChallenge(ctx, &authproto.CreateAuthenticateChallengeRequest{ + chal, err := userClient.CreateAuthenticateChallenge(t.Context(), &authproto.CreateAuthenticateChallengeRequest{ Request: &authproto.CreateAuthenticateChallengeRequest_ContextUser{}, ChallengeExtensions: &mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_HEADLESS_LOGIN, @@ -94,7 +93,7 @@ func TestPutHeadlessState(t *testing.T) { // setupHeadless creates new headless auth and returns its ID. If an actual headless auth ID // is not needed (e.g. to test a case where the headless auth ID is incorrect), use headlessID // instead. - setupHeadless func() string + setupHeadless func(context.Context) string // headlessID is used when setupHeadless is nil. headlessID string request client.HeadlessRequest @@ -113,7 +112,7 @@ func TestPutHeadlessState(t *testing.T) { }, { name: "invalid action", - setupHeadless: func() string { + setupHeadless: func(ctx context.Context) string { sshPubKey := []byte("fake-ssh-public-key-invalid-action") headlessID := services.NewHeadlessAuthenticationID(sshPubKey) @@ -134,7 +133,7 @@ func TestPutHeadlessState(t *testing.T) { }, { name: "accept without MFA response", - setupHeadless: func() string { + setupHeadless: func(ctx context.Context) string { sshPubKey := []byte("fake-ssh-public-key-accept-no-mfa") headlessID := services.NewHeadlessAuthenticationID(sshPubKey) @@ -155,7 +154,7 @@ func TestPutHeadlessState(t *testing.T) { }, { name: "accept with MFA response", - setupHeadless: func() string { + setupHeadless: func(ctx context.Context) string { // Create the headless authentication request. sshPubKey := []byte("fake-ssh-public-key-accept-with-mfa") headlessID := services.NewHeadlessAuthenticationID(sshPubKey) @@ -178,7 +177,7 @@ func TestPutHeadlessState(t *testing.T) { }, { name: "denied without MFA response", - setupHeadless: func() string { + setupHeadless: func(ctx context.Context) string { sshPubKey := []byte("fake-ssh-public-key") headlessID := services.NewHeadlessAuthenticationID(sshPubKey) @@ -202,7 +201,7 @@ func TestPutHeadlessState(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var headlessID string if tt.setupHeadless != nil { - headlessID = tt.setupHeadless() + headlessID = tt.setupHeadless(t.Context()) } else { headlessID = tt.headlessID } @@ -213,7 +212,7 @@ func TestPutHeadlessState(t *testing.T) { } endpoint := pack.clt.Endpoint("webapi", "headless", headlessID) - resp, err := pack.clt.PutJSON(ctx, endpoint, request) + resp, err := pack.clt.PutJSON(t.Context(), endpoint, request) require.Equal(t, tt.expectedStatus, resp.Code(), "unexpected status code") if tt.expectedStatus == 200 { @@ -228,7 +227,7 @@ func TestPutHeadlessState(t *testing.T) { } // Verify the state was updated correctly. - ha, err := env.server.Auth().GetHeadlessAuthentication(ctx, username, headlessID) + ha, err := env.server.Auth().GetHeadlessAuthentication(t.Context(), username, headlessID) require.NoError(t, err) var expectedState types.HeadlessAuthenticationState @@ -246,7 +245,6 @@ func TestPutHeadlessState(t *testing.T) { func TestGetHeadless(t *testing.T) { t.Parallel() - ctx := context.Background() env := newWebPack(t, 1) proxy := env.proxies[0] @@ -259,7 +257,7 @@ func TestGetHeadless(t *testing.T) { // setupHeadless creates new headless auth and returns its ID. If an actual headless auth ID // is not needed (e.g. to test a case where the headless auth ID is incorrect), use headlessID // instead. - setupHeadless func() string + setupHeadless func(context.Context) string // headlessID is used when setupHeadless is nil. headlessID string expectedStatus int @@ -267,7 +265,7 @@ func TestGetHeadless(t *testing.T) { }{ { name: "get existing headless authentication", - setupHeadless: func() string { + setupHeadless: func(ctx context.Context) string { sshPubKey := []byte("fake-ssh-public-key-get") headlessID := services.NewHeadlessAuthenticationID(sshPubKey) @@ -294,13 +292,13 @@ func TestGetHeadless(t *testing.T) { t.Run(tt.name, func(t *testing.T) { var headlessID string if tt.setupHeadless != nil { - headlessID = tt.setupHeadless() + headlessID = tt.setupHeadless(t.Context()) } else { headlessID = tt.headlessID } endpoint := pack.clt.Endpoint("webapi", "headless", headlessID) - resp, err := pack.clt.Get(ctx, endpoint, nil) + resp, err := pack.clt.Get(t.Context(), endpoint, nil) require.Equal(t, tt.expectedStatus, resp.Code(), "unexpected status code") From 3345fa742d82a09bb808c2835447865f699ca1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 28 Jan 2026 15:38:53 +0100 Subject: [PATCH 04/10] Remove need for `headlessID` as separate field --- lib/web/headless_test.go | 42 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go index 1ad7d39d3f8ca..ba89ffdc200ff 100644 --- a/lib/web/headless_test.go +++ b/lib/web/headless_test.go @@ -90,20 +90,18 @@ func TestPutHeadlessState(t *testing.T) { tests := []struct { name string - // setupHeadless creates new headless auth and returns its ID. If an actual headless auth ID - // is not needed (e.g. to test a case where the headless auth ID is incorrect), use headlessID - // instead. - setupHeadless func(context.Context) string - // headlessID is used when setupHeadless is nil. - headlessID string + // setupHeadless creates new headless auth and returns its ID. + setupHeadless func(context.Context) string request client.HeadlessRequest useMFA bool expectedStatus int expectedErrMsg string }{ { - name: "invalid headless authentication ID", - headlessID: "non-existent-id", + name: "invalid headless authentication ID", + setupHeadless: func(ctx context.Context) string { + return "non-existent-id" + }, request: client.HeadlessRequest{ Action: "denied", }, @@ -199,12 +197,7 @@ func TestPutHeadlessState(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var headlessID string - if tt.setupHeadless != nil { - headlessID = tt.setupHeadless(t.Context()) - } else { - headlessID = tt.headlessID - } + headlessID := tt.setupHeadless(t.Context()) request := tt.request if tt.useMFA { @@ -254,12 +247,8 @@ func TestGetHeadless(t *testing.T) { tests := []struct { name string - // setupHeadless creates new headless auth and returns its ID. If an actual headless auth ID - // is not needed (e.g. to test a case where the headless auth ID is incorrect), use headlessID - // instead. - setupHeadless func(context.Context) string - // headlessID is used when setupHeadless is nil. - headlessID string + // setupHeadless creates new headless auth and returns its ID. + setupHeadless func(context.Context) string expectedStatus int expectedErrMsg string }{ @@ -281,8 +270,10 @@ func TestGetHeadless(t *testing.T) { expectedStatus: 200, }, { - name: "non-existent headless authentication", - headlessID: "non-existent-id", + name: "non-existent headless authentication", + setupHeadless: func(ctx context.Context) string { + return "non-existent-id" + }, expectedStatus: 400, expectedErrMsg: "requested invalid headless session", }, @@ -290,12 +281,7 @@ func TestGetHeadless(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - var headlessID string - if tt.setupHeadless != nil { - headlessID = tt.setupHeadless(t.Context()) - } else { - headlessID = tt.headlessID - } + headlessID := tt.setupHeadless(t.Context()) endpoint := pack.clt.Endpoint("webapi", "headless", headlessID) resp, err := pack.clt.Get(t.Context(), endpoint, nil) From 5afeca97869ffd5863925bfedf7536b87a5d3f9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 28 Jan 2026 15:40:55 +0100 Subject: [PATCH 05/10] Simplify err asserts when `expectedStatus` is 200 --- lib/web/headless_test.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go index ba89ffdc200ff..c092f57ab1054 100644 --- a/lib/web/headless_test.go +++ b/lib/web/headless_test.go @@ -208,16 +208,12 @@ func TestPutHeadlessState(t *testing.T) { resp, err := pack.clt.PutJSON(t.Context(), endpoint, request) require.Equal(t, tt.expectedStatus, resp.Code(), "unexpected status code") - if tt.expectedStatus == 200 { - require.NoError(t, err) - } else { - require.Error(t, err) - } if tt.expectedStatus != 200 { require.ErrorContains(t, err, tt.expectedErrMsg, "unexpected error message") return } + require.NoError(t, err) // Verify the state was updated correctly. ha, err := env.server.Auth().GetHeadlessAuthentication(t.Context(), username, headlessID) @@ -292,6 +288,7 @@ func TestGetHeadless(t *testing.T) { require.ErrorContains(t, err, tt.expectedErrMsg, "unexpected error message") return } + require.NoError(t, err) var ha types.HeadlessAuthentication err = json.Unmarshal(resp.Bytes(), &ha) From dac50106752a5044b993a52b8b98aa03ac747a91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 28 Jan 2026 15:41:28 +0100 Subject: [PATCH 06/10] Update copyright year --- lib/web/headless_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go index c092f57ab1054..69d1684bca811 100644 --- a/lib/web/headless_test.go +++ b/lib/web/headless_test.go @@ -1,6 +1,6 @@ /* * Teleport - * Copyright (C) 2025 Gravitational, Inc. + * Copyright (C) 2026 Gravitational, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by From 9e8a9166bfff37e753ff1650c374a2a415545e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 28 Jan 2026 15:43:04 +0100 Subject: [PATCH 07/10] Close `userClient` --- lib/web/headless_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go index 69d1684bca811..8c5c8dce88978 100644 --- a/lib/web/headless_test.go +++ b/lib/web/headless_test.go @@ -62,6 +62,9 @@ func TestPutHeadlessState(t *testing.T) { // Register a WebAuthn device for the user. userClient, err := env.server.NewClient(authtest.TestUser(username)) require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, userClient.Close()) + }) webauthnDev, err := authtest.RegisterTestDevice( t.Context(), userClient, From 867759d17e256dbca73310e1ecbc58764adaeacb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 28 Jan 2026 15:45:19 +0100 Subject: [PATCH 08/10] Make `getMFAResponse` a helper --- lib/web/headless_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go index 8c5c8dce88978..d216d65590195 100644 --- a/lib/web/headless_test.go +++ b/lib/web/headless_test.go @@ -75,6 +75,7 @@ func TestPutHeadlessState(t *testing.T) { require.NoError(t, err) getMFAResponse := func() *client.MFAChallengeResponse { + t.Helper() // Create an authentication challenge and solve it with the WebAuthn device. chal, err := userClient.CreateAuthenticateChallenge(t.Context(), &authproto.CreateAuthenticateChallengeRequest{ Request: &authproto.CreateAuthenticateChallengeRequest_ContextUser{}, From d827e2565cd77d69d1de6e5795357e005c801737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Wed, 28 Jan 2026 15:45:19 +0100 Subject: [PATCH 09/10] Remove unnecessary empty lines --- lib/web/headless_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/web/headless_test.go b/lib/web/headless_test.go index d216d65590195..b59a23362e9c3 100644 --- a/lib/web/headless_test.go +++ b/lib/web/headless_test.go @@ -210,9 +210,7 @@ func TestPutHeadlessState(t *testing.T) { endpoint := pack.clt.Endpoint("webapi", "headless", headlessID) resp, err := pack.clt.PutJSON(t.Context(), endpoint, request) - require.Equal(t, tt.expectedStatus, resp.Code(), "unexpected status code") - if tt.expectedStatus != 200 { require.ErrorContains(t, err, tt.expectedErrMsg, "unexpected error message") return @@ -285,9 +283,7 @@ func TestGetHeadless(t *testing.T) { endpoint := pack.clt.Endpoint("webapi", "headless", headlessID) resp, err := pack.clt.Get(t.Context(), endpoint, nil) - require.Equal(t, tt.expectedStatus, resp.Code(), "unexpected status code") - if tt.expectedStatus != 200 { require.ErrorContains(t, err, tt.expectedErrMsg, "unexpected error message") return From 8a04d993f1dea9d0a182ed80db9d248fdfbe6fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=C5=82=20Cie=C5=9Blak?= Date: Mon, 2 Feb 2026 12:13:39 +0100 Subject: [PATCH 10/10] Move nil check to `MFAChallengeResponse` --- lib/client/weblogin.go | 4 ++++ lib/web/headless.go | 11 +++-------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index d586da9c79476..b67bdd1184a56 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -135,6 +135,10 @@ type SSOResponse struct { // GetOptionalMFAResponseProtoReq converts response to a type proto.MFAAuthenticateResponse, // if there were any responses set. Otherwise returns nil. func (r *MFAChallengeResponse) GetOptionalMFAResponseProtoReq() (*proto.MFAAuthenticateResponse, error) { + if r == nil { + return nil, nil + } + var availableResponses int if r.TOTPCode != "" { availableResponses++ diff --git a/lib/web/headless.go b/lib/web/headless.go index 937fe679e7be3..4bb9306ef74cf 100644 --- a/lib/web/headless.go +++ b/lib/web/headless.go @@ -24,7 +24,6 @@ import ( "github.com/gravitational/trace" "github.com/julienschmidt/httprouter" - "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/client" "github.com/gravitational/teleport/lib/httplib" @@ -73,13 +72,9 @@ func (h *Handler) putHeadlessState(_ http.ResponseWriter, r *http.Request, param } // MFAResponse is required only when accepting a request. - var mfaResp *proto.MFAAuthenticateResponse - if req.MFAResponse != nil { - var err error - mfaResp, err = req.MFAResponse.GetOptionalMFAResponseProtoReq() - if err != nil { - return nil, trace.Wrap(err) - } + mfaResp, err := req.MFAResponse.GetOptionalMFAResponseProtoReq() + if err != nil { + return nil, trace.Wrap(err) } var action types.HeadlessAuthenticationState