Open baxeico opened 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
Hi @vitalik!
Any news?
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).
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?
@fantasticle nope, there is a chance you have some typo :) without a code could not help
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.
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.
@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.
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}
@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.
@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
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"
@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?
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.
Is it secure to have a static "JWT_SIGNING_KEY" like that?
@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
@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?
@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'sTokenRefreshView
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 }
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!