mirror of
https://github.com/juanfont/headscale.git
synced 2026-04-14 12:59:56 +02:00
Render an interstitial showing device hostname, OS, and machine-key
fingerprint before finalising OIDC registration. The user must POST
to /register/confirm/{auth_id} with a CSRF double-submit cookie.
Removes the TODO at oidc.go:201.
103 lines
3.2 KiB
Go
103 lines
3.2 KiB
Go
package hscontrol
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/juanfont/headscale/hscontrol/types"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newConfirmRequest(t *testing.T, authID types.AuthID, formCSRF, cookieCSRF string) *http.Request {
|
|
t.Helper()
|
|
|
|
form := strings.NewReader(registerConfirmCSRFCookie + "=" + formCSRF)
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(),
|
|
http.MethodPost,
|
|
"/register/confirm/"+authID.String(),
|
|
form,
|
|
)
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.AddCookie(&http.Cookie{
|
|
Name: registerConfirmCSRFCookie,
|
|
Value: cookieCSRF,
|
|
})
|
|
|
|
rctx := chi.NewRouteContext()
|
|
rctx.URLParams.Add("auth_id", authID.String())
|
|
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
|
|
|
return req
|
|
}
|
|
|
|
// TestRegisterConfirmHandler_RejectsCSRFMismatch verifies that the
|
|
// /register/confirm POST handler refuses to finalise a pending
|
|
// registration when the form CSRF token does not match the cookie.
|
|
func TestRegisterConfirmHandler_RejectsCSRFMismatch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := createTestApp(t)
|
|
provider := &AuthProviderOIDC{h: app}
|
|
|
|
// Mint a pending registration with a stashed pending-confirmation,
|
|
// as the OIDC callback would have done after resolving the user
|
|
// identity but before the user clicked the interstitial form.
|
|
authID := types.MustAuthID()
|
|
regReq := types.NewRegisterAuthRequest(&types.RegistrationData{
|
|
Hostname: "phish-target",
|
|
})
|
|
regReq.SetPendingConfirmation(&types.PendingRegistrationConfirmation{
|
|
UserID: 1,
|
|
CSRF: "expected-csrf",
|
|
})
|
|
app.state.SetAuthCacheEntry(authID, regReq)
|
|
|
|
rec := httptest.NewRecorder()
|
|
provider.RegisterConfirmHandler(rec,
|
|
newConfirmRequest(t, authID, "wrong-csrf", "expected-csrf"),
|
|
)
|
|
|
|
assert.Equal(t, http.StatusForbidden, rec.Code,
|
|
"CSRF cookie/form mismatch must be rejected with 403")
|
|
|
|
// And the registration must still be pending — the rejected POST
|
|
// must not have called handleRegistration.
|
|
cached, ok := app.state.GetAuthCacheEntry(authID)
|
|
require.True(t, ok, "rejected POST must not evict the cached registration")
|
|
require.NotNil(t, cached.PendingConfirmation(),
|
|
"rejected POST must not clear the pending confirmation")
|
|
}
|
|
|
|
// TestRegisterConfirmHandler_RejectsWithoutPending verifies that
|
|
// /register/confirm refuses to finalise a registration that did not
|
|
// first complete the OIDC interstitial. Without this check an attacker
|
|
// who knew an auth_id could POST directly to the confirm endpoint and
|
|
// claim the device.
|
|
func TestRegisterConfirmHandler_RejectsWithoutPending(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
app := createTestApp(t)
|
|
provider := &AuthProviderOIDC{h: app}
|
|
|
|
authID := types.MustAuthID()
|
|
// Cached registration with NO pending confirmation set — i.e. the
|
|
// OIDC callback has not run yet.
|
|
app.state.SetAuthCacheEntry(authID, types.NewRegisterAuthRequest(
|
|
&types.RegistrationData{Hostname: "no-oidc-yet"},
|
|
))
|
|
|
|
rec := httptest.NewRecorder()
|
|
provider.RegisterConfirmHandler(rec,
|
|
newConfirmRequest(t, authID, "fake", "fake"),
|
|
)
|
|
|
|
assert.Equal(t, http.StatusForbidden, rec.Code,
|
|
"confirm without prior OIDC pending state must be rejected with 403")
|
|
}
|