Skip to content

[Bug] OAuth2 authentication failed #184

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
2 tasks done
BewareMyPower opened this issue Feb 1, 2023 · 5 comments · Fixed by #190
Closed
2 tasks done

[Bug] OAuth2 authentication failed #184

BewareMyPower opened this issue Feb 1, 2023 · 5 comments · Fixed by #190

Comments

@BewareMyPower
Copy link
Contributor

BewareMyPower commented Feb 1, 2023

Search before asking

  • I searched in the issues and found nothing similar.

Version

  • Broker: Pulsar 2.9.3.16 (StreamNative's release)
  • Client: Pulsar C++ Client 3.1.1
  • OS: Windows 11 (Visual Studio 2019) and Ubuntu 20.04 WSL (GCC 9)

Minimal reproduce step

Set up an account on StreamNative cloud, create the topic my-topic in advance and grant the produce permission. The following example uses test-auth instance under the sndev organization, replace YOUR-KEY-FILE-PATH to the path of the credential file.

#include <pulsar/Client.h>
using namespace pulsar;

int main(int argc, char *argv[]) {
  std::string params = R"({
    "issuer_url": "https://auth.streamnative.cloud/",
    "private_key": "YOUR-KEY-FILE-PATH",
    "audience": "urn:sn:pulsar:sndev:test-auth"})";
  ClientConfiguration config;
  config.setAuth(AuthOauth2::create(params));
  Client client("pulsar+ssl://test-auth.sndev.snio.cloud:6651", config);

  Producer producer;
  auto result =
      client.createProducer("persistent://public/default/my-topic", producer);
  if (result != ResultOk) {
    std::cerr << "Failed to create producer: " << result << std::endl;
    return 1;
  }

  MessageId id;
  result = producer.send(MessageBuilder().setContent("hello").build(), id);
  if (result == ResultOk) {
    std::cout << "Sent to " << id << std::endl;
  } else {
    std::cerr << "Failed to send: " << result << std::endl;
  }

  client.close();
  return 0;
}

What did you expect to see?

It should succeed

What did you see instead?

Ubuntu:

