keystoneauth1.identity.v3.websso module

Keystone WebSSO authentication plugin.

Keystone’s WebSSO endpoint delegates authentication to a browser. This plugin opens the endpoint in the user’s browser, listens on a loopback port for the form POST that Keystone makes once the identity provider has authenticated the user, and turns the token in that POST into an unscoped AccessInfo.

The unscoped token it obtains can be rescoped to any project, domain or system, so an application that holds on to it through get_unscoped_auth_state spares the user a login for each one.

WebSSO is not a standardised protocol. Keystone defined it so that Horizon could authenticate users against an external identity provider, and modelled it on the SAML 2.0 Web Browser SSO Profile: as in that profile’s HTTP POST binding, the identity service returns an auto-submitting HTML form which posts the credential to a pre-registered, trusted origin. Because Keystone compares that origin against its [federation] trusted_dashboard list verbatim, the callback path and default port used here are the ones given in the Horizon and Keystone federation installation guides rather than values of our choosing.

Warning

The callback is open to login CSRF and cannot be closed to it. While the listener is running, any page open in the user’s browser can submit a form to the callback port and have its own Keystone token accepted, which would leave the user operating as whoever obtained that token.

Nothing in the request distinguishes such a submission from Keystone’s. The Fetch Metadata headers of a scripted cross-origin form submission are identical to those of Keystone’s auto-submitted form, and on the https to http callback that this flow relies on the Fetch standard serializes Origin as null and drops Referer for both.

Binding the callback to the request it belongs to would need a nonce in the origin parameter, and there is nowhere to put one: Keystone requires that parameter to match a trusted_dashboard entry exactly, so it cannot carry per-request data. The window is limited instead: the listener binds to loopback only, runs only while a login is in progress, stops at the first token it accepts, and times out.

class keystoneauth1.identity.v3.websso.WebSSO(auth_url: str, identity_provider: str, protocol: str, *, redirect_host: str = 'localhost', redirect_port: int = 9990, login_timeout: float = 60, username: str | None = None, trust_id: str | None = None, system_scope: str | None = None, domain_id: str | None = None, domain_name: str | None = None, project_id: str | None = None, project_name: str | None = None, project_domain_id: str | None = None, project_domain_name: str | None = None, reauthenticate: bool = True, include_catalog: bool = True)

Bases: FederationBaseAuth

Authenticate using Keystone’s browser based WebSSO flow.

The user is sent to Keystone’s WebSSO endpoint in their browser and authenticates there against the configured identity provider. Keystone then posts the resulting unscoped token back to a listener this plugin runs on a loopback port.

The callback URL, http://<redirect_host>:<redirect_port>/auth/websso/, must appear verbatim in the server’s [federation] trusted_dashboard list or Keystone refuses to complete the flow.

username is optional and plays no part in authenticating: the identity is chosen in the browser. It only labels the unscoped token for caching, so a caller who signs in as more than one SSO account against the same identity provider can tell those accounts’ cached tokens apart. Give each account a distinct value; leave it unset if there is only one.

login_timeout is how many seconds to wait for the user to finish authenticating in the browser before giving up. The default suits an established session, but a first login through an external identity provider, particularly one that prompts for MFA, can take longer; raise it for those.

__abstractmethods__ = frozenset({})
__annotations__ = {'MIN_TOKEN_LIFE_SECONDS': 'int', '_discovery_cache': 'dict[str, discover.Discover]', '_unscoped_auth_ref': 'access.AccessInfoV3 | None', 'auth_ref': 'access.AccessInfo | None', 'auth_url': 'str', 'interactive_unscoped_auth': 'ty.ClassVar[bool]', 'reauthenticate': 'bool'}
__doc__ = "Authenticate using Keystone's browser based WebSSO flow.\n\nThe user is sent to Keystone's WebSSO endpoint in their browser and\nauthenticates there against the configured identity provider. Keystone\nthen posts the resulting unscoped token back to a listener this plugin\nruns on a loopback port.\n\nThe callback URL, ``http://<redirect_host>:<redirect_port>/auth/websso/``,\nmust appear verbatim in the server's ``[federation] trusted_dashboard``\nlist or Keystone refuses to complete the flow.\n\n``username`` is optional and plays no part in authenticating: the identity\nis chosen in the browser. It only labels the unscoped token for caching, so\na caller who signs in as more than one SSO account against the same\nidentity provider can tell those accounts' cached tokens apart. Give each\naccount a distinct value; leave it unset if there is only one.\n\n``login_timeout`` is how many seconds to wait for the user to finish\nauthenticating in the browser before giving up. The default suits an\nestablished session, but a first login through an external identity\nprovider, particularly one that prompts for MFA, can take longer; raise it\nfor those.\n"
__firstlineno__ = 341
__init__(auth_url: str, identity_provider: str, protocol: str, *, redirect_host: str = 'localhost', redirect_port: int = 9990, login_timeout: float = 60, username: str | None = None, trust_id: str | None = None, system_scope: str | None = None, domain_id: str | None = None, domain_name: str | None = None, project_id: str | None = None, project_name: str | None = None, project_domain_id: str | None = None, project_domain_name: str | None = None, reauthenticate: bool = True, include_catalog: bool = True)
__module__ = 'keystoneauth1.identity.v3.websso'
__static_attributes__ = ('login_timeout', 'redirect_host', 'redirect_port', 'redirect_uri', 'username')
_abc_impl = <_abc._abc_data object>
property _base_url: str

