feat: add --use-device-code flag to auth login#257
Conversation
Add device code authentication flow for environments where browser access is not available. Users can now authenticate by entering a code at https://microsoft.com/devicelogin from any device with a browser. Usage: fab auth login --use-device-code fab auth login --use-device-code --tenant <tenant_id> The option is also available in the interactive authentication menu as "Device code". Implementation: - Add --use-device-code argument to the auth login parser - Add _acquire_token_by_device_code() using MSAL's initiate_device_flow and acquire_token_by_device_flow APIs - Add device_code_flow_failed() error message - Wrap device code flow in try/finally to ensure flag cleanup on failure - Use silent refresh for subsequent scopes after initial device code auth Assisted-by: GitHub Copilot:claude-opus-4.6
7aa9a30 to
fb068dc
Compare
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because no GitHub Actions runner was available. Make sure your repository has a runner available to run Copilot's review, or add a copilot-setup-steps.yml file specifying one with the runs-on attribute. See the docs for more details.
Adds a device-code authentication option to fab auth login to support non-browser environments (SSH/containers/CI) and updates core auth logic to better handle MSAL silent acquisition errors.
Changes:
- Adds
--use-device-codeflag and “Device code” interactive option to the auth login command. - Implements device-code token acquisition in
FabAuthand routes user auth through it when enabled. - Adds/updates core + command-level tests and a changelog entry.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/fabric_cli/parsers/fab_auth_parser.py |
Adds CLI flag and updates examples for device-code login. |
src/fabric_cli/commands/auth/fab_auth.py |
Wires new flag + interactive selection into the login command flow. |
src/fabric_cli/core/fab_auth.py |
Adds device-code flow implementation + dispatch logic in token acquisition. |
src/fabric_cli/errors/auth.py |
Adds a dedicated error message for device-code initiation failures. |
tests/test_core/test_fab_auth.py |
Adds extensive unit tests for the device-code flow and error cases. |
tests/test_commands/test_auth.py |
Adds command-level tests for flag behavior, precedence, and cleanup. |
.changes/unreleased/added-20260630-223439.yaml |
Changelog entry documenting the new flag. |
| if args.use_device_code: | ||
| FabAuth().set_access_mode("user", args.tenant) | ||
| with FabAuth().device_code_flow(): | ||
| FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT) | ||
| FabAuth().get_access_token(scope=fab_constant.SCOPE_ONELAKE_DEFAULT) | ||
| FabAuth().get_access_token(scope=fab_constant.SCOPE_AZURE_DEFAULT) | ||
| Context().context = FabAuth().get_tenant() |
There was a problem hiding this comment.
Fixed — using a single auth = FabAuth() instance now. Note: FabAuth is a singleton, so this was functionally correct before, but a local variable makes the intent explicit.
| elif selected_auth == "Device code": | ||
| FabAuth().set_access_mode("user", args.tenant) | ||
| with FabAuth().device_code_flow(): | ||
| FabAuth().get_access_token(scope=fab_constant.SCOPE_FABRIC_DEFAULT) | ||
| FabAuth().get_access_token(scope=fab_constant.SCOPE_ONELAKE_DEFAULT) | ||
| FabAuth().get_access_token(scope=fab_constant.SCOPE_AZURE_DEFAULT) | ||
| Context().context = FabAuth().get_tenant() |
| prompt="select_account", | ||
| parent_window_handle=msal.PublicClientApplication.CONSOLE_WINDOW_HANDLE, | ||
| ) | ||
| if token is not None and "id_token_claims" in token: | ||
| self.set_tenant(token.get("id_token_claims")["tid"]) | ||
|
|
||
| if token and token.get("error"): |
There was a problem hiding this comment.
Fixed — using .get("tid") with a truthy guard before calling set_tenant().
| def _acquire_token_by_device_code(self, scope: list[str]) -> Optional[dict]: | ||
| """ | ||
| Acquire a token using the device code flow. |
There was a problem hiding this comment.
Done — renamed to scopes to align with MSAL's parameter naming.
| Returns: | ||
| Full MSAL result dictionary containing access_token, expires_on, etc., | ||
| or None if the device flow returns no result. Raises: | ||
| FabricCLIError: When device code flow initiation or token acquisition fails |
There was a problem hiding this comment.
Fixed — added a blank line before Raises: section.
| accounts = self._get_app().get_accounts() | ||
| if accounts: | ||
| token = self._get_app().acquire_token_silent( | ||
| scopes=scope, account=accounts[0], force_refresh=True | ||
| ) |
There was a problem hiding this comment.
Done — stored as app = self._get_app() and reused throughout the method.
| if token and token.get("access_token"): | ||
| return token | ||
|
|
||
| flow = self._get_app().initiate_device_flow(scopes=scope) |
| utils_ui.print_grey(flow["message"]) | ||
|
|
||
| # Wait for the user to authenticate | ||
| token = self._get_app().acquire_token_by_device_flow(flow) |
| Usage: | ||
| with FabAuth().device_code_flow(): | ||
| FabAuth().get_access_token(scope=...) |
There was a problem hiding this comment.
Fixed — docstring now shows auth = FabAuth(); with auth.device_code_flow(): ...
| Returns: | ||
| Full MSAL result dictionary containing access_token, expires_on, etc., | ||
| or None if the device flow returns no result. Raises: | ||
| FabricCLIError: When device code flow initiation or token acquisition fails | ||
| """ |
There was a problem hiding this comment.
Same fix as above — already addressed
- Use single FabAuth instance in device code login paths for clarity - Guard id_token_claims['tid'] with .get() to avoid KeyError - Rename scope -> scopes in _acquire_token_by_device_code for MSAL alignment - Fix docstring formatting (Raises: on its own line) - Store self._get_app() in local variable to reduce repeated calls - Fix usage example in device_code_flow() docstring to show single instance Assisted-by: GitHub Copilot:claude-opus-4.6
|
Hi @iemejia, Thank you for the contribution. Please note that authentication is an area with restricted contributions, as outlined in our contributing guide: https://github.com/microsoft/fabric-cli/blob/main/CONTRIBUTING.md#areas-with-restricted-contributions Additionally, external contributions should follow CONTRIBUTING.md and address issues labelled For these reasons, we're closing this PR. Thank you again for your interest, and we hope to see more from you on eligible issues. |
|
Thanks @ayeshurun, understood -- I should have followed the process and linked this to an issue first. My apologies for the shortcut. I've commented on the existing issue #215 with expanded use cases (headless servers, SSH sessions, containers, CI runners) and offered to resubmit once the team confirms the approach and labels it accordingly. Happy to rework the PR whenever the team is ready to accept contributions on this. |
Description
Add device code authentication flow for
fab auth login, similar toaz login --use-device-codein Azure CLI. This allows users to authenticate from environments where browser access is not available (SSH sessions, containers, remote servers, CI runners).Usage
The option is also available in the interactive authentication menu as "Device code".
Prerequisite: App registration change
The Fabric CLI app registration (5814bfb4-2705-4994-b8d6-39aabeb5eaeb) is configured as a confidential client. Device code flow requires "Allow public client flows" to be enabled:
Implementation
Tests