opencdms-dev / legacy-opencdms-api

⭐🐍 OpenCDMS server application written in Python (FastAPI) and exposing a web interface for `opencdms-app` and other applications
MIT License
3 stars 3 forks source link

API authentication using Django credentials #4

Open isedwards opened 3 years ago

isedwards commented 3 years ago

Can access to our FastAPI API be managed using Django or directly with Django's auth_user table?

Shaibujnr commented 3 years ago

Hello @isedwards,

According to this (https://stackoverflow.com/questions/62671257/import-hashed-password-from-flask-to-django) django uses the PBKDF2-sha256 hashing algorithm by default. We could also use the same algorithm in fastapi. I'd be happy to test this out if I can get the django application source to to ensure it's using the default hasher and also test data.

The migration discussed in the stack overflow post might also be something we can consider. If we can't figure out a way to reproduce the hashing algorithm on FastAPI's side of things, we can have the django application run parallel for a while to authenticate users and have them update their passwords. The new passwords will be stored with a known hash.

Or, instead of having to run the django parallel on a separate server. We can mount the django application on a particular path as described in this guide (https://fastapi.tiangolo.com/advanced/wsgi/). This feature hasn't been used extensively I believe however there has been next to no report on issues concerning this feature.

I'd suggest to go with approach one. I'd be happy to test it out and based on the outcome we can decide further.

isedwards commented 3 years ago

I prefer the first option because it doesn't make Django a mandatory dependency. Could you have a go with a fresh install of Django 2.2.8?

We should have access to actual Django code base that we'll be interesting with in about 2 weeks.

There's another requirement I'd like to consider.... they have an existing Web API they've created using Django REST Framework that doesn't have authentication. Instead of adding authentication to their solution using Django, is there a sensible way for them to use our FastAPI solution to provide auth using the first option you mentioned and then expose their existing API?

Shaibujnr commented 3 years ago

@isedwards I will test this on a fresh install with Django 2.2.8. And yes absolutely once we are able to get the hashing right, we can recreate and produce the existing endpoints with FastAPI and also have them protected with JWT Bearer Tokens.

Shaibujnr commented 3 years ago

Hello @isedwards I was able to setup a django project with Django-2.2.8 and I created 2 users in the sqlite3 database using django-admin createsuperuser and also using the User model directly. Here is a sample of the password hash in the django database. pbkdf2_sha256$150000$h8igyXN7OAc9$27hvHOmtp9HS3zy8OdppcFJYFb28IC+IWozGgzwr7ps=.

Good news is, I was able to verify the hash on FastApi side using passlib. Apparently passlib directly supports django password hashing algorithms. (https://passlib.readthedocs.io/en/stable/lib/passlib.hash.django_std.html#django-1-4-hashes).

isedwards commented 3 years ago

That's great news, thank you @Shaibujnr. Can you suggest options for securing a Django REST API by putting opencdms-server in front to manage authentication and requests (presumably either as a reverse proxy or using the WSGI mounting option that you mentioned)?

Hopefully we can use this to solve the issues here: https://github.com/opencdms/surface/issues/15

I've tried to illustrate this below:

api-security

Shaibujnr commented 3 years ago

@isedwards If I understand correctly.We have HTTP API endpoints currently in Django that are accessible by anyone and we would like to have a n authentication system for these endpoints right?

I don't think mounting would work in this case, since mounting would simply expose the Django application to the public on a specific path.

For this to work we would have to hide the the Django APIs from the public entirely. I'd suggest we run both servers, the FastAPI one would be exposed to the public and would implement the authentication and then the Django API will be served internally (only accessible by the FastAPI server). On successful authentication, the FastAPI would simply make an API request to the corresponding Django API.

isedwards commented 3 years ago

Could you check and make sure? It sounds like the WSGI mounting approach "wraps" the other application - so presumably we could limit access to its end points based on whether the user is authenticated (but otherwise allow unmodified access)?

Shaibujnr commented 3 years ago

@isedwards FasAPI mounts WSGI applications using a WSGIMiddleware this might be good news. Since it's a middle ware, it's most likely possible to have another middleware perform authentication before calling the WSGI app. I'll try this with the mock django application and give feedback by tomorrow.

Shaibujnr commented 3 years ago

Hello @isedwards Good news, wrapping the WSGIMiddleware with another custom middleware for authentication works well. Here is the snippet for the middleware.

class DjangoAuthenticationMiddleWare:
    """Middleware for authenticating a request before passing it on
    to the mounted django application.
    """

    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        request = Request(scope, receive, send)
        authorization_header = request.headers.get("Authorization")
        if authorization_header is None:
            raise HTTPException(401, "Unauthorized request")
        scheme, token = get_authorization_scheme_param(authorization_header)
        if scheme.lower() != "bearer":
            raise HTTPException(401, "Invalid authorization header scheme")
        # Token Validation code goes here
        if token != "auth_token":
            raise HTTPException(401, "Unauthorized request")
        await self.app(scope, receive, send)

# Wrap django_application  with WSGIMiddleware and DjangoAuthenticationMiddlewre
app.mount("/django", DjangoAuthenticationMiddleWare(WSGIMiddleware(django_application)))

This works well.

Shaibujnr commented 3 years ago

@isedwards https://github.com/opencdms/opencdms-server/pull/7

I have a branch make_mch_api_installable that defines a setup.py file for the mch_api flask applicaiton. This allowed me to install the mch-api into opencdms_server and then import and mount the flask applicaiton.

I couldn't follow this approach for the surface django project. I could not successfully package the entire project to make it installable and importable. This is mainly because the manage.py file required to run most of the django commands, is not in a package, it's located in the root folder.

As a work around, the opencdms_server docker image, clones the surface django project into its code source, installs all it's requirements and imports the wsgi application from there.

NOTE: I also had the set PYTHONPATH environment variable to include the cloned surface so that the opencdms_server can import it successfully.

isedwards commented 3 years ago

Thank you @Shaibujnr - this is looking excellent.

Could you submit a pull request to the mch-api repository for your branch and we can get that merged.

Also, could you give an example of how we can log in and make authenticated API requests (do you have a method that you are using to do this when you are testing)?