The versioned root of the identity service.

_discovery_cache: dict[str, discover.Discover]
_get_auth_token() str

Send the user to their browser and wait for the token.

auth_ref: access.AccessInfo | None
auth_url: str
property federated_token_url: str

URL that starts the WebSSO flow.

get_unscoped_auth_ref(session: Session) AccessInfoV3

Authenticate in a browser and return the unscoped token.

Keystone hands back only the token itself, so it is validated against the identity service to pick up its expiry and catalog.

get_unscoped_cache_id_elements() dict[str, str | None]

Add the caller supplied username to the unscoped token’s identity.

The browser, not this plugin, chooses who logs in, so the username is not used to authenticate and nothing checks it against the token that comes back. It is here only so a caller who authenticates as more than one SSO account against the same identity provider can keep their unscoped tokens apart in a cache; leaving it unset keeps the identifier as it would otherwise be.

interactive_unscoped_auth: ty.ClassVar[bool] = True

Whether authenticating requires the user to do something, such as completing a login in a browser. Where it does, holding on to the credential saves an interaction rather than merely a round trip.

reauthenticate: bool
class keystoneauth1.identity.v3.websso._CallbackApp(keystone_origin: str)

Bases: object

WSGI application that receives Keystone’s WebSSO form POST.

Accepts exactly one well formed POST and records the token from it. The listener is bound to a port that any page in the user’s browser can post to, and nothing in the request proves it came from Keystone, so see the note on login CSRF in the module docstring.

__call__(environ: dict[str, Any], start_response: StartResponse) list[bytes]

Call self as a function.

__dict__ = mappingproxy({'__module__': 'keystoneauth1.identity.v3.websso', '__firstlineno__': 159, '__doc__': "WSGI application that receives Keystone's WebSSO form POST.\n\nAccepts exactly one well formed POST and records the token from it. The\nlistener is bound to a port that any page in the user's browser can post\nto, and nothing in the request proves it came from Keystone, so see the\nnote on login CSRF in the module docstring.\n", '__init__': <function _CallbackApp.__init__>, '__call__': <function _CallbackApp.__call__>, '_handle': <function _CallbackApp._handle>, '_check_request_shape': <function _CallbackApp._check_request_shape>, '__static_attributes__': ('_keystone_origin', 'token'), '__dict__': <attribute '__dict__' of '_CallbackApp' objects>, '__weakref__': <attribute '__weakref__' of '_CallbackApp' objects>, '__annotations__': {'token': 'str | None'}})
__doc__ = "WSGI application that receives Keystone's WebSSO form POST.\n\nAccepts exactly one well formed POST and records the token from it. The\nlistener is bound to a port that any page in the user's browser can post\nto, and nothing in the request proves it came from Keystone, so see the\nnote on login CSRF in the module docstring.\n"
__firstlineno__ = 159
__init__(keystone_origin: str)
__module__ = 'keystoneauth1.identity.v3.websso'
__static_attributes__ = ('_keystone_origin', 'token')
__weakref__

list of weak references to the object

_check_request_shape(environ: dict[str, Any]) tuple[str, str] | None

Reject requests that do not look like Keystone’s callback.

These checks narrow what reaches the token handling below. None of them identify the sender: a page can submit a form to this port and produce the same request shape Keystone does. See the note on login CSRF in the module docstring.

_handle(environ: dict[str, Any]) tuple[str, str]
exception keystoneauth1.identity.v3.websso._MissingTokenError(message: str | None = None)

Bases: AuthPluginException

Keystone never delivered a token to the callback listener.

__annotations__ = {}
__doc__ = 'Keystone never delivered a token to the callback listener.'
__firstlineno__ = 122
__module__ = 'keystoneauth1.identity.v3.websso'
__static_attributes__ = ()
message = 'Could not get a token from the WebSSO callback.'
class keystoneauth1.identity.v3.websso._QuietWSGIRequestHandler(request, client_address, server)

Bases: WSGIRequestHandler

Request handler that keeps its access log off stderr.

__doc__ = 'Request handler that keeps its access log off stderr.'
__firstlineno__ = 280
__module__ = 'keystoneauth1.identity.v3.websso'
__static_attributes__ = ()
log_message(format: str, *args: Any) None

Do not log requests to stderr.

keystoneauth1.identity.v3.websso._assert_loopback(host: str) None

Check that host only resolves to loopback addresses.

The callback receives an unscoped Keystone token in a plain HTTP request, so the listener must not be reachable from another machine.

keystoneauth1.identity.v3.websso._origin(url: str) str

Reduce a URL to its scheme and authority, lowercased.

keystoneauth1.identity.v3.websso._wait_for_token(redirect_host: str, redirect_port: int, keystone_origin: str, start_flow: Callable[[], None], timeout: float = 60) str

Serve the callback endpoint until Keystone posts a token to it.

start_flow is called once the callback port is listening, and is what sends the user into the flow.