def authenticate_OIDC( self, client_id: str, provider_id: str = None, webbrowser_open=None, timeout=120, server_address: Tuple[str, int] = None ) -> 'Connection': """ Authenticates a user to the backend using OpenID Connect. :param client_id: Client id to use for OpenID Connect authentication :param webbrowser_open: optional handler for the initial OAuth authentication request (opens a webbrowser by default) :param timeout: number of seconds after which to abort the authentication procedure :param server_address: optional tuple (hostname, port_number) to serve the OAuth redirect callback on TODO: deprecated? """ # TODO: option to increase log level temporarily? provider_id, provider = self._get_oidc_provider(provider_id) client_info = OidcClientInfo(client_id=client_id, provider=provider) authenticator = OidcAuthCodePkceAuthenticator( client_info=client_info, webbrowser_open=webbrowser_open, timeout=timeout, server_address=server_address, ) return self._authenticate_oidc(authenticator, provider_id=provider_id)
def _get_oidc_provider_and_client_info( self, provider_id: str, client_id: Union[str, None], client_secret: Union[str, None] ) -> Tuple[str, OidcClientInfo]: """ Resolve provider_id and client info (as given or from config) :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). Can be None if there is just one provider. :return: (client_id, client_secret) """ provider_id, provider = self._get_oidc_provider(provider_id) if client_id is None: client_id, client_secret = self._get_auth_config().get_oidc_client_configs( backend=self._orig_url, provider_id=provider_id ) _log.info("Using client_id {c!r} from config (provider {p!r})".format(c=client_id, p=provider_id)) if client_id is None: raise OpenEoClientException("No client ID found.") client_info = OidcClientInfo(client_id=client_id, client_secret=client_secret, provider=provider) return provider_id, client_info
def authenticate_oidc_refresh_token( self, client_id: str, refresh_token: str = None, client_secret: str = None, provider_id: str = None) -> 'Connection': """ OpenId Connect Refresh Token WARNING: this API is in experimental phase """ provider_id, provider = self._get_oidc_provider(provider_id) if refresh_token is None: store = RefreshTokenStore() # TODO: allow client_id/secret to be None and fetch it from config/cache? refresh_token = store.get(issuer=provider.issuer, client_id=client_id) if refresh_token is None: raise OpenEoClientException("No refresh token") client_info = OidcClientInfo(client_id=client_id, provider=provider, client_secret=client_secret) authenticator = OidcRefreshTokenAuthenticator( client_info=client_info, refresh_token=refresh_token) return self._authenticate_oidc(authenticator, provider_id=provider_id)
def test_oidc_resource_owner_password_credentials_flow(requests_mock): client_id = "myclient" client_secret = "$3cr3t" oidc_discovery_url = "http://oidc.test/.well-known/openid-configuration" username, password = "******", "j0hn" oidc_mock = OidcMock( requests_mock=requests_mock, expected_grant_type="password", expected_client_id=client_id, expected_fields={ "username": username, "password": password, "scope": "openid testpwd", "client_secret": client_secret }, oidc_discovery_url=oidc_discovery_url, scopes_supported=["openid", "testpwd"], ) provider = OidcProviderInfo(discovery_url=oidc_discovery_url, scopes=["testpwd"]) authenticator = OidcResourceOwnerPasswordAuthenticator( client_info=OidcClientInfo(client_id=client_id, provider=provider, client_secret=client_secret), username=username, password=password, ) tokens = authenticator.get_tokens() assert oidc_mock.state["access_token"] == tokens.access_token
def test_oidc_device_flow_auto_detect(requests_mock, caplog, mode, use_pkce, client_secret, expected_fields): client_id = "myclient" oidc_discovery_url = "http://oidc.test/.well-known/openid-configuration" oidc_mock = OidcMock( requests_mock=requests_mock, expected_grant_type="urn:ietf:params:oauth:grant-type:device_code", expected_client_id=client_id, oidc_discovery_url=oidc_discovery_url, expected_fields=expected_fields, state={ "device_code_callback_timeline": ["authorization_pending", "slow_down", "great success"] }, scopes_supported=["openid", "df"]) provider = OidcProviderInfo(discovery_url=oidc_discovery_url, scopes=["df"]) display = [] authenticator = OidcDeviceAuthenticator(client_info=OidcClientInfo( client_id=client_id, provider=provider, client_secret=client_secret), display=display.append, use_pkce=use_pkce) with mock.patch.object(openeo.rest.auth.oidc.time, "sleep") as sleep: with caplog.at_level(logging.INFO): tokens = authenticator.get_tokens() assert oidc_mock.state["access_token"] == tokens.access_token assert re.search( r"visit https://auth\.test/dc and enter the user code {c!r}".format( c=oidc_mock.state['user_code']), display[0]) assert display[1] == "Authorized successfully." assert sleep.mock_calls == [mock.call(2), mock.call(2), mock.call(7)] assert re.search( r"Authorization pending\..*Polling too fast, will slow down\..*Authorized successfully\.", caplog.text, flags=re.DOTALL)
def test_oidc_client_info_uess_device_flow_pkce_support(requests_mock): oidc_discovery_url = "http://oidc.test/.well-known/openid-configuration" oidc_mock = OidcMock( requests_mock=requests_mock, oidc_discovery_url=oidc_discovery_url, expected_grant_type=None, ) provider = OidcProviderInfo( discovery_url=oidc_discovery_url, default_clients=[ { "id": "c1", "grant_types": ["authorization_code+pkce"] }, { "id": "c2", "grant_types": ["urn:ietf:params:oauth:grant-type:device_code"] }, { "id": "c3", "grant_types": ["urn:ietf:params:oauth:grant-type:device_code+pkce"] }, { "id": "c4", "grant_types": [ "refresh_token", "urn:ietf:params:oauth:grant-type:device_code+pkce" ] }, ]) for client_id, expected in [("c1", False), ("c2", False), ("c3", True), ("c4", True), ("foo", False)]: client_info = OidcClientInfo(client_id=client_id, provider=provider) assert client_info.guess_device_flow_pkce_support() is expected
def test_oidc_client_credentials_flow(requests_mock): client_id = "myclient" oidc_discovery_url = "http://oidc.test/.well-known/openid-configuration" client_secret = "$3cr3t" oidc_mock = OidcMock(requests_mock=requests_mock, expected_grant_type="client_credentials", expected_client_id=client_id, expected_fields={"client_secret": client_secret}, oidc_discovery_url=oidc_discovery_url) provider = OidcProviderInfo(discovery_url=oidc_discovery_url) authenticator = OidcClientCredentialsAuthenticator( client_info=OidcClientInfo(client_id=client_id, provider=provider, client_secret=client_secret)) tokens = authenticator.get_tokens() assert oidc_mock.state["access_token"] == tokens.access_token
def authenticate_oidc_device(self, client_id: str, client_secret: str, provider_id: str = None, **kwargs) -> 'Connection': """ Authenticate with OAuth Device Authorization grant/flow WARNING: this API is in experimental phase """ provider_id, provider = self._get_oidc_provider(provider_id) client_info = OidcClientInfo(client_id=client_id, provider=provider, client_secret=client_secret) authenticator = OidcDeviceAuthenticator(client_info=client_info, **kwargs) return self._authenticate_oidc(authenticator, provider_id=provider_id)
def _get_oidc_provider_and_client_info( self, provider_id: str, client_id: Union[str, None], client_secret: Union[str, None], default_client_grant_types: Union[None, List[DefaultOidcClientGrant]] = None ) -> Tuple[str, OidcClientInfo]: """ Resolve provider_id and client info (as given or from config) :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). Can be None if there is just one provider. :return: OIDC provider id and client info """ provider_id, provider = self._get_oidc_provider(provider_id) if client_id is None: _log.debug("No client_id: checking config for prefered client_id") client_id, client_secret = self._get_auth_config( ).get_oidc_client_configs(backend=self._orig_url, provider_id=provider_id) if client_id: _log.info("Using client_id {c!r} from config (provider {p!r})". format(c=client_id, p=provider_id)) if client_id is None and default_client_grant_types: # Try "default_client" from backend's provider info. _log.debug( "No client_id given: checking default client in backend's provider info" ) client_id = provider.get_default_client_id( grant_types=default_client_grant_types) if client_id: _log.info( "Using default client_id {c!r} from OIDC provider {p!r} info." .format(c=client_id, p=provider_id)) if client_id is None: raise OpenEoClientException("No client_id found.") client_info = OidcClientInfo(client_id=client_id, client_secret=client_secret, provider=provider) return provider_id, client_info
def authenticate_oidc_client_credentials( self, client_id: str, client_secret: str = None, provider_id: str = None, ) -> 'Connection': """ OpenID Connect Client Credentials flow. WARNING: this API is in experimental phase """ provider_id, provider = self._get_oidc_provider(provider_id) # TODO: load credentials from file/config client_info = OidcClientInfo(client_id=client_id, provider=provider, client_secret=client_secret) authenticator = OidcClientCredentialsAuthenticator( client_info=client_info) return self._authenticate_oidc(authenticator, provider_id=provider_id)
def test_oidc_refresh_token_flow_no_secret(requests_mock, caplog): client_id = "myclient" refresh_token = "r3fr35h.d4.t0k3n.w1lly4" oidc_discovery_url = "http://oidc.test/.well-known/openid-configuration" oidc_mock = OidcMock(requests_mock=requests_mock, expected_grant_type="refresh_token", expected_client_id=client_id, oidc_discovery_url=oidc_discovery_url, expected_fields={ "scope": "openid", "refresh_token": refresh_token }, scopes_supported=["openid"]) provider = OidcProviderInfo(discovery_url=oidc_discovery_url) authenticator = OidcRefreshTokenAuthenticator(client_info=OidcClientInfo( client_id=client_id, provider=provider), refresh_token=refresh_token) tokens = authenticator.get_tokens() assert oidc_mock.state["access_token"] == tokens.access_token assert oidc_mock.state["refresh_token"] == tokens.refresh_token
def test_oidc_auth_code_pkce_flow(requests_mock): requests_mock.get("http://oidc.test/.well-known/openid-configuration", json={"scopes_supported": ["openid"]}) client_id = "myclient" oidc_discovery_url = "http://oidc.test/.well-known/openid-configuration" oidc_mock = OidcMock(requests_mock=requests_mock, expected_grant_type="authorization_code", expected_client_id=client_id, expected_fields={"scope": "openid testpkce"}, oidc_discovery_url=oidc_discovery_url, scopes_supported=["openid", "testpkce"]) provider = OidcProviderInfo(discovery_url=oidc_discovery_url, scopes=["openid", "testpkce"]) authenticator = OidcAuthCodePkceAuthenticator( client_info=OidcClientInfo(client_id=client_id, provider=provider), webbrowser_open=oidc_mock.webbrowser_open) # Do the Oauth/OpenID Connect flow tokens = authenticator.get_tokens() assert oidc_mock.state["access_token"] == tokens.access_token
def authenticate_oidc_resource_owner_password_credentials( self, client_id: str, username: str, password: str, client_secret: str = None, provider_id: str = None) -> 'Connection': """ OpenId Connect Resource Owner Password Credentials WARNING: this API is in experimental phase """ provider_id, provider = self._get_oidc_provider(provider_id) # TODO: load password from file/config client_info = OidcClientInfo(client_id=client_id, provider=provider, client_secret=client_secret) authenticator = OidcResourceOwnerPasswordAuthenticator( client_info=client_info, username=username, password=password) return self._authenticate_oidc(authenticator, provider_id=provider_id)
def test_oidc_refresh_token_invalid_token(requests_mock, caplog): client_id = "myclient" refresh_token = "wr0n9.t0k3n" oidc_discovery_url = "http://oidc.test/.well-known/openid-configuration" oidc_mock = OidcMock(requests_mock=requests_mock, expected_grant_type="refresh_token", expected_client_id=client_id, oidc_discovery_url=oidc_discovery_url, expected_fields={ "scope": "openid", "refresh_token": "c0rr3ct.t0k3n" }, scopes_supported=["openid"]) provider = OidcProviderInfo(discovery_url=oidc_discovery_url) authenticator = OidcRefreshTokenAuthenticator(client_info=OidcClientInfo( client_id=client_id, provider=provider), refresh_token=refresh_token) with pytest.raises( OidcException, match="Failed to retrieve access token.*invalid refresh token"): tokens = authenticator.get_tokens()
def authenticate_oidc_authorization_code( self, client_id: str, client_secret: str = None, provider_id: str = None, timeout: int = None, server_address: Tuple[str, int] = None, webbrowser_open: Callable = None) -> 'Connection': """ OpenID Connect Authorization Code Flow (with PKCE). WARNING: this API is in experimental phase """ provider_id, provider = self._get_oidc_provider(provider_id) # TODO: load client info and settings from config file? client_info = OidcClientInfo(client_id=client_id, client_secret=client_secret, provider=provider) authenticator = OidcAuthCodePkceAuthenticator( client_info=client_info, webbrowser_open=webbrowser_open, timeout=timeout, server_address=server_address) return self._authenticate_oidc(authenticator, provider_id=provider_id)