vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.2k stars 426 forks source link

JWT authentication #45

Open baxeico opened 3 years ago

baxeico commented 3 years ago

Hi @vitalik , first of all congrats for your work in this project! I really like it. I've a simple question, sorry if this is a trivial one! ;) In Django Rest Framework we can integrate JWT authentication quite easily using this https://github.com/SimpleJWT/django-rest-framework-simplejwt.

Is there something similar for django-ninja? I saw that in another issue (https://github.com/vitalik/django-ninja/issues/9) you mentioned JWT, but I don't understand how it could be done in practice.

Thank you very much for your help and keep up the good work!

vitalik commented 3 years ago

it is not out of the box, but (as it actually requested few more time) I will try to pull out some example into separate repository as a plugin

bondarev commented 3 years ago

Hi @vitalik!

Any news?

ghost commented 3 years ago

I was having the same question... Since JWT is just another form of bearer authentication what about this? I'm using python-jose for the JWT.

from jose import jwt

[...]
class JWT(HttpBearer):
    def authenticate(self, request, token):
        try:
            jwt.decode(token, 'key')
            return True
        except:
            return False
[...]

@router.get('/', auth=JWT())

This works just fine.

EDIT: Somehow it only worked when using Django's development server and didn't translate to the productive environment. God knows why but it's already too late for me to think properly. I fiddled around with it even more, another solution at least within a development setting is:


def validate_jwt(request):
    token = request.META['HTTP_AUTHORIZATION'].split(" ")[1]
    try:
        jwt.decode(token, 'key')
        return True
    except:
        return False
[...]
@router.get('/', auth=validate_jwt)

But this also sucks. I will look into it tomorrow.

EDIT 2: Seems like I don't get the standard AuthBearer way to work either (following the tutorial).

ghost commented 3 years ago

I don't know if there is a bug or not, but I could make it work with the ApiKey(APIKeyHeader) method in my production environment. I contaminated every second line of my script with loggers, and for the AuthBearer method, it seems like the authentication function is never called. Has anyone experienced a similar thing?

vitalik commented 3 years ago

@fantasticle nope, there is a chance you have some typo :) without a code could not help

bencleary commented 2 years ago

I have been able to integrate it easily in a few places, initially i followed the GlobalAuth example in the docs here by extending the HttpBearer class like so and using the JWTAuthentication class from the simplejwt library, like so:

from typing import Any, Optional
from django.http import HttpRequest
from ninja.security import HttpBearer
from rest_framework_simplejwt.authentication import JWTAuthentication

class JWTAuthRequired(HttpBearer):
    def authenticate(self, request: HttpRequest, token: str) -> Optional[Any]:
        jwt_authenticator = JWTAuthentication()
        try:
            response = jwt_authenticator.authenticate(request)
            if response is not None:
                return True # 200 OK
            return False # 401
        except Exception:
            # Any exception we want it to return False i.e 401
            return False

As for the creation of tokens, its a fairly simple approach also. I have 2 schemas one for the auth and one for the repsonse:


from ninja import Schema

class AuthSchema(Schema):
    username: str
    password: str

class JWTPairSchema(Schema):
    refresh: str
    access: str

These are then used in my authentication router like so:

from ninja import Router
from auth.api.schema import AuthSchema, JWTPairSchema
from django.contrib.auth import authenticate
from rest_framework_simplejwt.tokens import RefreshToken

router = Router()

@router.post('/login', response=JWTPairSchema, auth=None)
def login(request, auth: AuthSchema):
    user = authenticate(**auth.dict())
    if user is not None:
        refresh = RefreshToken.for_user(user)

    return {
        'refresh': str(refresh),
        'access': str(refresh.access_token),
    }

I have reused all my authentication logic from DRF in django-ninja and it works well. Hope that helps someone else.

VetalM84 commented 1 year ago

Hi, found this post to implement auth https://www.reddit.com/r/django/comments/r2tti8/django_ninja_auth_example/ But that did not work for me.

epicserve commented 1 year ago

@VetalM84,

@bencleary's comment above worked for me. However, it's less than ideal and isn't something I want to go to production with since it installs the Django Rest Framework because it is a dependency of djangorestframework-simplejwt.

I'm curious, what didn't work in the example you linked to? I think the solution you linked to since it uses PyJWT.

P.S. your link didn't work when I clicked on it. I had to copy and paste it.

For the project I'm working on, I'm also considering going back to just using django_auth and coming up with a system for the front-end to use csrf tokens.

VetalM84 commented 1 year ago

I'm curious, what didn't work in the example you linked to?

I've got the working one based on that!

