diff --git a/crates/agent/src/daemon.rs b/crates/agent/src/daemon.rs index 766c0a2d..cb5eb91a 100644 --- a/crates/agent/src/daemon.rs +++ b/crates/agent/src/daemon.rs @@ -609,12 +609,15 @@ where let remote_discovery = inventory.clone(); let state_dir = args.state_dir.clone(); let oidc_callback_listen = args.oidc_callback_listen; + // A system-mode daemon is root: its browser would not be the user's. + let open_browser = args.user; let remote_enrollment = enrollment.clone(); tokio::spawn(remote::run( controller, remote_discovery, state_dir, oidc_callback_listen, + open_browser, reconciler, remote_enrollment, controller_status.clone(), diff --git a/crates/agent/src/oidc.rs b/crates/agent/src/oidc.rs index 253593c8..4dbf5211 100644 --- a/crates/agent/src/oidc.rs +++ b/crates/agent/src/oidc.rs @@ -289,6 +289,7 @@ pub async fn enroll( controller: &ControllerConnectionConfig, enrollment: &EnrollmentState, callback_listen: Option, + open_browser: bool, ) -> anyhow::Result { if controller.device_authorization { return enroll_with_device_authorization(controller, enrollment).await; @@ -315,6 +316,7 @@ pub async fn enroll( &redirect_uri, std::mem::take(&mut begin.state), callback_listen, + open_browser, ) .await?; enrollment.set("enrolling").await; @@ -631,6 +633,7 @@ pub(crate) async fn wait_for_authorization_code( redirect_uri: &Url, expected_state: String, callback_listen: Option, + open_browser: bool, ) -> anyhow::Result { wait_for_authorization_code_with_page( authorization_url, @@ -641,7 +644,7 @@ pub(crate) async fn wait_for_authorization_code( subscription_available: false, github_available: false, }, - true, + open_browser, ) .await? .context("required authorization was skipped") diff --git a/crates/agent/src/remote.rs b/crates/agent/src/remote.rs index 23dfe192..3e00b4a7 100644 --- a/crates/agent/src/remote.rs +++ b/crates/agent/src/remote.rs @@ -184,6 +184,7 @@ pub async fn run( mut discovered: watch::Receiver>, state_dir: PathBuf, oidc_callback_listen: Option, + open_browser: bool, reconciler: Reconciler, enrollment: EnrollmentState, controller_status: ControllerConnectionState, @@ -207,6 +208,7 @@ pub async fn run( &mut discovered, &state_dir, oidc_callback_listen, + open_browser, &reconciler, &enrollment, &controller_status, @@ -304,6 +306,7 @@ async fn run_session( discovered: &mut watch::Receiver>, state_dir: &Path, oidc_callback_listen: Option, + open_browser: bool, reconciler: &Reconciler, enrollment: &EnrollmentState, controller_status: &ControllerConnectionState, @@ -338,7 +341,7 @@ async fn run_session( enrollment, logout, INITIAL_RETRY_DELAY, - || oidc::enroll(controller, enrollment, oidc_callback_listen), + || oidc::enroll(controller, enrollment, oidc_callback_listen, open_browser), ) .await; identity::save(&identity_path, &identity)?; diff --git a/docs/device-identity.md b/docs/device-identity.md index d1f3587d..5e7920c8 100644 --- a/docs/device-identity.md +++ b/docs/device-identity.md @@ -72,13 +72,13 @@ sequenceDiagram Controller-->>Daemon: enrollment_id, authorization_url, state, redirect_uri, token_endpoint, client_id Daemon->>Daemon: Listen on loopback redirect_uri (default 127.0.0.1:51327) Daemon->>Daemon: Publish authorization_url on GET /v1/enrollment and wait up to 10 min - alt Daemon runs as a system service (normal install) - Note over Daemon: Cannot open a browser. It runs outside any user session. + alt Daemon runs in system mode (normal install) + Note over Daemon: Does not open a browser. It runs as root, outside the user's session. User->>App: Sign in App->>Daemon: GET /v1/enrollment Daemon-->>App: authorization_url App->>Browser: Open authorization_url - else Daemon runs in the user's session (sudo, user mode, development) + else Daemon runs in user mode (--user, development) Daemon->>Browser: Open the local sign-in page User->>Browser: Continue to sign in end @@ -217,15 +217,16 @@ native app (RFC 8252): (default `http://127.0.0.1:51327/callback`) and publishes the authorization URL on `/v1/enrollment`. How the browser gets there depends on how the daemon runs: - - **As a system service** (the normal install: LaunchDaemon, Windows - service, systemd), the daemon runs outside any user session and cannot - open a browser. Its attempt to do so fails and is only logged. The user - clicks sign-in in the desktop app, which reads the URL from - `/v1/enrollment` and opens it in the user's browser. The browser goes - straight to the IdP. - - **In the user's session** (`sudo` from a terminal, `--user` mode, - development), the daemon opens the browser on its own landing page, and - **Continue to sign in** redirects to the IdP. + - **In system mode** (the normal install: LaunchDaemon, Windows service, + systemd; also `sudo agentdesktop daemon` from a terminal), the daemon + runs as root and does not open a browser. Such an attempt would not + simply fail: on macOS, `open` run by root starts Safari on the daemon's + landing page instead of the user's browser. The user clicks sign-in in + the desktop app, which reads the URL from `/v1/enrollment` and opens it + in the user's browser. The browser goes straight to the IdP. + - **In user mode** (`--user`, development), the daemon opens the browser + on its own landing page, and **Continue to sign in** redirects to the + IdP. `agentdesktop enrollment` prints the same URL. 5. After the user authenticates, the IdP redirects to the loopback callback.