I'm building an API with Flask-Restless that requires an API key, that will be in the Authorization HTTP header.
In the Flask-Restless example here for a preprocessor:
def check_auth(instance_id=None, **kw):
# Here, get the current user from the session.
current_user = ...
# Next, check if the user is authorized to modify the specified
# instance of the model.
if not is_authorized_to_modify(current_user, instance_id):
raise ProcessingException(message='Not Authorized',
status_code=401)
manager.create_api(Person, preprocessors=dict(GET_SINGLE=[check_auth]))
How do I retrieve the Authorization header in the check_auth function?
I have tried accessing the Flask response object, but it is None during the scope of this function. The kw parameter is also an empty dict.
In a normal Flask request-response cycle, the request context is active when the Flask-Restful preprocessors and postprocessors are being run.
As such, using:
from flask import request, abort
def check_auth(instance_id=None, **kw):
current_user = None
auth = request.headers.get('Authorization', '').lower()
try:
type_, apikey = auth.split(None, 1)
if type_ != 'your_api_scheme':
# invalid Authorization scheme
ProcessingException(message='Not Authorized',
status_code=401)
current_user = user_for_apikey[apikey]
except (ValueError, KeyError):
# split failures or API key not valid
ProcessingException(message='Not Authorized',
status_code=401)
should Just Work.
Related
I'm creating a Restful API using Django Rest Framework, i'm not serving sensitive data but still i wanted to add some sort of authorization system for viewing my API endpoints.
Basically each user has an API key assigned, and in order to view any endpoint, the user needs to provide the key when performing any request. All the endpoints use only GET to retrieve the data, so what i did is the following:
The API key is provided in the GET params, so something like myURL/api/endpoint/?key=1234&filter=test
A middleware checks if that API key exists in my database, and if it does the user is able to get the data.
Here is my middleware:
TOKEN_QUERY = "key"
class TokenMiddleware(AuthenticationMiddleware):
def process_request(self, request):
if request.user.is_authenticated:
return None
else:
try:
token = request.GET[TOKEN_QUERY]
except Exception as e:
# A token isn't included in the query params
return JsonResponse({'error': 'Missing parameter: make sure to include your key.'})
try:
query = API_keys.objects.get(api_token=token)
except:
token = None
if token != None:
return None
else:
return JsonResponse({'error': 'Authentication failed. Make sure to provid a valid API key.'})
This system works without any problem, but i'm concerned about safety. How safe is this? Should i not use a GET request (of course i'll make sure to use HTTPS and SSL) ? Or is there a de facto way to create this kind of system? Any kind of advice is appreciated.
You can try this
from rest_framework import permissions
TOKEN_QUERY = "key"
# guest token validation class
class GuestTokenPermission(permissions.BasePermission):
def __init__(self, allowed_methods):
self.allowed_methods = allowed_methods
def has_permission(self, request, view):
token = request.META.get('HTTP_GUEST_TOKEN', None)
if token == TOKEN_QUERY:
return request.method in self.allowed_methods
else:
if request.user.is_superuser:
return request.method in self.allowed_methods
# put where you want to set permission
permission_classes = (partial(GuestTokenPermission, ['GET', 'POST', 'HEAD']),)
Refer https://www.django-rest-framework.org/api-guide/permissions/
I have know that we created a session object with unique sessionID to response to client when a user first logged, and then when user request others' they will request with a cookie with that ID, so server can find the session object by that ID, which will denote the user have logged!
But this is one user situation, I find most blogs doesn't say if there are many users to manage, if we need to create many sessions in memory to every user. I think so!
But when I lookup flask-login source code, I can't find a session collections to maintain session for every user?
def login_user(user, remember=False, force=False, fresh=True):
'''
Logs a user in. You should pass the actual user object to this. If the
user's `is_active` property is ``False``, they will not be logged in
unless `force` is ``True``.
This will return ``True`` if the log in attempt succeeds, and ``False`` if
it fails (i.e. because the user is inactive).
:param user: The user object to log in.
:type user: object
:param remember: Whether to remember the user after their session expires.
Defaults to ``False``.
:type remember: bool
:param force: If the user is inactive, setting this to ``True`` will log
them in regardless. Defaults to ``False``.
:type force: bool
:param fresh: setting this to ``False`` will log in the user with a session
marked as not "fresh". Defaults to ``True``.
:type fresh: bool
'''
if not force and not user.is_active:
return False
user_id = getattr(user, current_app.login_manager.id_attribute)()
session['user_id'] = user_id
session['_fresh'] = fresh
session['_id'] = _create_identifier()
if remember:
session['remember'] = 'set'
_request_ctx_stack.top.user = user
user_logged_in.send(current_app._get_current_object(), user=_get_user())
return True
There is one session to keep the user, but what if another user come?
# -*- coding: utf-8 -*-
"""
flask.globals
~~~~~~~~~~~~~
Defines all the global objects that are proxies to the current
active context.
:copyright: (c) 2011 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
from functools import partial
from werkzeug.local import LocalStack, LocalProxy
def _lookup_req_object(name):
top = _request_ctx_stack.top
if top is None:
raise RuntimeError('working outside of request context')
return getattr(top, name)
def _lookup_app_object(name):
top = _app_ctx_stack.top
if top is None:
raise RuntimeError('working outside of application context')
return getattr(top, name)
def _find_app():
top = _app_ctx_stack.top
if top is None:
raise RuntimeError('working outside of application context')
return top.app
# context locals
_request_ctx_stack = LocalStack()
_app_ctx_stack = LocalStack()
current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))
I find session is an global variable, and is an localstack(), but I still don't konw how does it works?
class Local(object):
__slots__ = ('__storage__', '__ident_func__')
def __init__(self):
object.__setattr__(self, '__storage__', {})
object.__setattr__(self, '__ident_func__', get_ident)
def __iter__(self):
return iter(self.__storage__.items())
def __call__(self, proxy):
"""Create a proxy for a name."""
return LocalProxy(self, proxy)
def __release_local__(self):
self.__storage__.pop(self.__ident_func__(), None)
def __getattr__(self, name):
try:
return self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
def __setattr__(self, name, value):
ident = self.__ident_func__()
storage = self.__storage__
try:
storage[ident][name] = value
except KeyError:
storage[ident] = {name: value}
def __delattr__(self, name):
try:
del self.__storage__[self.__ident_func__()][name]
except KeyError:
raise AttributeError(name)
Many people say it will use another thread id to identify, storage[ident][name] = value , but i disable threading, it works well for multi-users?
I just find it use current_user variable to identify current user, but current_user is so magic! It doesn't maintain user session collection but just one current_user to solve the problem! I don't know how it works?
def login_required(func):
'''
If you decorate a view with this, it will ensure that the current user is
logged in and authenticated before calling the actual view. (If they are
not, it calls the :attr:`LoginManager.unauthorized` callback.) For
example::
#app.route('/post')
#login_required
def post():
pass
If there are only certain times you need to require that your user is
logged in, you can do so with::
if not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
...which is essentially the code that this function adds to your views.
It can be convenient to globally turn off authentication when unit testing.
To enable this, if the application configuration variable `LOGIN_DISABLED`
is set to `True`, this decorator will be ignored.
.. Note ::
Per `W3 guidelines for CORS preflight requests
<http://www.w3.org/TR/cors/#cross-origin-request-with-preflight-0>`_,
HTTP ``OPTIONS`` requests are exempt from login checks.
:param func: The view function to decorate.
:type func: function
'''
#wraps(func)
def decorated_view(*args, **kwargs):
if request.method in EXEMPT_METHODS:
return func(*args, **kwargs)
elif current_app.login_manager._login_disabled:
return func(*args, **kwargs)
elif not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
return func(*args, **kwargs)
return decorated_view
So where is process of comparing current user sessionID from cookie with session collection mantained by server? Anybody can help me?
I take a look at flask-login/flask_login/login_manager.py:_load_user()
I guess you are talking about SESSION_PROTECTION. In this case the way user will be reloaded depends on basic or strong auth modes. If you have no session protection, flask try to load the user from request, header or cookies if you have handlers for this.
class LoginManager(object):
...
def _load_user(self):
'''Loads user from session or remember_me cookie as applicable'''
user_accessed.send(current_app._get_current_object())
# first check SESSION_PROTECTION
config = current_app.config
if config.get('SESSION_PROTECTION', self.session_protection):
deleted = self._session_protection()
if deleted:
return self.reload_user()
# If a remember cookie is set, and the session is not, move the
# cookie user ID to the session.
#
# However, the session may have been set if the user has been
# logged out on this request, 'remember' would be set to clear,
# so we should check for that and not restore the session.
is_missing_user_id = 'user_id' not in session
if is_missing_user_id:
cookie_name = config.get('REMEMBER_COOKIE_NAME', COOKIE_NAME)
header_name = config.get('AUTH_HEADER_NAME', AUTH_HEADER_NAME)
has_cookie = (cookie_name in request.cookies and
session.get('remember') != 'clear')
if has_cookie:
return self._load_from_cookie(request.cookies[cookie_name])
elif self.request_callback:
return self._load_from_request(request)
elif header_name in request.headers:
return self._load_from_header(request.headers[header_name])
return self.reload_user()
def _load_from_request(self, request):
user = None
if self.request_callback:
user = self.request_callback(request)
if user is not None:
self.reload_user(user=user)
app = current_app._get_current_object()
user_loaded_from_request.send(app, user=_get_user())
else:
self.reload_user()
Flask passes request to your callback if it presented. Flask-login has good example(Custom Login using Request Loader) how you can load user from request.
#login_manager.request_loader
def load_user_from_request(request):
# first, try to login using the api_key url arg
api_key = request.args.get('api_key')
if api_key:
user = User.query.filter_by(api_key=api_key).first()
if user:
return user
# next, try to login using Basic Auth
api_key = request.headers.get('Authorization')
if api_key:
api_key = api_key.replace('Basic ', '', 1)
try:
api_key = base64.b64decode(api_key)
except TypeError:
pass
user = User.query.filter_by(api_key=api_key).first()
if user:
return user
# finally, return None if both methods did not login the user
return None
The api_key can be assigned when a client will be authorized through the backend for different logins from one physical machine.
Tech used:
http://www.django-rest-framework.org
Exceptions: http://www.django-rest-framework.org/api-guide/exceptions/
Included rest_framework default example in custom exceptions.py file:
from rest_framework.views import exception_handler
import sys
def custom_exception_handler(exc, context=None):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['request'] = request
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
This sends basic error info like "Http404" etc, but no request data, like ip address, etc.
Best way to add my request into the response? Thanks in advance.
UPDATE (and solved):
So, I was initially trying to solve this using DjangoRestFramework 2.4.x, but that version doesn't have the request or context data options for the custom exception handler. Upgrading to 3.1.3 made it easy to add the data into the response. New code now looks like this (using version 3.1.3):
def custom_exception_handler(exc, request):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, request)
# Send error to rollbar
rollbar.report_exc_info(sys.exc_info(), request)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
This should work for your case.
from rest_framework.views import exception_handler
import sys
def custom_exception_handler(exc, context=None):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['request'] = context['request']
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
You can access the request from the context passed to the custom_exception_handler. This was added in DRF 3.1.0. Also refer this issue where it was resolved.
If you are using DRF<3.1, there would be no request in the context of exception handler. You can upgrade to DRF 3.1.3(latest version in PyPI) and then easily access the request in context.
Taken from DRF 3.1.1 source code:
def get_exception_handler_context(self):
"""
Returns a dict that is passed through to EXCEPTION_HANDLER,
as the `context` argument.
"""
return {
'view': self,
'args': getattr(self, 'args', ()),
'kwargs': getattr(self, 'kwargs', {}),
'request': getattr(self, 'request', None)
}
Also, you need to configure the exception handler in your settings.py file.
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
If it is not specified, the 'EXCEPTION_HANDLER' setting defaults to the standard exception handler provided by REST framework:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
}
Note:
Exception handler will only be called for responses generated by
raised exceptions. It will not be used for any responses returned
directly by the view, such as the HTTP_400_BAD_REQUEST responses that
are returned by the generic views when serializer validation fails.
I use django, django rest framework and ember.js; my entire application thereforce communicates via ajax.
Authentication is done via oauth2 and a token is send in the headers within every request.
Everythings nice and shiny but file downloads.
At one point users can download a pdf and I don't know how to apply authentication there - because on the file download I cannot send and headers, it's just a link.
I thought of adding SessionAuthentication to that particular rest api call, but the session always flags the incoming user as anyonymous.
How can I force django to create a session on top of the oauth2 token flow?
I tried login(request, user), but it somehow does not kick in.
I ended up with signed tickets, e.g. i send back a token, that is able to bypass auth for a defined timeframe. Therefore the ajax app can first request the token and then fire again a standard get request with the token attached.
Here's the basic idea, that I mixin to views:
class DownloadableMixin():
"""
Manages a ticket response, where a ticket is a signed response that gives a user limited access to a resource
for a time frame of 5 secs.
Therefore, file downloads can request a ticket for a resource and gets a ticket in the response that he can
use for non-ajax file-downloads.
"""
MAX_AGE = 5
def check_ticket(self, request):
signer = TimestampSigner()
try:
unsigned_ticket = signer.unsign(request.QUERY_PARAMS['ticket'], max_age=self.__class__.MAX_AGE)
except SignatureExpired:
return False
except BadSignature:
return False
if self.get_requested_file_name() == unsigned_ticket:
return True
return False
def get_ticket(self):
signer = TimestampSigner()
return signer.sign(self.get_requested_file_name())
def has_ticket(self, request):
return 'ticket' in request.QUERY_PARAMS
def requires_ticket(self, request):
return 'download' in request.QUERY_PARAMS
def get_requested_file_name(self):
raise NotImplementedError('Extending classes must define the requested file name.')
I'm passing in a csrf_token for every post and xhr request and want to validate the token against the session csrf token. If they don't match, I throw a 401.
I've used the NewResponse subscriber in pyramid to inspect the request and validate the csrf token in the request params against the token in the session. The validation works but it still calls the view so it def does not work as it should.
Any suggestions on the proper way to do this?
#subscriber(NewResponse)
def new_response(event):
"""Check the csrf_token if the user is authenticated and the
request is a post or xhr req.
"""
request = event.request
response = event.response
user = getattr(request, 'user', None)
# For now all xhr request are csrf protected.
if (user and user.is_authenticated()) and \
(request.method == "POST" or request.is_xhr) and \
(not request.params.get('csrf_token') or \
request.params.get('csrf_token') != unicode(request.session.get_csrf_token())):
response.status = '401 Unauthorized'
response.app_iter = []
The NewResponse subscriber is called after your view is invoked.
You want to be using an event that is invoked earlier, for example NewRequest or ContextFound. In Pyramid 1.0, you'll need to use ContextFound to properly handle things because you cannot raise exceptions in NewRequest events (this is fixed in 1.1).
The way to do this with a ContextFound event is to register an exception view for HTTPException objects like this:
config.add_view(lambda ctx, req: ctx, 'pyramid.httpexceptions.HTTPException')
Basically this will return the exception as the response object when you raise it, which is perfectly valid for HTTPException objects which are valid Pyramid Response objects.
You can then register your event and deal with the CSRF validation:
#subscriber(ContextFound)
def csrf_validation_event(event):
request = event.request
user = getattr(request, 'user', None)
csrf = request.params.get('csrf_token')
if (request.method == 'POST' or request.is_xhr) and \
(user and user.is_authenticated()) and \
(csrf != unicode(request.session.get_csrf_token())):
raise HTTPUnauthorized
Pyramid contains its own CSRF validation, which is probably a better choice.
Given your session stored CSRF tokens, this would result in the following configuration:
from pyramid.csrf import SessionCSRFStoragePolicy
def includeme(config):
# ...
config.set_csrf_storage_policy(SessionCSRFStoragePolicy())
config.set_default_csrf_options(require_csrf=True)