fleet/server/sso/session_store_test.go
Scott Gress d4271986e0
End-user authentication for Window/Linux setup experience: backend (#34835)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34529 

# Details

This PR implements the backend (and some related front-end screens) for
allowing Fleet admins to require that users authenticate with an IdP
prior to having their devices set up. I'll comment on changes inline but
the high-level for the device enrollment flow is:

1. The handler for the `/orbit/enroll` endpoint now checks whether the
end-user authentication is required for the team (or globally, if using
the global enroll secret).
2. If so, it checks whether a `host_mdm_idp_accounts` row exists with a
`host_uuid` matching the identifier sent with the request
3. If a row exists, enroll. If not, return back a new flavor of
`OrbitError` with a `401` status code and a message
(`END_USER_AUTH_REQUIRED`) that Orbit can interpret and act accordingly.

Additionally some changes were made to the MDM SSO flow. Namely, adding
more data to the session we store for correlating requests we make to
the IdP to initiate SSO to responses aimed at our callback. We now store
a `RequestData` struct which contains the UUID of the device making the
request, as well as the "initiator" (in this case, "setup_experience").
When our SSO callback detects that the initiator was the setup
experience, it attempts to add all of the relevant records to our
database to associate the host with an IdP account. This removes the
enrollment gate in the `/orbit/enroll` endpoint.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
Will put the changelog in the last ticket for the story

- [X] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## Testing

- [X] Added/updated automated tests
will see if there's any more to update

- [X] QA'd all new/changed functionality manually
To test w/ SimpleSAML

1. Log in to your local Fleet dashboard with MDM and IdP set up for
SimpleSAML
   1. Go to Settings -> Integrations -> Identity provider
   2. Use "SimpleSAML" for the provider name
   3. Use `mdm.test.com` for the entity ID
4. Use `http://127.0.0.1:9080/simplesaml/saml2/idp/metadata.php` for the
metadata URL
1. Set up a team (or "no team") to have End User Authentication required
(Controls -> Setup experience)
1. Get the enroll secret of that team
1. In the browser console, do:
```
fetch("https://localhost:8080/api/fleet/orbit/enroll", {
  "headers": {
    "accept": "application/json, text/plain, */*",
    "cache-control": "no-cache",
    "content-type": "application/json",
    "pragma": "no-cache",
  },
  "body": "{\"enroll_secret\":\"<enroll secret>", \"hardware_uuid\":\"abc123\" }",
  "method": "POST",
});
``` 
replacing `<enroll secret>` with your team's enroll secret.

8. Verify in the network tab that you get a 401 error with message
`END_USER_AUTH_REQUIRED`
1. Go to
https://localhost:8080/mdm/sso?initiator=setup_experience&host_uuid=abc123
1. Verify that a new screen appears asking you to log in to your IdP
1. Log in to SimpleSAML with `sso_user / user123#`
1. Verify that you're taken to a success screen
1. In your database, verify that records exist in the `mdm_idp_accounts`
and `host_mdm_idp_accounts` tables with uuid `abc123`
1. Try the `fetch` command in the browser console again, verify that it
succeeds.

## fleetd/orbit/Fleet Desktop

- [ ] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
This is _not_ compatible with the current version of fleetd or the
soon-to-be-released 1.49.x. Until #34847 changes are released in fleetd,
this will need to be put behind a feature flag or withheld from Fleet
releases.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
* Added support for device UUID linkage during MDM enrollment to enable
host-initiated enrollment tracking
* Introduced setup experience flow for device authentication during
enrollment
* Added end-user authentication requirement configuration for macOS MDM
enrollment

* **Improvements**
* Enhanced MDM enrollment process to maintain device context through
authentication
* Updated authentication UI to display completion status for device
setup flows
  * Refined form layout styling for improved visual consistency

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2025-10-31 11:16:42 -05:00

63 lines
1.9 KiB
Go

package sso
import (
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/datastore/redis/redistest"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSessionStore(t *testing.T) {
runTest := func(t *testing.T, pool fleet.RedisPool) {
store := NewSessionStore(pool)
// Create session that lives for 1 second.
err := store.create("sessionID123", "requestID123", "https://originalurl.com", "some metadata", 1, SSORequestData{HostUUID: "host-uuid-123"})
require.NoError(t, err)
sess, err := store.get("sessionID123")
require.NoError(t, err)
require.NotNil(t, sess)
assert.Equal(t, "requestID123", sess.RequestID)
assert.Equal(t, "https://originalurl.com", sess.OriginalURL)
assert.Equal(t, "some metadata", sess.Metadata)
assert.Equal(t, "host-uuid-123", sess.RequestData.HostUUID)
// Wait a little bit more than one second, session should no longer be present.
time.Sleep(1100 * time.Millisecond)
sess, err = store.get("sessionID123")
var authRequiredError *fleet.AuthRequiredError
assert.ErrorAs(t, err, &authRequiredError)
assert.Nil(t, sess)
// Create another session for 1 second
err = store.create("sessionID456", "requestID456", "https://originalurl.com", "some metadata", 1, SSORequestData{})
require.NoError(t, err)
// Forcefully expire it
err = store.expire("sessionID456")
require.NoError(t, err)
// It is not present anymore
sess, err = store.get("sessionID456")
assert.ErrorAs(t, err, &authRequiredError)
assert.Nil(t, sess)
// Expire a session that does not exist is fine
err = store.expire("sessionIDNOSUCH")
require.NoError(t, err)
}
t.Run("standalone", func(t *testing.T) {
p := redistest.SetupRedis(t, "request", false, false, false)
runTest(t, p)
})
t.Run("cluster", func(t *testing.T) {
p := redistest.SetupRedis(t, "request", true, false, false)
runTest(t, p)
})
}