apache / superset

Apache Superset is a Data Visualization and Data Exploration Platform
https://superset.apache.org/
Apache License 2.0
63.15k stars 13.99k forks source link

Cannot autheticate on API. Get the tokens (JWT and csrf) but all responses are "401" #19525

Open pedrohdemedeiros opened 2 years ago

pedrohdemedeiros commented 2 years ago

I cannot use the API succefully for any request (except to get the secutiry tokens), always getting " Response 401" for any request.

How to reproduce the bug

On the docker container where the superset is running:

>>>
import requests

session = requests.session()

jwt_token = session.post(
    url='http://localhost:8088/api/v1/security/login',
    json={
    "username": "admin",
    "password": "admin",
    "refresh": False,
    "provider": "db"
    }
).json()["access_token"]

csrf_token = session.get(
    url='http://localhost:8088/api/v1/security/csrf_token/',
    headers={
        'Authorization': f'Bearer {jwt_token}',
    }
).json()["result"]

headers = {
    'accept': 'application/json',
    'Authorization': f'Bearer {jwt_token}',
    'X-CSRFToken': csrf_token,
}

#trying to use the "current user" request as a test
response = requests.get('http://localhost:8088/api/v1/me', headers=headers)

session.close()`

response

Expected results

{ "result": { "email": "admin@superset.com", "first_name": "Superset", "id": 1, "is_active": true, "is_anonymous": false, "last_name": "Admin", "username": "admin" } }

Actual results

>>> response
<Response [401]>

Screenshots

Environment

(please complete the following information):

Checklist

MM-Lehmann commented 2 years ago

did you try with 1.4.2?

MarcinZegar commented 2 years ago

Instead of

#trying to use the "current user" request as a test
response = requests.get('http://localhost:8088/api/v1/me', headers=headers)

you should try:

session.headers.update(headers)

response = session.get('http://localhost:8088/api/v1/me') 
pedrohdemedeiros commented 2 years ago

First of all, thank you @MM-Lehmann and @MarcinZegar .

I've tried with 1.4.0 and most of my API problems were solved but one: I still cannot upload a CSV using it. I've tried a lot of different things, learned a lot about requests using python, curl and so on but it seens that it simply doesn't work.

Not a big problem since I've decided to update my database by MySQL directly (witch I had to learn also).

I will keep an eye on the next versions just to test if, someday, it will be possible

rusackas commented 1 year ago

Closing this since most issues were resolved. It's also been a while since this thread was active! Hopefully things are working better in 1.5.x or 2.0.x but if there are still bugs being found, let us know and we can re-open this, no problem!

raghulprashath commented 1 year ago

Instead of

#trying to use the "current user" request as a test
response = requests.get('http://localhost:8088/api/v1/me', headers=headers)

you should try:

session.headers.update(headers)

response = session.get('http://localhost:8088/api/v1/me') 

This didn't work now. The code is accessing g.user from flask_login. The flask_login doesn't load the user when the request is sent from the api. I think there should be a better solution for the new version of code.

sazary commented 1 year ago

I think i'm experiencing the same issue with superset 2.0.1.

this is the script that I run to get jwt token, csrf token, and finally get my user's info:

const base_url = "https://<server_addr>/api/v1/";

async function get_csrf_token(access_token) {
  const res = await fetch(base_url + "security/csrf_token", {
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + access_token,
    },
    method: "GET",
  });

  const complete_res = await res.json();
  return complete_res.result;
}

async function login() {
  const res = await fetch(base_url + "security/login", {
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      username: "user",
      password: "pass",
      provider: "db",
      refresh: true,
    }),
    method: "POST",
  });

  const complete_res = await res.json();
  return complete_res.access_token;
}

async function get_me(access_token, csrf_token) {
  const res = await fetch(base_url + "me", {
    credentials: "same-origin",
    headers: {
      "Content-Type": "application/json",
      Authorization: "Bearer " + access_token,
      "X-CSRFToken": csrf_token,
    },
    method: "GET",
  });

  return await res.json();
}

(async () => {
  const access_token = await login();
  console.log("res is ", access_token);

  const csrf_token = await get_csrf_token(access_token);
  console.log("res is ", csrf_token);

  const me_res = await get_me(access_token, csrf_token);
  console.log("me is ", me_res);
})();

when I run the script the result is:

$ node ./superset_api_test.js
res is  <jwt token>
res is  <csrf token>
me is  { message: 'Not authorized' }

and I think @raghulprashath is right. lines 35-63 of get_me method of superset.views.users.api.CurrentUserRestApi are like:

    @expose("/", methods=["GET"])
    @safe
    def get_me(self) -> Response:
        """Get the user object corresponding to the agent making the request
        ---
        get:
          description: >-
            Returns the user object corresponding to the agent making the request,
            or returns a 401 error if the user is unauthenticated.
          responses:
            200:
              description: The current user
              content:
                application/json:
                  schema:
                    type: object
                    properties:
                      result:
                        $ref: '#/components/schemas/UserResponseSchema'
            401:
              $ref: '#/components/responses/401'
        """
        try:
            if g.user is None or g.user.is_anonymous:
                return self.response_401()
        except NoAuthorizationError:
            return self.response_401()

        return self.response(200, result=user_response_schema.dump(g.user))

i can confirm that the same workflow works totally fine for version 1.2.0, albeit on a view other than /me

versions:

superset: 2.0.1
python: 3.8.16
nicolaskodak commented 1 year ago

Still facing this issue.

versions

superset: v2021.41.0-4182-gdf91664 ( `git clone` -> `docker compose -f docker-compose-non-dev.yml up -d` )
Python 3.8.13

have tried

The superset service is launched in docker, with superset_config_docker.py as

SESSION_COOKIE_SAMESITE = None
ENABLE_PROXY_FIX = True
PUBLIC_ROLE_LIKE_GAMMA = True
FEATURE_FLAGS = {
  "EMBEDDED SUPERSET" : True
}
CORS_OPTIONS = {
  'supports_credentials': True,
  'allow_headers': ['*'],
  'resources' : ['*'],
  'origins': ['http://localhost:8088', 'http://localhost:8888']
}
CSRF_ENABLED = False
WTF_CSRF_ENABLED = False

Wonder if anyone could point me to possible solutions for this?

raghulprashath commented 1 year ago

Actually I was able to solve this problem.

import requests
# from bs4 import BeautifulSoup

username = "admin"
password = "admin"
session = requests.session()

login_form = session.post('http://localhost:8088/login')
# soup = BeautifulSoup(login_form.text, 'html.parser')
# csrf_token = soup.find('input',{'id':'csrf_token'})['value']
data = {
  'username': username,
  'password': password,
  # 'csrf_token': csrf_token
}
response = session.post('http://localhost:8088/login', data=data)
response = session.get('http://localhost:8088/api/v1/me')

This is a work around to make this api work. Comment out the lines in above code, if you want CSRF token i.e iff you enabled the flag in config

sazary commented 1 year ago

I can confirm that this issue still exists with the latest release. Expected behavior, what goes wrong and steps to reproduce are the same as my first report.

versions:

apache-superset==2.1.0
Python 3.8.16

@rusackas Could you please re open this issue?

partizaans commented 1 year ago

After a couple of hours doing debugging it seems that I could resolve the problem with a temporary solution. In my case, I was requesting POST:api/v1/chart/data using the JWT authentication method. For this endpoint we have permission_str=can_read and the class_permission_name=Chart. somewhere in flask_appbuilder/security/decorators.py:84 we have:

            if current_app.appbuilder.sm.is_item_public(
                permission_str, class_permission_name
            ):

In my running superset instance, reading a chart was a public action but only some of the charts were actually public. So the decorator prevents execution of verify_jwt_in_request() before processing the request, because of can read on Chart is in permissions of the Public role.

Temporary Solution

On the superset UI I edited the public role and removed can read on Chart from its permissions.

huamichaelchen commented 1 year ago

I think the Referer header is missing could be the culprit?

Wrote a guide on it, hopefully, it helps someone 😄 Choose your preferred media 😝

https://huamichaelchen.substack.com/p/end-to-end-example-of-setting-up

https://medium.com/@huamichaelchen/end-to-end-example-of-setting-up-superset-embedded-dashboard-f72fc985559

Krishnamohan1604 commented 1 year ago

Hi Guys,

We are facing similar issue while authenticating superset API request (Superset version - 2.0.1). I have tried all the solution listed here but unfortunately nothing worked for me. Do we have any update on the issue?

Thanks!

larshelge commented 1 year ago

This issue seems to be present in version 3.0.0 as well. Running with Docker Compose. Requests to /api/v1/me/roles is always returning 401. This seems to break the embed dashboard feature. It would be great if anyone on the Superset side could look into this.

lucidprojects commented 11 months ago

same issue on 3.0.2

hhhonzik commented 11 months ago

Temporary Solution

On the superset UI I edited the role of public and removed can read on Chart from its permissions.

We've just had this issue when upgrading 3.0.1 to 3.1.0rc3 and this solved it. Thanks @partizaans

m1zzo commented 10 months ago

3.0.2 Roles was 401, but after turning Talisman on - all seems work. Except other then security API endpoints :(

michael-s-molina commented 9 months ago

Closing this as it seems the issue was resolved in recent versions of Superset. Please reopen if that's not the case.

EuphoriaCelestial commented 8 months ago

@michael-s-molina Hi, I am using the lastest version of Superset, but still facing this issue, can we re-open it? All settings are leave as default except FAB_ADD_SECURITY_API = True I am able to login, get csrf_token and call every other api except /api/v1/me/, it return 401

partizaans commented 4 months ago

Closing this as it seems the issue was resolved in recent versions of Superset. Please reopen if that's not the case.

Faced the same problem after following the upgrade instruction to the latest stable version (4.0.2) and happened again. @michael-s-molina

lsfc02 commented 3 months ago

Closing this as it seems the issue was resolved in recent versions of Superset. Please reopen if that's not the case.

Faced the same problem after following the upgrade instruction to the latest stable version (4.0.2) and happened again. @michael-s-molina

Same thing here, did you manage to solve it?

partizaans commented 3 months ago

Closing this as it seems the issue was resolved in recent versions of Superset. Please reopen if that's not the case.

Faced the same problem after following the upgrade instruction to the latest stable version (4.0.2) and happened again. @michael-s-molina

Same thing here, did you manage to solve it?

Unfortunately no. Had to do some manual actions after upgrading was completed, described as the Temporary Solution here: https://github.com/apache/superset/issues/19525#issuecomment-1566791040

julienbrodier commented 1 month ago

Same here. Getting a 404 when calling /api/v1/dashboard/14

1. Possible Explanation:

2. User Context (current_user)

3. Differences in Initialization of Global Variables (g or current_user)

mat-codehaus commented 1 month ago

Why is def get_me(self) -> Response: not decorated with @protect() ?

I'll confess to not knowing the code that well, but is there another way if g.user is None or g.user.is_anonymous: can return false without @protect() ?