def create_token(username):
    jwt_signing_key = getattr(settings, "JWT_SIGNING_KEY", None)
    jwt_access_expire = getattr(settings, "JWT_ACCESS_EXPIRY", 60)
    payload = {"username": username}
    access_expire = datetime.datetime.now(tz=timezone.utc) + datetime.timedelta(
        minutes=jwt_access_expire
    )
    payload.update({"exp": access_expire})
    token = jwt.encode(payload=payload, key=jwt_signing_key, algorithm="HS256")
    return token

class AuthBearer(HttpBearer):
    def authenticate(self, request, token):
        jwt_signing_key = getattr(settings, "JWT_SIGNING_KEY", None)
        try:
            payload = jwt.decode(token, key=jwt_signing_key, algorithms=["HS256"])
        except Exception as e:
            return {"error": e}
        username: str = payload.get("username", None)
        return username

@api.post("/sign_in", auth=None)
def sign_in(request, username: str = Form(...), password: str = Form(...)):
    user_model = get_object_or_404(User, username=username)

    passwords_match = check_password(password, user_model.password)
    if not passwords_match:
        raise ValidationError([{"error": "Wrong password"}])

    token = create_token(user_model.username)
    return {"token": token}
ognjenk commented 1 year ago

@VetalM84 Hey, I'm the author of that comment in the reddit thread. As far as I can tell you managed to get it to work now, but if you have any more problems let me know and I'll try to help. I am building a pretty large enterprise app and this auth method has been happily working for me for a year now.

VetalM84 commented 1 year ago

@ognjenk How I can unittest sign in endpoint?

    def test_sign_in(self):
        """Test Sing in."""
        data = {
            "username": "TestUserName",
            "password": "test"
        }
        response = self.client.post(
            path="/api/sign_in",
            data=data,
            content_type="application/x-www-form-urlencoded",
            follow=True
        )
        self.assertEqual(response.status_code, 200)

This code returns me AssertionError: 422 != 200

ognjenk commented 1 year ago

Hey, if you have set up your sign in endpoint to send urlencoded data (and set up parameters as form data with something like username: str = Form(...), password: str = Form(...) than you would have to set up your test something like this:

data = f"username={username}&password={password}"
response = client.post(f"{API_ROOT}/auth/sign_in", data=data, content_type="application/x-www-form-urlencoded")

I have since switched my sign in endpoint to expect the usual json post request where I'm sending username and password in json, so then the test would be like yours, only content_type would be = "application/json"

VetalM84 commented 1 year ago

@ognjenk Thank you. Although I've found one more easy solution:

...
username: str = Form(...), password: str = Form(...)
...
    def test_sign_in(self):
        """Test Sing in."""
        data = {"username": "TestUserName", "password": "test"}
        response = self.client.post(
            path="/api/sign_in",
            data=data,
            # or like this
            # data=f"username=TestUserName&password=test",
            # content_type="application/x-www-form-urlencoded",
        )
        self.assertEqual(response.status_code, 200)

I just had to remove this line content_type="application/x-www-form-urlencoded" and it worked like I did a json request.

I wonder do you user roles (Django user groups) to split access to different endpoints?

ognjenk commented 1 year ago

No, we have rolled out our own solution for endpoint authorization. We cache it in Redis per user and have created a custom decorator which we use for endpoints that need the check.

bendowlingtech commented 1 year ago

Is it secure to have a static "JWT_SIGNING_KEY" like that?

picturedots commented 4 months ago

@VetalM84 's solution worked for me, but I had to set the request.user in the authenticate method, i.e.

def authenticate(self, request, token):
        ...
        User = get_user_model()
        user = get_object_or_404(User, username=username)
        request.user = user
        ...

I would also recommend changing the sign in step to use the authenticate method, check user.is_active and use AuthenticationError exceptions. For example

def sign_in(request, ...):
    ....
    user = authenticate(username=username, password=password)
    if user is not None and user.is_active:
        return {"token": create_token(user.username)}
    raise AuthenticationError
async-costelo commented 2 months ago

@bencleary I hope this comment gets to you. I've used your example in my app, and it worked perfectly. Since we are manually creating the tokens via RefreshToken method, how do you implement Simple JWT's TokenRefreshView into Django-Ninja way?

async-costelo commented 2 months ago

@bencleary I hope this comment gets to you. I've used your example in my app, and it worked perfectly. Since we are manually creating the tokens via RefreshToken method, how do you implement Simple JWT's TokenRefreshView into Django-Ninja way?

nevermind, I think this does the trick


from rest_framework_simplejwt.tokens import RefreshToken

@router.post("/v1/token/refresh", auth=None}
def refresh_token(request, req: someschema):

    token = RefreshToken(req.refresh)

    return { "access": token.access_token }