VEXXHOST Logo

Bringing Browser-Based MFA SSO to the OpenStack CLI

Mohammed Naser
Mohammed NaserAuthor

Learn how a lightweight keystoneauth1 plugin brings your existing browser-based MFA and SSO to the OpenStack CLI, with no changes to any client tools.

You run an OpenStack cloud with federated identity. Your users sign in through Horizon with OpenID Connect or SAML, complete their MFA challenge in the browser, and land on a dashboard. The web experience works well.

The command line does not. Keystone's standard auth plugins expect a username and password passed directly. This breaks the moment your identity provider requires a browser redirect or a second-factor prompt. The common workaround is application-specific passwords: static credentials created outside the identity provider's normal auth flow. They bypass MFA entirely. They rarely get rotated. They create the exact kind of long-lived secret federated identity was meant to eliminate.

We built keystoneauth-websso to solve this. The plugin lets any OpenStack CLI tool use the same browser-based WebSSO flow Horizon uses, directly from your terminal.

How Keystone WebSSO works today

Keystone's WebSSO flow was built for Horizon. Here is what happens:

  1. Horizon redirects your browser to a Keystone endpoint protected by mod_auth_openidc.
  2. Apache handles the OpenID Connect negotiation.
  3. You authenticate at your identity provider.
  4. mod_auth_openidc establishes a session.
  5. Keystone renders a small HTML page (the "callback template") containing a hidden form with an unscoped token.
  6. The form auto-submits back to Horizon's callback URL via HTTP POST.
  7. Horizon picks up the token.

Every step assumes a browser. The identity provider redirect, the MFA challenge, the cookie-based session, the auto-submitted form. None of this maps to a curl-style request/response exchange. A CLI tool driving this flow with raw HTTP calls would need a full browser engine, or at least a JavaScript runtime to render the identity provider's login page. This approach is not practical.

The architecture: a localhost callback server

Our approach is minimal. Instead of replicating a browser, we use the actual browser. The plugin opens your default browser to start the WebSSO flow and spins up a short-lived HTTP server on localhost to receive the token when the flow completes.

The full sequence:

  1. You run an OpenStack CLI command, for example openstack server list, with auth_type set to v3websso.
  2. The plugin constructs the federated WebSSO URL for your configured identity provider and protocol. The URL includes ?origin=http://localhost:9990/auth/websso/ as a query parameter. This tells Keystone where to POST the token after authentication succeeds.
  3. Before opening the browser, the plugin binds a single-request HTTP server to localhost:9990. This server uses Python's built-in http.server module. No external dependencies. No framework. A 60-second socket timeout prevents the server from hanging if you abandon the flow.
  4. The plugin opens the constructed URL in your default browser via Python's webbrowser module.
  5. You authenticate normally in the browser. MFA, hardware tokens, conditional access policies. All of these work because authentication happens where those flows were designed to run.
  6. After authentication, mod_auth_openidc hands control back to Keystone. Keystone renders the callback template containing a form with the unscoped token. Because the origin points to localhost:9990, the form auto-submits to the plugin's waiting HTTP server.
  7. The HTTP server's POST handler parses the multipart form body, extracts the token field, stores the value, sends back a minimal HTML page telling you to close the tab, and shuts down.
  8. The plugin now holds a valid unscoped Keystone token. The plugin retrieves token metadata (expiration, catalog, roles) via a standard GET /v3/auth/tokens call, then proceeds with the command you originally ran.

From your perspective: the terminal pauses, a browser tab opens, you authenticate, the tab says "close this window," and the terminal prints the result.

Plugging into keystoneauth1

This works without changes to python-openstackclient or any other OpenStack client. The keystoneauth1 library has a plugin loading system built on stevedore and setuptools entry points. The plugin registers itself under the keystoneauth1.plugin entry point group as v3websso. When you set auth_type: v3websso in your clouds.yaml or pass --os-auth-type v3websso on the command line, keystoneauth1 discovers and loads the plugin automatically. No client-side patches. No vendor forks. No monkey-patching.

The plugin subclasses keystoneauth1's FederationBaseAuth, which handles project/domain scoping, token rescoping, and session management. The plugin only needs to implement one method: get_unscoped_auth_ref. This method contains the browser-and-localhost-server flow described above. Catalog lookups, endpoint discovery, and service clients all work unchanged downstream.

Token caching

Opening a browser tab every time you run openstack server list would be painful. The plugin caches tokens locally to avoid this.

After a successful authentication, the plugin writes the unscoped token and its metadata to a JSON file in your platform's user cache directory (resolved via platformdirs). The filename is derived from the auth_url and identity_provider, preventing collisions between different clouds or identity providers.

Before starting the browser flow, the plugin checks for a cached token and validates the expiration timestamp. If a valid cached token exists, the plugin uses the token directly and the browser never opens. The cache file is written with 0600 permissions so other users on the system cannot read the token.

The browser flow happens once per token lifetime, typically a few hours depending on your Keystone configuration. Subsequent CLI calls are instant.

Security considerations

The callback server only binds to localhost. No network exposure. The server accepts one request and shuts down immediately.

The socket has a 60-second timeout. If no callback arrives, the server closes and the plugin raises an error instead of blocking your terminal.

The cache file is created with mode 0600, readable only by the owning user. The cache directory follows platform conventions (~/.cache on Linux, ~/Library/Caches on macOS) via platformdirs.

The plugin never sees or stores your identity provider password. Authentication happens entirely in the browser, in the identity provider's domain. The only artifact the plugin captures is the Keystone token, the same token Horizon would receive.

What this means for your operations

If you have invested in federated identity, this plugin removes the last gap in your SSO story. Your users do not need application passwords. They do not need MFA exceptions for CLI workflows. They authenticate the same way whether they use Horizon or the terminal. The same access policies, session controls, and audit logs apply.

The implementation is roughly 300 lines of Python with two runtime dependencies beyond keystoneauth1multipart for parsing the POST body, and platformdirs for resolving cache paths. You need one Keystone config change: add http://localhost:9990/auth/websso/ to the trusted_dashboard list in keystone.conf. No changes to any CLI client.

The project is open source under the Apache 2.0 license at github.com/vexxhost/keystoneauth-websso.

Virtual machines, Kubernetes & Bare Metal Infrastructure

Choose from Atmosphere Cloud, Hosted, or On-Premise.
Simplify your cloud operations with our intuitive dashboard.
Run it yourself, tap our expert support, or opt for full remote operations.
Leverage Terraform, Ansible or APIs directly powered by OpenStack & Kubernetes

Bringing Browser-Based MFA SSO to the OpenStack CLI