I am a bit confused on the use of the HTTPOnly cookie. When my user logs in it assigns the cookie and I see it in the response headers:
res = make_response({"status":"success","data":ret_rec})
res.set_cookie(key="PPS-Token",value=token,secure=True,httponly=True)
return res
In the response header:
Set-Cookie: PPS-Token=mylongtoken; Secure; HttpOnly; Path=/
Now I am unsure on how to validate the token once the user logs in:
def check_token(f):
#wraps(f)
def decorator(*args, **kwargs):
token = request.cookies.get("PPS-Token")
if not token:
build_response(status="error", message="Invalid Token...Contact Support")
...
How does the token persists when the user logs in? How do I verify the token on subsequent server requests>
The cookie cannot be accessed by the client when the HTTPOnly flag is set and it doesn't need to. It's usually set on the backends domain and will be send automatically by the web browser. So, whenever a request is being processed, you can execute some validation logic (i.e authentication or authorization). This is usually done in a decorator like that:
from functools import wraps
from flask import jsonify, current_app
def validate_something():
def wrapper(fn):
#wraps(fn)
def decorated_view(*args, **kwargs):
verify_token_somehow()
if not something:
return jsonify(msg="Permitted!"), 403
return current_app.ensure_sync(fn)(*args, **kwargs)
return decorated_view
return wrapper
Depending on how the cookie is set (session cookie or persistent cookie), it will be persistent until it gets invalid or until the session is closed.
Related
I have stored the SimpleJWT access and refresh tokens in HttpOnly cookies. Now, how do I reproduce an access token from the refresh token once it expires?
In React I used setTimeout() to request for new access token and it worked just fine. Now that I'm storing these as HttpOnly cookies, how do I check whether the access is valid or not and if not, create and send a new access token as HttpOnly cookie?
I have this middleware where I add the tokens from cookie to Authorization middleware, is it possible to renew to token in any such middleware:
class AuthorizationHeaderMiddleware:
def __init__(self, get_response=None):
self.get_response = get_response
def process_view(self, request, view_func, view_args, view_kwargs):
view_name = '.'.join((view_func.__module__, view_func.__name__))
#print(view_name)
if view_name in EXCLUDE_FROM_MIDDLEWARE:
return None
def __call__(self, request):
access_token = request.COOKIES.get('access')
if access_token:
request.META['HTTP_AUTHORIZATION'] = f'Bearer {access_token}'
return self.get_response(request)
I'm assuming you get and refresh the access token on the backend. Let me know if this doesn't fit your use case.
You can store the expiration time on the cookies once you generate access tokens. On your __call__ method, check if it's past the expiration time. If so, generate a new token and set that on the cookies along with the new expiration time.
In my Flask API, I'm trying to get sessions to stay permanent, so that it will still be there even after browser closes. My front-end is written in React, and uses the Fetch API to make requests. However, after testing out what I have so far, it doesn't seem to work. My code (left out some irrelevant database stuff that works):
#bp.route('/login', methods=('POST',))
def login():
...
error=None
res = {}
db = get_db()
cursor = db.cursor(dictionary=True)
...
user = cursor.fetchone()
...
if error is None:
session.clear()
session.permanent = True
session['userID'] = user['id']
current_app.logger.debug(session['userID'])
res['actionSuccess']= True
else:
res['actionSuccess'] = False
res['error'] = error
return jsonify(res)
So far, I can tell that sessions is indeed storing the userID value. I then write another route to tell me if an userID value is stored in session, like so:
#bp.route('/init', methods=('GET',))
def init():
userID = session.get('userID')
if userID is None:
res = {"signedIn": False}
else:
res = {"signedIn": True, "username": userID}
return jsonify(res)
However, everytime I make the call to '/init', it returns False even though I previously signed in. I don't know why session isn't permanent here. Is it because I'm running the client locally on my machine? Do I need to allow cookies somewhere on my Chrome browser? I used Chrome to look into the cookies stored for the client, and no "sessions" was stored there. Do I have to something extra on the front-end to store the cookies/session, or do they store automatically? Am I misunderstanding the usage of Flask sessions?
Found out why sessions wasn't working after a lot of research! Flask sessions are essentially cookies, and I was using the Fetch API to perform CORS operations. Fetch() by default does not allow cookies to be received or sent, and must be configured in order to use Flask sessions.
On my React.js client, I did this by setting 'include' for 'credentials':
fetch(url, {
method: 'POST',
mode: 'cors',
body: JSON.stringify(loginObj),
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
})
.then(res => res.json())
...
Because of this configuration, the request isn't considered a "simple request", and the client will actually "preflight" the POST request with an OPTIONS request. This means that before my POST request is sent, a OPTIONS request testing to see if the server has the correct access will be sent to my Flask server first.
The preflight OPTIONS request will test to see if the response from the server has the correct headers containing "Access-Control-Allow-Origin", 'Access-Control-Allow-Credentials', and 'Access-Control-Allow-Headers'. If the test sent by the OPTIONS request fails, the actual POST request will not be sent and you'll get a Fetch error.
I then set the headers accordingly on my Flask server like so:
#bp.route('/login', methods=('POST','OPTIONS'))
def login():
if request.method == 'OPTIONS':
resp = Response()
resp.headers['Access-Control-Allow-Origin'] = clientUrl
resp.headers['Access-Control-Allow-Credentials'] = 'true'
resp.headers['Access-Control-Allow-Headers'] = "Content-Type"
return resp
else:
'''
use session for something
'''
res['actionSuccess'] = False
js = json.dumps(res)
resp = Response(js, status=200, mimetype='application/json')
resp.headers['Access-Control-Allow-Origin'] = clientUrl
resp.headers['Access-Control-Allow-Credentials'] = 'true'
resp.headers['Access-Control-Allow-Headers'] = "Content-Type"
return resp
Take note that 'Access-Control-Allow-Credentials' was set to 'true' as opposed to the Python boolean True, as the client will not recognize the Python boolean.
And with that, a Flask Session object should be stored in your cookies.
I'm trying to implement an integration test for the password reset flow, but I'm stuck at the "password_reset_confirm" view. I already tested the flow manually, and it works fine. Unfortunately, the Django unit test client seems unable to follow correctly the redirects required in this view.
urls config
from django.contrib.auth import views as auth_views
url(r"^accounts/password_change/$",
auth_views.PasswordChangeView.as_view(),
name="password_change"),
url(r"^accounts/password_change/done/$",
auth_views.PasswordChangeDoneView.as_view(),
name="password_change_done"),
url(r"^accounts/password_reset/$",
auth_views.PasswordResetView.as_view(email_template_name="app/email/accounts/password_reset_email.html",
success_url=reverse_lazy("app:password_reset_done"),
subject_template_name="app/email/accounts/password_reset_subject.html"),
name="password_reset"),
url(r"^accounts/password_reset/done/$",
auth_views.PasswordResetDoneView.as_view(),
name="password_reset_done"),
url(r"^accounts/reset/(?P<uidb64>[0-9A-Za-z_\-]+)/(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$",
auth_views.PasswordResetConfirmView.as_view(
success_url=reverse_lazy("app:password_reset_complete"),
form_class=CustomSetPasswordForm),
name="password_reset_confirm"),
url(r"^accounts/reset/complete/$",
auth_views.PasswordResetCompleteView.as_view(),
name="password_reset_complete"),
Test code
import re
from django.urls import reverse, NoReverseMatch
from django.test import TestCase, Client
from django.core import mail
from django.test.utils import override_settings
from django.contrib.auth import authenticate
VALID_USER_NAME = "username"
USER_OLD_PSW = "oldpassword"
USER_NEW_PSW = "newpassword"
PASSWORD_RESET_URL = reverse("app:password_reset")
def PASSWORD_RESET_CONFIRM_URL(uidb64, token):
try:
return reverse("app:password_reset_confirm", args=(uidb64, token))
except NoReverseMatch:
return f"/accounts/reset/invaliduidb64/invalid-token/"
def utils_extract_reset_tokens(full_url):
return re.findall(r"/([\w\-]+)",
re.search(r"^http\://.+$", full_url, flags=re.MULTILINE)[0])[3:5]
#override_settings(EMAIL_BACKEND="anymail.backends.test.EmailBackend")
class PasswordResetTestCase(TestCase):
#classmethod
def setUpClass(cls):
super().setUpClass()
cls.myclient = Client()
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, token),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))
Now, the assert fails: the user is authenticated with the old password. From the log I'm able to detect that the change password is not executed.
A few extra useful information:
post returns a successful HTTP 200;
the response.redirect_chain is [('/accounts/reset/token_removed/set-password/', 302)] and I think this is wrong, as it should have another loop (in the manual case I see another call to the dispatch method);
I'm executing the test with the Django unit test tools.
Any idea on how to properly test this scenario? I need this to make sure emails and logging are properly executed (and never removed).
Many thanks!
EDIT: solution
As well explained by the accepted solution, here the working code for the test case:
def test_password_reset_ok(self):
# ask for password reset
response = self.myclient.post(PASSWORD_RESET_URL,
{"email": VALID_USER_NAME},
follow=True)
# extract reset token from email
self.assertEqual(len(mail.outbox), 1)
msg = mail.outbox[0]
uidb64, token = utils_extract_reset_tokens(msg.body)
# change the password
self.myclient.get(PASSWORD_RESET_CONFIRM_URL(uidb64, token), follow=True)
response = self.myclient.post(PASSWORD_RESET_CONFIRM_URL(uidb64, "set-password"),
{"new_password1": USER_NEW_PSW,
"new_password2": USER_NEW_PSW},
follow=True)
self.assertIsNone(authenticate(username=VALID_USER_NAME,password=USER_OLD_PSW))
This is very interesting; so it looks like Django has implemented a security feature in the password reset page to prevent the token from being leaked in the HTTP Referrer header. Read more about Referrer Header Leaks here.
TL;DR
Django is basically taking the sensitive token from the URL and placing it in Session and performing a internal redirect (same domain) to prevent you from clicking away to a different site and leaking the token via the Referer header.
Here's how:
When you hit /accounts/reset/uidb64/token/ (you should be doing a GET here, however you are doing a POST in your test case) the first time, Django pulls the token from the URL and sets it in session and redirects you to /accounts/reset/uidb64/set-password/.
This now loads the /accounts/reset/uidb64/set-password/ page, where you can set the passwords and perform a POST
When you POST from this page, the same View handles your POST request since the token URL param can handle both the token and the string set-password.
This time though, the view sees that you have accessed it with set-password and not a token, so it expects to pull your actual token from session, and then change the password.
Here's the flow as a chart:
GET /reset/uidb64/token/ --> Set token in session --> 302 Redirect to /reset/uidb64/set-token/ --> POST Password --> Get token from Session --> Token Valid? --> Reset password
Here's the code!
INTERNAL_RESET_URL_TOKEN = 'set-password'
INTERNAL_RESET_SESSION_TOKEN = '_password_reset_token'
#method_decorator(sensitive_post_parameters())
#method_decorator(never_cache)
def dispatch(self, *args, **kwargs):
assert 'uidb64' in kwargs and 'token' in kwargs
self.validlink = False
self.user = self.get_user(kwargs['uidb64'])
if self.user is not None:
token = kwargs['token']
if token == INTERNAL_RESET_URL_TOKEN:
session_token = self.request.session.get(INTERNAL_RESET_SESSION_TOKEN)
if self.token_generator.check_token(self.user, session_token):
# If the token is valid, display the password reset form.
self.validlink = True
return super().dispatch(*args, **kwargs)
else:
if self.token_generator.check_token(self.user, token):
# Store the token in the session and redirect to the
# password reset form at a URL without the token. That
# avoids the possibility of leaking the token in the
# HTTP Referer header.
self.request.session[INTERNAL_RESET_SESSION_TOKEN] = token
redirect_url = self.request.path.replace(token, INTERNAL_RESET_URL_TOKEN)
return HttpResponseRedirect(redirect_url)
# Display the "Password reset unsuccessful" page.
return self.render_to_response(self.get_context_data())
Notice the comment in the code where this magic happens:
Store the token in the session and redirect to the
password reset form at a URL without the token. That
avoids the possibility of leaking the token in the
HTTP Referer header.
I think this makes it clear how you can fix your unit test; do a GET on the PASSWORD_RESET_URL which will give you the redirect URL, you can then POST to this redirect_url and perform password resets!
I've constructed a APIView as such:
class CustomAPIView(APIView):
def get(self, request, *args, **kwargs):
if not request.user or not request.user.is_authenticated():
return Response("User not logged in", status=status.HTTP_403_FORBIDDEN)
# Other stuff
And in my html template I'm making a call to it using fetchAPI:
fetch('/api/request/url/', {method: 'get'})
.then(
// Process info );
I'm logged in through all this, but I'm always being greeted with a 403 response with the request.user variable in the APIView returning AnonymousUser. However, if I try and visit the api url manually everything works out right.
Can someone point out what I'm missing?
Thanks in advance.
The issue with fetch api is that by defualt it will not send cookies to the server.
By default, fetch won't send or receive any cookies from the server,
resulting in unauthenticated requests if the site relies on
maintaining a user session (to send cookies, the credentials init
option must be set).
So You have to set credentials: 'same-origin' in your fetch request,
fetch('/api/request/url/', {method: "GET", credentials: 'same-origin'})
.then(
// Process info );
for cross-origin requests, use credentials: 'include'
I have my rest-api set up in Django and am using React Native to connect with it. I have registered users and am able to generate tokens however I am unable to pass the token in the header of the GET request. My code is as follows:
try{
let response = await fetch("http://127.0.0.1:8000/fishes/auth/",
{
method: 'GET',
headers: {
// 'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': ' Token '+accessToken,
}});
let res = await response.text();
}}
I have been following this link http://cheng.logdown.com/posts/2015/10/27/how-to-use-django-rest-frameworks-token-based-authentication and have already verified that the response from the rest api is correct.
However on the phone with native react I get the following error in the console:
TypeError: Network request failed
at XMLHttpRequest.xhr.onerror (fetch.js:441)
at XMLHttpRequest.dispatchEvent (event-target.js:172)
at XMLHttpRequest.setReadyState (XMLHttpRequest.js:542)
What am I doing wrong in the GET code?
Alright 401 status code which means UnAuthorized.
For Django Rest Framework you must pass in the access Token as part of header for all your API requests.
Header Format will be
Key : Authorization
Value: Token <token>
You can see more here
http://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
I think that you need change
'Content-Type' to 'content-type'
On lowercase
See This answer.
The same-origin policy restricts the kinds of requests that a Web page can send to resources from another origin.
In the no-cors mode, the browser is limited to sending “simple” requests — those with safelisted methods and safelisted headers only.
To send a cross-origin request with headers like Authorization and X-My-Custom-Header, you have to drop the no-cors mode and support preflight requests (OPTIONS).
The distinction between “simple” and “non-simple” requests is for historical reasons. Web pages could always perform some cross-origin requests through various means (such as creating and submitting a form), so when Web browsers introduced a principled means of sending cross-origin requests (cross-origin resource sharing, or CORS), it was decided that such “simple” requests could be exempt from the preflight OPTIONS check.
I got around this issue by handling preflight request, as stated by the OP.
Previously, in my middleware, I filtered out requests that did not include an auth token and return 403 if they were trying to access private data. Now, I check for preflight and send a response allowing these types of headers. This way, when the following request comes (get, post, etc), it will have the desired headers and I can use my middleware as originally intended.
Here is my middleware:
class ValidateInflight(MiddlewareMixin):
def process_view(self, request, view_func, view_args, view_kwargs):
assert hasattr(request, 'user')
path = request.path.lstrip('/')
if path not in EXEMPT_URLS:
logger.info(path)
header_token = request.META.get('HTTP_AUTHORIZATION', None)
if header_token is not None:
try:
token = header_token
token_obj = Token.objects.get(token=token)
request.user = token_obj.user
except Token.DoesNotExist:
return HttpResponse(status=403)
elif request.method == 'OPTIONS':
pass
else:
return HttpResponse(status=403)
Here is my Options handling
class BaseView(View):
def options(self, request, *args, **kwargs):
res = super().options(request, *args, **kwargs)
res['Access-Control-Allow-Origin'] = '*'
res['Access-Control-Allow-Headers'] = '*'
return res