I am trying to do authorization via access token in cookie. But i am having trouble setting cookies with react.
I set cookies in login:
class ApiLoginView(APIView):
permission_classes = [AllowAny]
def post(self, request, ):
password = request.data.get("password")
email = request.data.get("email")
user = authenticate(username=email, password=password)
if user:
try:
user.auth_token.delete()
except Exception as e:
pass
Token.objects.create(user=user)
response = Response()
response.set_cookie(key='access_token', value=user.auth_token.key, httponly=True)
response.data = {"result": True, "token": user.auth_token.key}
print(request.COOKIES)
auth.info("user {} login".format(user))
return response
else:
return JsonResponse({"error": "Wrong Credentials"}, status=status.HTTP_400_BAD_REQUEST)
If I auth into postman, everything goes well and the cookies are set.
print(request.COOKIES)
{'csrftoken': 'JZ1OOBZ0Ilxwo8Zt7DR0SbQ8MUMyNjiPhKYOIUQqY3OeXBEheeUoIa9MSI5S0HXG', 'access_token': 'd67ab794f8752ef02bcba5418bef2c6f87cb74f2'}
But if you do it through the frontend, I get only this
{'_ym_uid': '1612967974591822622', '_ym_d': '1614006098'}
My frontend request:
const response = await fetch("httpS://blablabla/api/auth/login", {
method: "POST",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
});
I also have cors headers configured CORS_ALLOW_CREDENTIALS = True
I understood what was the matter, cookies do not work on localhost in chrome
Related
Here is the REST authentication class:
def get_authorization_header(request):
raw_token = request.COOKIES.get('auth_token', ) or None
auth = request.META.get('HTTP_AUTHORIZATION', )
if isinstance(auth, str):
auth = auth.encode(HTTP_HEADER_ENCODING)
return auth
class JWTAuthentication(BaseAuthentication):
keyword = 'auth_token'
def authenticate(self, request):
raw_token = request.COOKIES.get('auth_token', ) or None
if raw_token is None:
return None
return self.authenticate_credentials(raw_token)
def authenticate_credentials(self, key):
try:
user_model = get_user_model()
payload = jwt.decode(key, settings.SECRET_KEY, algorithms="HS256")
user = user_model.objects.get(email=payload['email'])
except (jwt.DecodeError, user_model.DoesNotExist):
raise exceptions.ParseError('Invalid token')
except jwt.ExpiredSignatureError:
raise exceptions.ParseError('Token has expired')
if not user.is_active:
raise exceptions.AuthenticationFailed('User inactive or deleted')
return (user, payload)
def authenticate_header(self, request):
return self.keyword
And here is the view:
class GoogleLogin(APIView):
permission_classes = [AllowAny]
def post(self, request):
data = request.data
response = Response()
token = data.get('tokenId', None)
if not token:
raise exceptions.AuthenticationFailed('No credentials provided.')
try:
token_info = id_token.verify_oauth2_token(token, requests.Request(), google_app_id)
email = token_info['email']
user = authenticate(email)
if not user:
serializer = RegisterSerializer(data={'email': token_info['email'], 'first_name': token_info['given_name'], 'last_name': token_info['family_name']})
serializer.is_valid(raise_exception=True)
serializer.save()
jwt_token = gen_token(email)
response.set_cookie(
key='auth_token',
value=jwt_token,
expires=datetime.datetime.utcnow() + datetime.timedelta(days=30),
secure=False,
httponly=True,
samesite='Lax'
)
return response
except ValueError:
return Response('Invalid TokenId.', status=status.HTTP_400_BAD_REQUEST)
I'm trying to implement Google social login where the frontend (ReactJS) sends tokenId to the backend (Django) to verify the token then returns a response with a JWT token stored in the cookies (cookie name is auth_token) as shown by this line response.set_cookie now when I try to login while I don't have the authentication token (I don't have auth_token in my cookies) everything works fine but when I try to login while I have an expired authentication token I get 'Token has expired' message even though I set the permission class to AllowAny
I think I implemented my authentication class wrong by I can't figure out where the issue is
AllowAny comes after authentication for checking registered or anonymous user has permission. If auth fail, request is blocked before permissions.
On your code, if there is no token, anonymous user can access login view. Auth accepts request.
There is no way for ExpiredToken to pass auth, because you raise exception.
Actually, your code does the right thing. If token is expired, front should delete invalid token, redirect to login page and make user push login button again without COOKIE.
I am having an issue with the SetCookie which I can't get it to work. I've seen too many SO topics but no success so far. Here is my situation:
I have an Ajax POST call which sends a token from my frontend (JavaScript) to my backend (Flask). This Ajax call send the token to the route /sessionLogin:
#app.route('/sessionLogin', methods=['POST'])
def session_login():
id_token = request.form['idToken']
csrfToken = request.form['csrfToken']
expires_in = datetime.timedelta(minutes=120)
expires = datetime.datetime.now() + expires_in
session_cookie = auth.create_session_cookie(id_token, expires_in=expires_in)
response = make_response(redirect('/home'))
response.set_cookie(
'session', session_cookie, expires=expires, httponly=True, secure=True)
return response
Then, on any 'protected route' I am basically getting the cookie 'session':
#app.route('/home', methods=['GET', 'POST'])
def home():
session_cookie = request.cookies.get('session')
# session_cookie always returning None
return render_template("index.html")
The problem is that when I sign in, get the token (from JS) and make the POST request, the request to /sessionLogin works great and I see the cookie 'session' in the response but any subsequent requests and by the way the redirect to /home does not include the cookie.
What I am missing here?
UPDATE (adding Ajax code):
firebase.auth().signInWithEmailAndPassword(email, pass)
.then(({user}) => {
return user.getIdToken().then((idToken) => {
console.log(user)
if (idToken) {
const csrfToken = getCookie('csrfToken')
return postIdTokenToSessionLogin('/sessionLogin', idToken, csrfToken)
.then(function() {
console.log('logged in.')
}, function(error) {
window.location.assign('/login');
})
}
})
})
const postIdTokenToSessionLogin = function(url, idToken, csrfToken) {
// POST to session login endpoint.
return $.ajax({
type:'POST',
url: url,
data: {idToken: idToken, csrfToken: csrfToken},
contentType: 'application/x-www-form-urlencoded'
});
};
I have a basic Django server with an python command line client for posting to this service. I have a login function and a post function. Even though the cookie for CSRF is being set from the login function the server is saying forbidden when I try to access the post_product endpoint after logging in.
I've been troubleshooting this for days and haven't had any luck.
/api/login/ function:
from django.views.decorators.csrf import csrf_exempt
from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse
#csrf_exempt
def handle_login(request):
if request.method == 'POST':
username = request.POST.get('username')
password = request.POST.get('password')
user = authenticate(request, username=username, password=password)
if user is not None:
if user.is_active:
login(request, user)
if user.is_authenticated:
return HttpResponse(author.name + ' is logged in, Welcome!', status=201, content_type='text/plain')
return HttpResponse(data)
else:
return HttpResponse('disabled account', status=400, content_type='text/plain')
else:
return HttpResponse('invalid login', status=400, content_type='text/plain')
else:
return HttpResponse('request method invalid ' + request.method, status=400, content_type='text/plain')
/api/postproduct/ function:
def post_story(request):
if request.method == 'POST' and request.user.is_authenticated:
# Pull product details from request.
# Validate product details.
# Create model and save.
Python terminal client
FAILURE_MESSAGE = "The server responded with an unsuccessful code: "
def run():
url ='http://127.0.0.1:8000' # For debugging
logged_in = false
with requests.session() as session:
while True:
command = input("""Enter one of the following commands:
login
post \n""")
# login case.
if command == "login":
url = user_inputs[1]
logged_in = login(session, url)
continue
# post case.
elif command == "post" and logged_in:
post(session, url)
continue
else:
print('incorrect command')
continue
def login(session, url):
username = input("Enter your username: \n")
password = input("Enter your password: \n")
response = session.post(url + "/api/login/", data={'username': username, 'password': password})
# If any response but success notify user.
if response.status_code != 201:
print(FAILURE_MESSAGE + str(response.status_code))
return False
else:
print("Successfully logged in!")
return True
def post(session, url):
# Check session is authenticated
if 'csrftoken' not in session.cookies:
print("Not authenticated, have you logged in to a service?")
return
# Omitted : prompt user for productname, category, price and details.
data = {'productname': productname, 'category': category, 'price': price, 'details': details}
data_json = json.dumps(data)
payload = {'json_payload': data_json}
if not session_is_active():
print("You aren't logged into any services")
return
else:
response = session.post(url + "/api/postproduct/", data=payload)
print(6)
if response.status_code != 201:
print(FAILURE_MESSAGE + str(response.status_code))
return
print("Post was successful")
When I run the client, login works fine and on inspection does set the csrf cookie. However when I then try and post the server responds with 403 forbidden. From the server's output:
[15/Aug/2019 15:45:23] "POST /api/login/ HTTP/1.1" 201 42
Forbidden (CSRF token missing or incorrect.): /api/postproduct/
Django's CSRF protection requires that you post the CSRF cookie and a token hidden in a form field. For AJAX requests, you can set a header instead of the form field.
Try something like the following (untested):
headers = {'X-CSRFToken': session.cookies['csrftoken']}
response = session.post(url + "/api/postproduct/", data=payload, headers=headers)
Error = csrf Forbidden (CSRF token missing or incorrect.) when submitting a request to a Django backend:
in a form, include {% csrf_token %}, which generates an input tag with the csrf token value,
and in the request, have the headers include ‘X-CSRFTOKEN
headers: {
content_type: 'application/json',
'X-CSRFToken': "{{ csrf_token }}"
},
I'm using Django with Allauth + REST-Auth for SPA social Login and successfully set up Facebook, VK and Google authorization but faced a problem while adding Twitter. It ends up with {"code":89,"message":"Invalid or expired token."}
Looks like i'm missing something 'cos standard login with Twitter works as it should
Here are my tries:
First of all, i've set Twitter login endpoint as described in doc:
class TwitterLogin(SocialLoginView):
serializer_class = TwitterLoginSerializer
adapter_class = CustomTwitterOAuthAdapter
It features post method, expecting access_token and token_secret
So redirect view was created to receive redirect from twitter, complete login and set inner django token to browser localStorage via template render (with couple of JS lines):
class TwitterReceiveView(APIView):
def get(self, request, *args, **kwargs):
access_token = request.query_params.get('oauth_token')
token_secret = request.query_params.get('oauth_verifier')
params = {'access_token': access_token,
'token_secret': token_secret}
try:
result = requests.post(settings.DOMAIN + reverse('tw_login'), data=params).text
result = json.loads(result)
except (requests.HTTPError, json.decoder.JSONDecodeError):
result = {}
access_token = result.get('access_token')
context = {'access_token': access_token}
return render(request, 'account/local_storage_setter.html',
context, content_type='text/html')
Have to mention that I tried two methods to start process(get initial token)
1. Used standard allauth url http://0.0.0.0:8080/accounts/twitter/login
2. Created another view (using lib python oauth2) which could be used from SPA:
class TwitterGetToken(APIView):
def get(self, request, *args, **kwargs):
request_token_url = 'https://api.twitter.com/oauth/request_token'
authorize_url = 'https://api.twitter.com/oauth/authorize'
app = SocialApp.objects.filter(name='Twitter').first()
if app and app.client_id and app.secret:
consumer = oauth.Consumer(app.client_id, app.secret)
client = oauth.Client(consumer)
resp, content = client.request(request_token_url, "GET")
if resp['status'] != '200':
raise Exception("Invalid response {}".format(resp['status']))
request_token = dict(urllib.parse.parse_qsl(content.decode("utf-8")))
twitter_authorize_url = "{0}?oauth_token={1}"\
.format(authorize_url, request_token['oauth_token'])
return redirect(twitter_authorize_url)
raise Exception("Twitter app is not set up")
I even tried to write get method for FacebookLoginView and pass twitter callback to it directly
class TwitterLogin(SocialLoginView):
serializer_class = TwitterLoginSerializer
adapter_class = TwitterOAuthAdapter
def get(self, request, *args, **kwargs):
data = {
'access_token': request.query_params.get('oauth_token'),
'token_secret': request.query_params.get('oauth_verifier')
}
self.request = request
self.serializer = self.get_serializer(data=data,
context={'request': request})
self.serializer.is_valid(raise_exception=True)
self.login()
return self.get_response()
All methods led me to mentioned error. Could you, please, advise something in my case. Thank you in advance!
UPDATE:
Here is how it's worked for me:
import json
import requests
import urllib.parse
import oauth2 as oauth
from requests_oauthlib import OAuth1Session
from django.urls import reverse
from django.conf import settings
from django.shortcuts import redirect, render
from rest_framework.views import APIView
from allauth.socialaccount.models import SocialApp
from allauth.socialaccount.providers.twitter.views import TwitterOAuthAdapter, TwitterAPI
from rest_auth.social_serializers import TwitterLoginSerializer
from rest_auth.registration.views import SocialLoginView
class TwitterGetToken(APIView):
'''
Initiates Twitter login process
Requests initial token and redirects user to Twitter
'''
def get(self, request, *args, **kwargs):
request_token_url = 'https://api.twitter.com/oauth/request_token'
authorize_url = 'https://api.twitter.com/oauth/authorize'
app = SocialApp.objects.filter(name='Twitter').first()
if app and app.client_id and app.secret:
consumer = oauth.Consumer(app.client_id, app.secret)
client = oauth.Client(consumer)
resp, content = client.request(request_token_url, "GET")
if resp['status'] != '200':
raise Exception("Invalid response {}".format(resp['status']))
request_token = dict(urllib.parse.parse_qsl(content.decode("utf-8")))
twitter_authorize_url = "{0}?oauth_token={1}"\
.format(authorize_url, request_token['oauth_token'])
return redirect(twitter_authorize_url)
raise Exception("Twitter app is not set up")
class TwitterLogin(SocialLoginView):
'''
Takes the final twitter access token, secret
Returns inner django Token
'''
serializer_class = TwitterLoginSerializer
adapter_class = TwitterOAuthAdapter
class TwitterReceiveView(APIView):
'''
Receives Twitter redirect,
Requests access token
Uses TwitterLogin to logn and get django Token
Renders template with JS code which sets django Token to localStorage and redirects to SPA login page
'''
def get(self, request, *args, **kwargs):
access_token_url = 'https://api.twitter.com/oauth/access_token'
callback_uri = settings.DOMAIN + '/accounts/twitter/login/callback/'
app = SocialApp.objects.filter(name='Twitter').first()
client_key = app.client_id
client_secret = app.secret
oauth_session = OAuth1Session(client_key,
client_secret=client_secret,
callback_uri=callback_uri)
redirect_response = request.get_full_path()
oauth_session.parse_authorization_response(redirect_response)
token = oauth_session.fetch_access_token(access_token_url)
params = {'access_token': token['oauth_token'],
'token_secret': token['oauth_token_secret']}
try:
result = requests.post(settings.DOMAIN + reverse('tw_login'),
data=params).text
result = json.loads(result)
except (requests.HTTPError, json.decoder.JSONDecodeError):
result = {}
access_token = result.get('access_token')
context = {'access_token': access_token,
'domain': settings.DOMAIN}
return render(request, 'account/local_storage_setter.html',
context, content_type='text/html')
Great code, Thank you for posting!
I'd like to add however that user authentication can be done directly from the front end, and given you're writing an SPA it seems logical to do so, instead of redirect your in your back-end (which kind of breaks the notion of a RESTful) to auth and then send a response.
I used the following JS helper class based of vue-authenticate. To make a popup and get info from the callback url
export default class OAuthPopup {
constructor(url, name, redirectURI) {
this.popup = null
this.url = url
this.name = name
this.redirectURI = redirectURI
}
open() {
try {
this.popup = window.open(this.url, this.name)
if (this.popup && this.popup.focus) {
this.popup.focus()
}
return this.pooling()
} catch(e) {
console.log(e)
}
}
pooling() {
return new Promise((resolve, reject) => {
let poolingInterval = setInterval(() => {
if (!this.popup || this.popup.closed || this.popup.closed === undefined) {
clearInterval(poolingInterval)
poolingInterval = null
reject(new Error('Auth popup window closed'))
}
try {
var popupLocation = this.popup.location.origin + this.popup.location.pathname
if (popupLocation == this.redirectURI) {
if (this.popup.location.search || this.popup.location.hash ) {
const urlParams = new URLSearchParams(this.popup.location.search);
var params = {
oauth_token: urlParams.get('oauth_token'),
oauth_verifier: urlParams.get('oauth_verifier'),
url: this.popup.location.href
}
if (params.error) {
reject(new Error(params.error));
} else {
resolve(params);
}
} else {
reject(new Error('OAuth redirect has occurred but no query or hash parameters were found.'))
}
clearInterval(poolingInterval)
poolingInterval = null
this.popup.close()
}
} catch(e) {
// Ignore DOMException: Blocked a frame with origin from accessing a cross-origin frame.
}
}, 250)
})
}
}
The methodology I followed is similar to yours however:
I make a GET request to TwitterGetToken and get the Twitter auth url back as a response
I use the url in the response from my front end to open a popup which the allows the user to auth
I make a POST request to TwitterReceiveView and attach the response url after twitter auth
everything else the just falls into place and the user is returned a access key.
In any case,
Thanks I messed around with loads of libraries in js and python but this was just the best way to do things
I'm having problems to do test on Django. I've been reading the documentation of the responses and I can't do the same as they explain on the documentation.
When I get the response, I only have access to response.status_code and can't access to context or redirect_chain when I write response.(and now PyCharm shows all available options).
I've checked on settings.py and I've 'BACKEND': 'django.template.backends.django.DjangoTemplates' to be sure that I'm using Django templates so I don't know why don't work the test. I need configure something?
The code of the test I'm trying to do it's:
from django.test import TestCase
from django.test.client import Client
class Test(TestCase):
def testLogin(self):
client = Client()
headers = {'X-OpenAM-Username': 'user', 'X-OpenAM-Password': 'password', 'Content-Type': 'application/json'}
data = {}
response = self.client.post('/login/', headers=headers, data=data, secure=True, follow=True)
assert (response.status_code == 200)
# self.assertRedirects(response, '/menu/', status_code=301, target_status_code=200)
I'm not using Django authentication, the login form sends the data to an IDP and if the IDP sends with a correct answer, the "login" it's successful:
def login(request):
logout(request)
message = None
if request.method == "POST":
form = LoginForm(request.POST)
if form.is_valid():
username = request.POST['username']
password = request.POST['password']
headers = {'X-OpenAM-Username': username, 'X-OpenAM-Password': password, 'Content-Type': 'application/json'}
data = {}
req = requests.post('http://openam.idp.com:8090/openamIDP/json/authenticate', headers=headers, params=data)
if req.status_code == 200:
respJson = json.loads(req.content)
tokenIdJson = respJson['tokenId']
request.session['tokenId'] = tokenIdJson
return render_to_response('menu/menu.html', request)
elif req.status_code == 401:
message = "Invalid username and/or password. Please, try again"
else:
form = LoginForm()
return render_to_response('registration/login.html', {'message': message, 'form': form},
context_instance=RequestContext(request))
The redirect assert it's commented because now it fails, when I do the debug I see an empty redirect_chain. I don't understand why happens this because running the web everything works, all views redirect as expected.
Why I only can check status_code? I'm doing something wrong when I redirect after a successful login that on a normal use it works but on the test not?
Thanks.
The remote authentication url expects the credentials as headers, but your local login view expects them as POST data. Your test passes the credentials as headers to your local view.
As a result, the form is passed an empty dictionary (request.POST contains no actual data), and the form is invalid. You get an empty form as a response, without any redirects.
You have to simply pass the credentials as post data to your local view:
def testLogin(self):
client = Client()
data = {'username': 'user', 'password': 'password'}
response = self.client.post('/login/', data=data, secure=True, follow=True)
assert (response.status_code == 200)
self.assertRedirects(response, '/menu/', status_code=301, target_status_code=200)