def _get_oidc_provider(self, provider_id: Union[str, None] = None) -> Tuple[str, OidcProviderInfo]: """ Get OpenID Connect discovery URL for given provider_id :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). Can be None if there is just one provider. :return: updated provider_id and provider info object """ if self._api_version.at_least("1.0.0"): oidc_info = self.get("/credentials/oidc", expected_status=200).json() providers = {p["id"]: p for p in oidc_info["providers"]} _log.info("Found OIDC providers: {p}".format(p=list(providers.keys()))) if provider_id: if provider_id not in providers: raise OpenEoClientException("Requested provider {r!r} not available. Should be one of {p}.".format( r=provider_id, p=list(providers.keys())) ) provider = providers[provider_id] elif len(providers) == 1: # No provider id given, but there is only one anyway: we can handle that. provider_id, provider = providers.popitem() else: raise OpenEoClientException("No provider_id given. Available: {p!r}.".format( p=list(providers.keys())) ) provider = OidcProviderInfo(issuer=provider["issuer"], scopes=provider.get("scopes")) else: # Per spec: '/credentials/oidc' will redirect to OpenID Connect discovery document provider = OidcProviderInfo(discovery_url=self.build_url('/credentials/oidc')) return provider_id, provider
def test_provider_info_default_client_none(requests_mock): requests_mock.get("https://authit.test/.well-known/openid-configuration", json={}) info = OidcProviderInfo(issuer="https://authit.test") assert info.get_default_client_id(grant_types=[]) is None assert info.get_default_client_id( grant_types=[DefaultOidcClientGrant.DEVICE_CODE_PKCE]) is None
def test_provider_info_get_scopes_string_refresh_token_offline_access( requests_mock, scopes_supported, expected): requests_mock.get("https://authit.test/.well-known/openid-configuration", json={"scopes_supported": scopes_supported}) p = OidcProviderInfo(issuer="https://authit.test") assert p.get_scopes_string() == "openid" assert p.get_scopes_string(request_refresh_token=True) == expected assert p.get_scopes_string() == "openid"
def _get_oidc_provider( self, provider_id: Union[str, None] = None) -> Tuple[str, OidcProviderInfo]: """ Get OpenID Connect discovery URL for given provider_id :param provider_id: id of OIDC provider as specified by backend (/credentials/oidc). Can be None if there is just one provider. :return: updated provider_id and provider info object """ if self._api_version.at_least("1.0.0"): oidc_info = self.get("/credentials/oidc", expected_status=200).json() providers = {p["id"]: p for p in oidc_info["providers"]} _log.info( "Found OIDC providers: {p}".format(p=list(providers.keys()))) if provider_id: if provider_id not in providers: raise OpenEoClientException( "Requested OIDC provider {r!r} not available. Should be one of {p}." .format(r=provider_id, p=list(providers.keys()))) provider = providers[provider_id] elif len(providers) == 1: provider_id, provider = providers.popitem() _log.info( "No OIDC provider given, but only one available: {p!r}. Use that one." .format(p=provider_id)) else: # Check if there is a single provider in the config to use. backend = self._orig_url provider_configs = self._get_auth_config( ).get_oidc_provider_configs(backend=backend) intersection = set(provider_configs.keys()).intersection( providers.keys()) if len(intersection) == 1: provider_id = intersection.pop() provider = providers[provider_id] _log.info( "No OIDC provider id given, but only one in config (backend {b!r}): {p!r}." " Use that one.".format(b=backend, p=provider_id)) else: raise OpenEoClientException( "No OIDC provider id given. Pick one from: {p!r}.". format(p=list(providers.keys()))) provider = OidcProviderInfo.from_dict(provider) else: # Per spec: '/credentials/oidc' will redirect to OpenID Connect discovery document provider = OidcProviderInfo( discovery_url=self.build_url('/credentials/oidc')) return provider_id, provider
def test_provider_info_default_client_available_list(requests_mock): requests_mock.get("https://authit.test/.well-known/openid-configuration", json={}) default_clients = [{ "id": "jak4l0v3-45lsdfe3d", "grant_types": ["urn:ietf:params:oauth:grant-type:device_code+pkce", "refresh_token"] }] info = OidcProviderInfo(issuer="https://authit.test", default_clients=default_clients) # Alias for compactness g = DefaultOidcClientGrant assert info.get_default_client_id(grant_check=[]) == "jak4l0v3-45lsdfe3d" assert info.get_default_client_id( grant_check=[g.DEVICE_CODE_PKCE]) == "jak4l0v3-45lsdfe3d" assert info.get_default_client_id( grant_check=[g.REFRESH_TOKEN]) == "jak4l0v3-45lsdfe3d" assert info.get_default_client_id( grant_check=[g.DEVICE_CODE_PKCE, g.REFRESH_TOKEN ]) == "jak4l0v3-45lsdfe3d" assert info.get_default_client_id(grant_check=[g.IMPLICIT]) is None assert info.get_default_client_id( grant_check=[g.IMPLICIT, g.REFRESH_TOKEN]) is None
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_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_provider_info_default_client_invalid_grants(requests_mock, caplog): requests_mock.get("https://authit.test/.well-known/openid-configuration", json={}) default_clients = [{ "id": "jak4l0v3-45lsdfe3d", "grant_types": ["refresh_token", "nope dis invalid"] }] info = OidcProviderInfo(issuer="https://authit.test", default_clients=default_clients) # Alias for compactness g = DefaultOidcClientGrant with caplog.at_level(logging.WARNING): assert info.get_default_client_id( grant_check=[g.REFRESH_TOKEN]) == "jak4l0v3-45lsdfe3d" assert "Invalid OIDC grant type 'nope dis" in caplog.text
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 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 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 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_provider_info_scopes(requests_mock): requests_mock.get("https://authit.test/.well-known/openid-configuration", json={"scopes_supported": ["openid", "test"]}) assert "openid" == OidcProviderInfo( issuer="https://authit.test").get_scopes_string() assert "openid" == OidcProviderInfo(issuer="https://authit.test", scopes=[]).get_scopes_string() assert "openid test" == OidcProviderInfo(issuer="https://authit.test", scopes=["test" ]).get_scopes_string() assert "openid test" == OidcProviderInfo(issuer="https://authit.test", scopes=["openid", "test" ]).get_scopes_string() assert "openid test" == OidcProviderInfo( issuer="https://authit.test", scopes=("openid", "test")).get_scopes_string() assert "openid test" == OidcProviderInfo(issuer="https://authit.test", scopes={"openid", "test" }).get_scopes_string()
def test_provider_info_issuer_slash(requests_mock): requests_mock.get("https://akkoint.net/.well-known/openid-configuration", json={"scopes_supported": ["openid"]}) p = OidcProviderInfo(issuer="https://akkoint.net/") assert p.discovery_url == "https://akkoint.net/.well-known/openid-configuration"
def test_provider_info_issuer(requests_mock): requests_mock.get("https://authit.test/.well-known/openid-configuration", json={"scopes_supported": ["openid"]}) p = OidcProviderInfo(issuer="https://authit.test") assert p.discovery_url == "https://authit.test/.well-known/openid-configuration" assert p.get_scopes_string() == "openid"
def main_add_oidc(args): """ Add a config entry for OIDC auth """ backend = args.backend provider_id = args.provider_id client_id = args.client_id ask_client_secret = args.ask_client_secret use_default_client = args.use_default_client config = AuthConfig() print("Will add OpenID Connect auth config for backend URL {b!r}".format( b=backend)) print("to config file: {c!r}".format(c=str(config.path))) con = connect(backend) api_version = con.capabilities().api_version_check if api_version < "1.0.0": raise CliToolException( "Backend API version is too low: {v} < 1.0.0".format( v=api_version)) # Find provider ID oidc_info = con.get("/credentials/oidc", expected_status=200).json() providers = OrderedDict((p["id"], OidcProviderInfo.from_dict(p)) for p in oidc_info["providers"]) if not providers: raise CliToolException( "No OpenID Connect providers listed by backend {b!r}.".format( b=backend)) if not provider_id: if len(providers) == 1: provider_id = list(providers.keys())[0] else: provider_id = _interactive_choice( title="Backend {b!r} has multiple OpenID Connect providers.". format(b=backend), options=[(p.id, "{t} (issuer {s})".format(t=p.title, s=p.issuer)) for p in providers.values()]) if provider_id not in providers: raise CliToolException( "Invalid provider ID {p!r}. Should be one of {o}.".format( p=provider_id, o=list(providers.keys()))) provider = providers[provider_id] print("Using provider ID {p!r} (issuer {i!r})".format(p=provider_id, i=provider.issuer)) # Get client_id and client_secret (if necessary) if use_default_client: if not provider.default_clients: show_warning( "No default clients declared for provider {p!r}".format( p=provider_id)) client_id, client_secret = None, None else: if not client_id: if provider.default_clients: client_prompt = "Enter client_id or leave empty to use default client, and press enter: " else: client_prompt = "Enter client_id and press enter: " client_id = builtins.input(client_prompt).strip() or None print("Using client ID {u!r}".format(u=client_id)) if not client_id and not provider.default_clients: show_warning("Given client ID was empty.") if client_id and ask_client_secret: client_secret = getpass( "Enter client_secret or leave empty to not use a secret, and press enter: " ) or None else: client_secret = None config.set_oidc_client_config(backend=backend, provider_id=provider_id, client_id=client_id, client_secret=client_secret, issuer=provider.issuer) print("Saved client information to {p!r}".format(p=str(config.path)))
def test_provider_info_discovery_url(requests_mock): discovery_url = "https://authit.test/.well-known/openid-configuration" requests_mock.get(discovery_url, json={"issuer": "https://authit.test"}) p = OidcProviderInfo(discovery_url=discovery_url) assert p.discovery_url == "https://authit.test/.well-known/openid-configuration" assert p.get_scopes_string() == "openid"
def test_provider_info_default_client_none(requests_mock): requests_mock.get("https://authit.test/.well-known/openid-configuration", json={}) info = OidcProviderInfo(issuer="https://authit.test") assert info.get_default_client_id(grant_check=[]) is None assert info.get_default_client_id(grant_check=lambda grants: True) is None