2023-02-01 22:58:13.267 DEBUG [139889604585216] ClientConnection:591 | [<none> -> pulsar+ssl://test-auth.sndev.snio.cloud:6651] Resolved hostname test-auth.sndev.snio.cloud to SECRET_IP:6651
2023-02-01 22:58:14.564 INFO  [139889604585216] ClientConnection:387 | [MY_LOCAL_IP:38550 -> SECRET_IP:6651] Connected to broker
2023-02-01 22:58:15.378 ERROR [139889604585216] ClientConnection:497 | [MY_LOCAL_IP:38550 -> SECRET_IP:6651] Failed to establish connection: AuthenticationError
2023-02-01 22:58:15.378 ERROR [139889604585216] ClientConnection:1646 | [MY_LOCAL_IP:38550 -> SECRET_IP:6651] Connection closed with AuthenticationError

Windows:


2023-02-01 23:05:55.508 ERROR [11216] D:\a\pulsar-client-cpp\pulsar-client-cpp\lib\ClientConnection:497 | [MY_LOCAL_IP:6224 -> SECRET_IP:6651] Handshake failed: unregistered scheme (STORE routines)
2023-02-01 23:05:55.508 INFO  [11216] D:\a\pulsar-client-cpp\pulsar-client-cpp\lib\ClientConnection:1646 | [MY_LOCAL_IP:6224 -> SECRET_IP:6651] Connection closed with ConnectError

Anything else?

No response

Are you willing to submit a PR?

  • I'm willing to submit a PR!
@BewareMyPower
Copy link
Contributor Author

It seems to be caused by the packaging style. It's very similar to this issue: apache/pulsar-client-node#281

I did some experiements with the Python client. Given the following similar Python script.

#!/bin/env python3
import pulsar

pulsar_url = "pulsar+ssl://test-auth.sndev.snio.cloud:6651"
pulsar_oauth_params = '''{
    "issuer_url": "https://auth.streamnative.cloud/",
    "private_key": "YOUR-KEY-FILE-PATH",
    "audience": "urn:sn:pulsar:sndev:test-auth"
}'''

pulsar_topic = "persistent://public/default/my-topic"

if __name__ == "__main__":
    client = pulsar.Client(
        pulsar_url,
        authentication=pulsar.AuthenticationOauth2(pulsar_oauth_params),
    )
    producer = client.create_producer(topic=pulsar_topic)
    producer.send(f"hello".encode('utf-8'))
    producer.close()
    client.close()

I tried two ways to install the Python wheel (on Ubuntu 20.04).

1. [OK] Build from source.

git clone [email protected]:apache/pulsar-client-python.git -b v3.1.0-candidate-1
git submodule update --init
cmake -B build
cmake --build build
cp build/lib_pulsar.so .
python3 ./setup.py bdist_wheel
sudo pip3 install dist/pulsar_client-3.1.0a1-cp38-cp38-linux_x86_64.whl --force-reinstall

The output:

2023-02-01 23:32:32.731 INFO  [139771614631680] ClientConnection:189 | [<none> -> pulsar+ssl://test-auth.sndev.snio.cloud:6651] Create ClientConnection, timeout=10000
2023-02-01 23:32:32.739 INFO  [139771614631680] ConnectionPool:97 | Created connection for pulsar://test-auth-broker-0.test-auth-broker-headless.sndev.svc.cluster.local:6650
2023-02-01 23:32:33.027 INFO  [139771614631680] ClientConnection:388 | [MY_LOCAL_IP:42426 -> REMOTE_IP:6651] Connected to broker through proxy. Logical broker: pulsar://test-auth-broker-0.test-auth-broker-headless.sndev.svc.cluster.local:6650

We can see the handshake succeeded and the client connected to the proxy

2. [FAILED] Install the existing wheel

curl -O -L https://dist.apache.org/repos/dist/dev/pulsar/pulsar-client-python-3.1.0-candidate-1/linux-glibc-x86_64/pulsar_client-3.1.0a1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
sudo pip3 install dist/pulsar_client-3.1.0a1-cp38-cp38-linux_x86_64.whl --force-reinstall

The output:

2023-02-01 23:34:37.694 ERROR [139763784525632] AuthOauth2:223 | Response failed for getting the well-known configuration https://auth.streamnative.cloud/. Error Code 77: error setting certificate verify locations:  CAfile: /etc/pki/tls/certs/ca-bundle.crt CApath: none
2023-02-01 23:34:37.695 INFO  [139763784525632] ConnectionPool:97 | Created connection for pulsar+ssl://test-auth.sndev.snio.cloud:6651
2023-02-01 23:34:38.058 INFO  [139763747448576] ClientConnection:386 | [MY_LOCAL_IP:48482 -> REMOTE_IP:6651] Connected to broker
2023-02-01 23:34:38.819 ERROR [139763747448576] ClientConnection:496 | [MY_LOCAL_IP:48482 -> REMOTE_IP:6651] Failed to establish connection: AuthenticationError

Conclusion

The cause might be apache/pulsar#16064, which is related to a CVE. We need to document the workround or change the way to build the client library.

/cc @merlimat @shibd @RobertIndie

@BewareMyPower
Copy link
Contributor Author

Updated:

Building the C++ client from source works. I think the main reason is the LINK_STATIC=ON build. I built the C++ client on Ubuntu 20.04 with the dependencies installed from the APT source and it worked well.

Boost_INCLUDE_DIRS: /home/xyz/software/boost-1.81/include
OPENSSL_INCLUDE_DIR: /usr/include
OPENSSL_LIBRARIES: /usr/lib/x86_64-linux-gnu/libssl.so/usr/lib/x86_64-linux-gnu/libcrypto.so
Protobuf_INCLUDE_DIRS: /usr/include
Protobuf_LIBRARIES: /usr/lib/x86_64-linux-gnu/libprotobuf.so-pthread
CURL_INCLUDE_DIRS: /usr/include/x86_64-linux-gnu
CURL_LIBRARIES: /usr/lib/x86_64-linux-gnu/libcurl.so

I think the key point is the libcurl and OpenSSL dependencies. I'm still investigating and testing it.

@BewareMyPower
Copy link
Contributor Author

BewareMyPower commented Feb 7, 2023

The root cause is that the OAuth2 client credential flow does not configure any CA cert for verification.

TL; DR, just see the following workaround section.

Workaround

Just take Ubuntu 20.04 for example, whose default CA file is /etc/ssl/certs/ca-certificates.crt.

C++ client: pass the CA cert path to ClientConfiguration:

ClientConfiguration config;
config.setTlsTrustCertsFilePath("/etc/ssl/certs/ca-certificates.crt")
Client client(serviceUrl, config);

Python and Node.js clients: though they also have the related configs to set the CA cert path, it does not work actually. (See the following section for detailed explanation). You have to copy the CA cert file to the specific path, which is determined by your system.

# All Python wheels for Linux use /etc/pki/tls/certs/ca-bundle.crt as the path of the CA cert
sudo mkdir -p /etc/pki/tls/certs
sudo cp /etc/ssl/certs/ca-certificates.crt /etc/pki/tls/certs/ca-bundle.crt

Analysis

From https://curl.se/docs/sslcerts.html we can see

If the remote server uses a self-signed certificate, if you do not install a CA cert store, if the server uses a certificate signed by a CA that is not included in the store you use or if the remote host is an impostor impersonating your favorite site, and you want to transfer files from this server, do one of the following:

  1. Tell libcurl to not verify the peer. With libcurl you disable this with curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, FALSE);

Before Pulsar C++ client 3.0.0, the OAuth2 client credential flow didn't verify the peer, which leads to the CVE-2022-33684. Then apache/pulsar#16064 fixes it by enabling the option to verify the peer.

However, the libcurl dependency is built from source and linked statically by:

  • Pre-built C++ libraries, e.g. the deb package
  • Python wheels
  • Node.js C++ Add-ons

When it's built from source, the default path of the CA cert is detected automatically and determined by the OS. e.g. the Python wheels for Linux are built on manylinux2014 images, the bundled path is /etc/pki/tls/certs/ca-bundle.crt. You can see the path from any workflow here:

20230207132723

The only workaround for Python and Node.js clients on Linux is copying your CA cert file into that path. The C++ client can work by configuring tlsTrustCertsFilePath because the deb package is compiled on Debian, rpm package is compiled on CentOS, the default path of the CA cert file is the same with the path on Debian-based distros and RedHat-based distros. However, the Python client for Linux is built on manylinux image, which is based on a RedHat-based distro, so the path is different with the Debian-based distros.

How to reproduce

Create an account on StreamNative cloud to get the client id and client secret. You can switch to your own vendor if you have an Pulsar service with OAuth2 authentication enabled.

TlsOauth2Example.cc

#include <pulsar/Client.h>
using namespace pulsar;

int main(int argc, char *argv[]) {
    std::string params = R"({
    "issuer_url": "https://auth.streamnative.cloud/", // St
    "client_id": "<your-client-id>",
    "client_secret": "<your-client-secret>",
    "audience": "<your-audience>"})";
    ClientConfiguration config;
    config.setAuth(AuthOauth2::create(params));
    // NOTE: here we use a customized CA cert path
    config.setTlsTrustCertsFilePath("/app/ca-certificates.crt");
    Client client("pulsar+ssl://<your-host>:<your-port>", config);

    Producer producer;
    auto result = client.createProducer("persistent://public/default/my-topic", producer);
    if (result != ResultOk) {
        std::cerr << "Failed to create producer: " << result << std::endl;
        return 1;
    }

    client.close();
    return 0;
}

