예제 #1
0
    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
예제 #2
0
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
예제 #3
0
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"
예제 #4
0
    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
예제 #5
0
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
예제 #6
0
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)
예제 #7
0
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
예제 #8
0
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
예제 #9
0
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
예제 #10
0
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
예제 #11
0
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
예제 #12
0
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()
예제 #13
0
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
예제 #14
0
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()
예제 #15
0
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"
예제 #16
0
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"
예제 #17
0
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)))
예제 #18
0
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"
예제 #19
0
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