Closed nicolaipre closed 2 years ago
I have almost got this working, and will happily share what I have once it works properly.
The problem right now is that exceptions do not trigger HTTP responses and return a 500 error when using authentication middleware on Controller Routes instead of Resource Routes extending the JWTAuthentication class.
not entirely sure I understand the issue. You want a custom HTTP response based on the exception thrown?
all exceptions should result in that 500 page and anytime you send Content-Type: application/json it will render that response as JSON
Thank you for clarifying that.
According to what you explained above, the exceptions defined here should return a response rendered as JSON containing the error of the exception that occurred, as long as Content-Type: application/json
is set in the request.
The problem here seems to be that even though Content-Type: application/json
is specified in the request, I still get the 500 error and not a rendered response.
For example, the following request returns ERROR:root:NoApiTokenFound in /home/user/backend/app/http/middleware/JWTAuthMiddleware.py on line 19
(500 Internal server error) when it should have returned {'error': 'no API token found'}
.
Everything works fine when a valid token is set, but whenever exceptions occur I get stuck at the 500 error instead of a rendered response.
Relevant routes
# Protected SSR routes. Requires valid JWT.
RouteGroup(
prefix="/jwt",
middleware=['jwt'],
routes=[
...
GET('/profile', 'JWTAuthController@test').name('jwt.profile'),
...
]
),
middleware.py
...
ROUTE_MIDDLEWARE = {
'auth': AuthenticationMiddleware,
'verified': VerifyEmailMiddleware,
'guard': GuardMiddleware,
# Custom auth middlewares
'jwt': JWTAuthMiddleware,
'apikey': APIKeyMiddleware,
}
...
JWTAuthMiddleware.py
"""Authentication Middleware."""
from masonite.request import Request
from masonite.api.authentication import JWTAuthentication, PermissionScopes
class JWTAuthMiddleware(JWTAuthentication):
"""Middleware To Check If a request contains a valid JWT token."""
def __init__(self, request: Request):
"""Inject Any Dependencies From The Service Container.
Arguments:
request {masonite.request.Request} -- The Masonite request class.
"""
self.request = request
def before(self):
"""Run This Middleware Before The Route Executes."""
self.authenticate(self.request)
return self.request
def after(self):
"""Run This Middleware After The Route Executes."""
pass
JWTAuthController.py
"""A AuthController Module."""
import jwt
import pendulum
from config.application import KEY
from masonite.auth import Auth, Sign, MustVerifyEmail
from masonite.request import Request
from masonite.controllers import Controller
from masonite.api.controllers import TokenController
from masonite.validation import Validator
from masonite.managers import MailManager
class JWTAuthController(Controller, TokenController):
"""AuthController Controller Class."""
def login(self, request: Request, auth: Auth):
return self.jwt(request, auth)
def logout(self, request: Request, auth: Auth):
# TODO: Implement logout
pass
def refresh(self, request: Request):
return self.jwt_refresh(request)
def register(self, request: Request, auth: Auth, mail_manager: MailManager, validate: Validator):
if not request.input('username') or not request.input('password') or not request.input('email'):
request.status(401)
return {'error': 'missing username, email or password.'}
errors = request.validate(
validate.required(["username", "email", "password"]),
validate.email("email"),
validate.strong(
"password",
length=8,
special=1,
uppercase=1,
# breach=True checks if the password has been breached before.
# Requires 'pip install pwnedapi'
breach=False,
),
)
if errors:
return {'error': f'{errors}'}
try:
user = auth.register({
"name": request.input('username'),
"password": request.input('password'),
"email": request.input('email'),
})
except:
# FIXME: Check if user already exists with given name or email.
return {"error": "Registration failed upon creating user."}
if isinstance(user, MustVerifyEmail):
user.verify_email(mail_manager, request)
if user:
return {'message': 'Registration successful.'}
return {'error': 'Registration failed.'}
def me(self, request: Request, auth: Auth):
return {'message': 'jwt_token["user"]'}
def test(self):
return {"message": "hello"}
This is also the same problem I addressed in MasoniteFramework/api#18.
Here is where the logic happens.
I think I see whats going on. In the middleware to set a response you need to do something like:
"""Authentication Middleware."""
from masonite.request import Request
from masonite.response import Response
from masonite.api.authentication import JWTAuthentication, PermissionScopes
class JWTAuthMiddleware(JWTAuthentication):
"""Middleware To Check If a request contains a valid JWT token."""
def __init__(self, request: Request, response: Response):
"""Inject Any Dependencies From The Service Container.
Arguments:
request {masonite.request.Request} -- The Masonite request class.
"""
self.request = request
self.response = response
def before(self):
"""Run This Middleware Before The Route Executes."""
if not self.authenticate(self.request):
self.response.json({'error': 'error message here'}, status=500)
def after(self):
"""Run This Middleware After The Route Executes."""
pass
Something like that
I attempted to apply this fix but unfortunately, the same issue still occurs. I have checked the headers that are being sent and verified that they are passed but in the "environ" part of the request. Could it be related to the header rewriting, where "HTTP_" is appended to certain headers? When printing the request in the middleware, I noticed that sending Content-Type
is being stored as environ.CONTENT_TYPE, and not HTTP_CONTENT_TYPE.
No idea if this is relevant though.
Update:
Also, the JWTAuthentication method from MasoniteFramework/api
does not really return anything, so not sure if that if not self.authenticate(self.request)
is useful at all?
Update 2:
Everything works fine when a Resource is requested and is used with masonite.api.providers import ApiProvider
. Maybe it is related to how resource routes are handled compared to controller routes?
Relevant routes
...
AdminUserResource('/users').routes(),
...
AdminUserResource.py
from app.User import User
from masonite.request import Request
from masonite.api.resources import Resource
from masonite.api.serializers import JSONSerializer
from masonite.api.authentication import JWTAuthentication, PermissionScopes, TokenAuthentication
from masonite.api.filters import FilterScopes
#, TokenAuthentication): # JWTAuthentication, PermissionScopes, FilterScopes)
class AdminUserResource(Resource, JSONSerializer, JWTAuthentication):
model = User
#methods = ['create', 'index', 'show']
#without = ['id', 'email', 'password']
#scopes = ['user:read']
"""
filter_scopes = {
'user:read': ['name', 'email'],
'user:manager': ['id', 'name', 'email', 'active', 'password']
}
"""
def show(self, request: Request):
return self.model.where('active', self.request.input('active')).get()
def index(self):
return self.model.all()
Also, is middleware the correct way to handle what I am trying to achieve with protected routes, or should I use guards for this instead? I am not that familiar with the functionality of guards and how they differ from middleware just yet.
Ok in gonna take a look at this myself and see what the issue is. It should be working as is
@nicolaipre Ok I made a middleware like this. It actually wasn't functioning exactly like I thought it would. I'm planning on rewriting how this header stuff works internally in Masonite 3.
With that being said you might have to do something like this:
"""Authentication Middleware."""
from masonite.request import Request
from masonite.response import Response
class JWTAuthMiddleware:
"""Middleware To Check If The User Is Logged In."""
def __init__(self, request: Request, response: Response):
"""Inject Any Dependencies From The Service Container.
Arguments:
request {masonite.request.Request} -- The Masonite request class.
"""
self.request = request
self.response = response
def before(self):
"""Run This Middleware Before The Route Executes."""
if self.request.header('Content-Type') == "application/json":
self.response.json({"error": "wrong"}, status=500)
def after(self):
"""Run This Middleware After The Route Executes."""
pass
Thank you for the fix in PR#360.
I just spent the last two hours debugging and writing a massive reply here, but all of a sudden I managed to get it working with your help! :+1: Very happy with that.
I solved the problem by rewriting the Middleware and creating a JWTAuthProvider class that extends MasoniteFramework/api's JWTAuthentication
.
The problem was that something was being done under the hood since error messages were being returned perfectly fine with Resources, but not with my JWTAuthMiddleware
. It seemed that BaseAuthentication.run_authentication
never got executed even though the Middelware extended JWTAuthentication
and I had to handle exceptions similar to this.
JWTAuthMiddleware.py
"""JWT Authentication Middleware."""
from masonite.request import Request
from masonite.response import Response
from masonite.api.authentication import JWTAuthentication, PermissionScopes
from ...providers.JWTAuthProvider import JWTAuthProvider
class JWTAuthMiddleware(JWTAuthProvider):
"""Middleware To Check If a Request Contains a Valid JWT Token."""
def __init__(self, request: Request, response: Response):
"""Inject Any Dependencies From The Service Container.
Arguments:
request {masonite.request.Request} -- The Masonite request class.
response {masonite.response.Response} -- The Masonite response class.
"""
self.request = request
self.response = response
def before(self):
"""Run This Middleware Before The Route Executes."""
self.check_jwt(self.request)
def after(self):
"""Run This Middleware After The Route Executes."""
pass
JWTAuthProvider.py
import pendulum
from masonite.request import Request
from masonite.api.exceptions import NoApiTokenFound, ExpiredToken, InvalidToken
from masonite.api.authentication import JWTAuthentication, PermissionScopes
from masonite.api.exceptions import (ApiNotAuthenticated, ExpiredToken, InvalidToken,
NoApiTokenFound, PermissionScopeDenied,
RateLimitReached)
class JWTAuthProvider(JWTAuthentication):
def check_jwt(self, request: Request):
""" Valdate the JWT token specified in Request """
try:
self.authenticate(self.request)
except ApiNotAuthenticated:
return self.response.json({'error': 'token not authenticated'}, status=500) # TODO: Change status codes...
except ExpiredToken:
return self.response.json({'error': 'token has expired'}, status=500)
except InvalidToken:
return self.response.json({'error': 'token is invalid'}, status=500)
except NoApiTokenFound:
return self.response.json({'error': 'no API token found'}, status=500)
except PermissionScopeDenied:
return self.response.json({'error': 'token has invalid scope permissions'}, status=500)
except RateLimitReached:
return self.response.json({'error': 'rate limit reached'}, status=500)
except Exception as e:
#raise e
return self.response.json({'error': str(e)}, status=500)
I did not find many tutorials or guides online where Masonite was used for RESTful APIs, so I have created a repo here for anyone who might find this useful.
As a feature request in 3.0: Perhaps a command such as craft auth:jwt
that implements functionality like this could be added for those wanting to use Masonite for RESTful APIs.
Closing in favor of #465. Will use this issue as reference
Hi.
A few weeks back I moved over to this project from Laravel, since I wanted to start writing my project backends in Python. I have also previously used Flask and FastAPI but did not like the way those libraries structure web applications. This project however does it in a very clean way and is one of the reasons why I like it and want to use it.
However, I have spent countless hours now trying to figure out how to get proper JWT authentication for my Single Page Application, where I will not use views. It is not starting to get tiring trying to get the framework to work the way I want it to and it's putting my development to a halt.
What I am trying to achieve, is simply getting a working and stable web application with the following routes:
It would be amazing if something like this could be crafted by doing
craft auth:jwt
for JWT authentication andcraft auth:apikey
for API Key authentication.