Start a docker container:

docker run -v $PWD:/app -it ubuntu:20.04 /bin/bash

Run the following commands inside the container:

cd /app
apt update -y
apt install -y curl g++
curl -O -L https://archive.apache.org/dist/pulsar/pulsar-client-cpp-3.1.1/deb-x86_64/apache-pulsar-client-dev.deb
curl -O -L https://archive.apache.org/dist/pulsar/pulsar-client-cpp-3.1.1/deb-x86_64/apache-pulsar-client.deb
apt install ./apache-pulsar-client*.deb
# NOTE: move the CA file from the default path to /app
mv /etc/ssl/certs/ca-certificates.crt .
g++ TlsOauth2Example.cc -std=c++11 -lpulsar
./a.out

You will see the following output:

2023-02-07 10:05:33.619 ERROR [139656646591168] AuthOauth2:224 | Response failed for getting the well-known configuration https://auth.streamnative.cloud/. Error Code 77: error setting certificate verify locations:  CAfile: /etc/ssl/certs/ca-certificates.crt CApath: none
...
2023-02-07 10:05:37.566 ERROR [139656646563584] ClientImpl:184 | Error Checking/Getting Partition Metadata while creating producer on persistent://public/default/my-topic -- AuthenticationError
Failed to create producer: 2023-02-07 10:05:37.566 INFO  [139656646563584] ClientConnection:267 | [LOCAL -> REMOTE] Destroyed connection
AuthenticationError

If you removed the setTlsTrustCertsFilePath config, you will see a more error line:

2023-02-07 10:07:59.114 ERROR [140482730030848] ClientConnection:486 | [LOCAL -> REMOTE] Handshake failed: certificate verify failed (SSL routines, tls_process_server_certificate)

That's because the for TLS connection, the C++ client reads the CA cert file from the config:

std::string trustCertFilePath = clientConfiguration.getTlsTrustCertsFilePath();
if (!trustCertFilePath.empty()) {
if (file_exists(trustCertFilePath)) {
ctx.load_verify_file(trustCertFilePath);
} else {

Solution

We should use the tlsTrustCertsFilePath for HTTP requests in OAuth2's client credential flow, just like HTTPLookupService does:

if (!tlsTrustCertsFilePath_.empty()) {
curl_easy_setopt(handle, CURLOPT_CAINFO, tlsTrustCertsFilePath_.c_str());
}

I will push a PR soon.

BewareMyPower added a commit to BewareMyPower/pulsar-client-cpp that referenced this issue Feb 7, 2023
…2 flow

Fixes apache#184

### Modifications

Add a `AuthenticationDataProvider` implementation `InitialAuthData`,
which holds the CA cert path. Then, in `AuthOauth2::getAuthData`,
retrieve the path and pass it to the `ClientCredentialFlow` for HTTP
requests performed by libcurl.

This solution is API and ABI compatible.

### Verifications

It's hard to add the test in CI because we need an OAuth2 server
configured with the CA configured.

Follow the **How to reproduce** section in
apache#184 (comment)
to reproduce this issue. Apply this patch and build the `libpulsar.so`
with `LINK_STATIC=ON`, then copy the `libpulsar.so` into the docker
container (under `/app/lib`).

Run `./a.out` directly, you will still see the `AuthenticationError`.
However, if you added the path of `libpulsar.so` to the
`LD_LIBRARY_PATH`:

```bash
export LD_LIBRARY_PATH=/app/lib
./a.out
```

No error will happen. You can also replace the `/lib/libpulsar.so` with
the `libpulsar.so` built from source.
BewareMyPower added a commit to BewareMyPower/pulsar-client-cpp that referenced this issue Feb 7, 2023
…2 flow

Fixes apache#184

### Modifications

Add a `AuthenticationDataProvider` implementation `InitialAuthData`,
which holds the CA cert path. Then, in `AuthOauth2::getAuthData`,
retrieve the path and pass it to the `ClientCredentialFlow` for HTTP
requests performed by libcurl.

This solution is API and ABI compatible.

### Verifications

It's hard to add the test in CI because we need an OAuth2 server
configured with the CA configured.

Follow the **How to reproduce** section in
apache#184 (comment)
to reproduce this issue. Apply this patch and build the `libpulsar.so`
with `LINK_STATIC=ON`, then copy the `libpulsar.so` into the docker
container (under `/app/lib`).

Run `./a.out` directly, you will still see the `AuthenticationError`.
However, if you added the path of `libpulsar.so` to the
`LD_LIBRARY_PATH`:

```bash
export LD_LIBRARY_PATH=/app/lib
./a.out
```

No error will happen. You can also replace the `/lib/libpulsar.so` with
the `libpulsar.so` built from source.

(cherry picked from commit d7eb539)
@shibd
Copy link
Member

shibd commented Feb 7, 2023

Nice catch! Node.js 1.8.0 oauth2 auth failed is the same as this.

When it's built from source, the default path of the CA cert is detected automatically and determined by the OS. e.g. the Python wheels for Linux are built on manylinux2014 images, the bundled path is /etc/pki/tls/certs/ca-bundle.crt. You can see the path from any workflow here:

I checked cross-compile logs on the macOS system, and the cert bundle and path is not set on arm64.

image

I'll use your PR to verify if this can be fixed.

@shibd
Copy link
Member

shibd commented Feb 7, 2023

It can solve this problem by configuring tlsTrustCertsFilePath.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
2 participants