I am facing a situation where I am able to set a session with Flask and verify the session exists when visiting the Python endpoints directly. When I make my frontend return the session status, the Python endpoint returns not logged in.
Python:
#app.route("/status")
def status():
try:
session["access_token"]
result = {
"rc": "loggedin",
"msg": f"User is logged in with access token {session['access_token']}."
}
except:
print("No access token found")
result = {
"rc": "notloggedin",
"msg": "User is not logged in."
}
return jsonify(result)
#app.route("/login")
def login():
return redirect(OAUTH_URL)
#app.route("/logout")
def logout():
try:
session.pop("access_token")
print(f"Ended session.")
except:
print("No session to end.")
return redirect(f"https://{HOME_URL}")
#app.route("/oauth/callback")
def oauth_callback():
print(REDIRECT_URI)
code = request.args["code"]
access_token = client.oauth.get_access_token(
code, redirect_uri=REDIRECT_URI
).access_token
session["access_token"] = access_token
Jquery:
$.ajax({
method: "GET",
cache: false,
url: "https://account.mydomain.net/status",
xhrFields: {
withCredentials: true
}
}).done(function( msg ) {
console.log( msg );
});
When calling the Python endpoints directly, it all works. I got to /login, am redirected to the Oauth provider and then returned to my home page. When I then go to /status, it returns:
{"msg":"User is logged in with access token REDACTED.","rc":"loggedin"}
When the Ajax function calls the endpoint (same browser, same URL as the endpoint I am hitting)
{"msg":"User is not logged in.","rc":"notloggedin"}
I saw some similar issues, but none that covered this. I expect my Flask session to stay alive, but it does not. Perhaps I am misunderstanding how this works. Don't mind all the print(), this is mostly for debugging this frustrating issue. The Python endpoint is on account.domain.net and the app calling it is on the apex domain.net. CORS is configured properly, since it is returning a value.
I checked both domains, the session cookie is set the same for both.
I didn't get this to work with Jquery, but native JS fetch:
app.config['SESSION_COOKIE_DOMAIN'] = ".domain.net"
app.config["SESSION_COOKIE_NAME"] = "domain-session"
app.config["REMEMBER_COOKIE_DOMAIN"] = "None"
async function postData(url = '') {
// Default options are marked with *
const response = await fetch(url, {
method: 'GET', // *GET, POST, PUT, DELETE, etc.
mode: 'cors', // no-cors, *cors, same-origin
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
credentials: 'include', // include, *same-origin, omit
headers: {
'Content-Type': 'application/json'
// 'Content-Type': 'application/x-www-form-urlencoded',
},
redirect: 'follow', // manual, *follow, error
referrerPolicy: 'no-referrer', // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
});
return response.json(); // parses JSON response into native JavaScript objects
}
Related
I am currently developing a React-Django App and using JWTs for authentication.
After a little research I found out that storing JWTs in client is not safe(XSS and XSRF) and most of the people advice that I should store them in server-side with HttpOnly cookies but nobody tells how to do it. So can anybody help with that?
I got jwt-cookies as response but it is not saved in the browser.
You can set cookie with set_cookie() method.
For example:
...
response = Response(serializer.data)
response.set_cookie('token', serializer.data['token'], httponly=True)
return response
Good article about where to store JWT (and how to do it) here.
Same problem I faced. Here samesite flag is 'Lax' or 'strict' so cookie blocked by browser. Because cross-site response not set cookie.
So when in development you have to host your backend and frontend under same IP. ex. my backend :
python manage.py runserver localhost:8000
localhost:8000
frontend:
localhost:3000
Different ports same ip.
This is not the scenario when it goes to production you can have any domain.
For more detail.
WithCredentials = true for both side..
Well I was making a silly mistake,
so moving {withCredentials:true} from here =>
export const login = (username, password) => dispatch => {
//Headers
const config = {
headers: {
"Content-type": "application/json"
}
}
//Request body
const body = JSON.stringify({ username, password })
axios.post("http://127.0.0.1:8000/auth/login/", body, config, {withCredentials: true})
.then(res => {
dispatch({
type: LOGIN_SUCCESS,
payload: res.data
})
}).catch(err => {
console.log(err)
dispatch({
type: LOGIN_FAIL
})
})
}
to here =>
//Headers
const config = {
headers: {
"Content-type": "application/json"
},
withCredentials: true
}
solved my problem.
I am trying to get the public token passed into my server (built in python flask). But I keep getting:
BadRequestKeyError: 400 Bad Request: The browser (or proxy) sent a request that this server could not understand.
KeyError: 'public_token'
Here is my frontend written in jQuery:
onSuccess: async function(public_token, metadata) {
// 2a. Send the public_token to your app server.
// The onSuccess function is called when the user has successfully
// authenticated and selected an account to use.
await fetch('/get_access_token', {
method: 'POST',
body: JSON.stringify({ public_token: public_token }),
});
},
And the function that has problems in flask:
#app.route("/get_access_token", methods=['POST'])
def get_access_token():
global access_token
global item_id
public_token = request.form['public_token']
print(public_token)
exchange_response = \
client.Item.public_token.exchange(public_token)
# Store the access_token and item_id in your database
access_token = exchange_response['access_token']
item_id = exchange_response['item_id']
return jsonify(exchange_response)
My frontend (React) and backend (Django) are decoupled, runnning on localhost:3000 and 127.0.0.1:8000 respectively.
Consider the following frontend request:
async function test() {
let token = await getCsrfToken() // fetched from other endpoint
let url = 'http://localhost:8000/test'
let response = await fetch(url, {
method: 'POST',
headers: {
'X-CSRFToken': token,
},
credentials: 'include',
})
let response_text = await response.text()
return response_text
}
test()
to the following endpoint:
def test(request):
return HttpResponse('OK')
It works fine. But if I change:
let url = 'http://localhost:8000/test'
to:
let url = 'http://127.0.0.1:8000/test'
it will fail with:
Forbidden (CSRF cookie not set.): /test
Per my understanding, localhost and 127.0.0.1 are supposed to be synonymous. Why isn't it so in this context?
What confuses me even more is that the Django's development server explicitly runs on 127.0.0.1:8000.
Note: I am using django-cors-headers middleware and the following CORS/CSRF settings:
CORS_ALLOW_CREDENTIALS = True
CORS_ORIGIN_WHITELIST = ['http://localhost:3000']
CSRF_TRUSTED_ORIGINS = ['localhost:3000']
I've got an app on Google App Engine for which I use the webapp2 authentication as described in this tutorial (thus Google Account API is not being used for user account management).
Therefore I'm using this Google tutorial to implement Google+ Sign-In. The front-end works fine, however I am having troubles with the callback. I would like to do this without Flask, since the only thing it seems to be used for is generating a response. The original code for the first part of the callback is:
if request.args.get('state', '') != session['state']:
response = make_response(json.dumps('Invalid state parameter.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
To get rid of the Flask dependency, I rewrote this to:
if self.request.get('state') != self.session.get('state'):
msg = json.dumps('Invalid state parameter.')
self.response.headers["Content-Type"] = 'application/json'
self.response.set_status(401)
return self.response.out.write(msg)
The problem though, is that self.request.get('state') returns nothing. I'm guessing this is because I am not reading the response properly, however I don't know how to do it right.
The Javascript that launches the callback is:
function signInCallback(authResult) {
if (authResult['code']) {
// Send the code to the server
$.ajax({
type: 'POST',
url: '/signup/gauth',
contentType: 'application/octet-stream; charset=utf-8',
success: function(result) {
console.log(result),
processData: false,
data: authResult['code']
});
} else if (authResult['error']) {
// There was an error.
// Possible error codes:
// "access_denied" - User denied access to your app
// "immediate_failed" - Could not automatially log in the user
console.log('There was an error: ' + authResult['error']);
}
}
I am building an app on Google App Engine using Flask. I am implementing Google+ login from the server-side flow described in https://developers.google.com/+/web/signin/server-side-flow. Before switching to App Engine, I had a very similar flow working. Perhaps I have introduced an error since then. Or maybe it is an issue with my implementation in App Engine.
I believe the url redirected to by the Google login flow should have a GET argument set "gplus_id", however, I am not receiving this parameter.
I have a login button created by:
(function() {
var po = document.createElement('script');
po.type = 'text/javascript'; po.async = true;
po.src = 'https://plus.google.com/js/client:plusone.js?onload=render';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(po, s);
})();
function render() {
gapi.signin.render('gplusBtn', {
'callback': 'onSignInCallback',
'clientid': '{{ CLIENT_ID }}',
'cookiepolicy': 'single_host_origin',
'requestvisibleactions': 'http://schemas.google.com/AddActivity',
'scope': 'https://www.googleapis.com/auth/plus.login',
'accesstype': 'offline',
'width': 'iconOnly'
});
}
In the javascript code for the page I have a function to initiate the flow:
var helper = (function() {
var authResult = undefined;
return {
onSignInCallback: function(authResult) {
if (authResult['access_token']) {
// The user is signed in
this.authResult = authResult;
helper.connectServer();
} else if (authResult['error']) {
// There was an error, which means the user is not signed in.
// As an example, you can troubleshoot by writing to the console:
console.log('GPlus: There was an error: ' + authResult['error']);
}
console.log('authResult', authResult);
},
connectServer: function() {
$.ajax({
type: 'POST',
url: window.location.protocol + '//' + window.location.host + '/connect?state={{ STATE }}',
contentType: 'application/octet-stream; charset=utf-8',
success: function(result) {
// After we load the Google+ API, send login data.
gapi.client.load('plus','v1',helper.otherLogin);
},
processData: false,
data: this.authResult.code,
error: function(e) {
console.log("connectServer: error: ", e);
}
});
}
}
})();
/**
* Calls the helper method that handles the authentication flow.
*
* #param {Object} authResult An Object which contains the access token and
* other authentication information.
*/
function onSignInCallback(authResult) {
helper.onSignInCallback(authResult);
}
This initiates the flow at "/connect" (See step 8. referenced in the above doc):
#app.route('/connect', methods=['GET', 'POST'])
def connect():
# Ensure that this is no request forgery going on, and that the user
# sending us this connect request is the user that was supposed to.
if request.args.get('state', '') != session.get('state', ''):
response = make_response(json.dumps('Invalid state parameter.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
# Normally the state would be a one-time use token, however in our
# simple case, we want a user to be able to connect and disconnect
# without reloading the page. Thus, for demonstration, we don't
# implement this best practice.
session.pop('state')
gplus_id = request.args.get('gplus_id')
code = request.data
try:
# Upgrade the authorization code into a credentials object
oauth_flow = client.flow_from_clientsecrets('client_secrets.json', scope='')
oauth_flow.redirect_uri = 'postmessage'
credentials = oauth_flow.step2_exchange(code)
except client.FlowExchangeError:
app.logger.debug("connect: Failed to upgrade the authorization code")
response = make_response(
json.dumps('Failed to upgrade the authorization code.'), 401)
response.headers['Content-Type'] = 'application/json'
return response
# Check that the access token is valid.
access_token = credentials.access_token
url = ('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=%s'
% access_token)
h = httplib2.Http()
result = json.loads(h.request(url, 'GET')[1])
# If there was an error in the access token info, abort.
if result.get('error') is not None:
response = make_response(json.dumps(result.get('error')), 500)
response.headers['Content-Type'] = 'application/json'
return response
# Verify that the access token is used for the intended user.
if result['user_id'] != gplus_id:
response = make_response(
json.dumps("Token's user ID doesn't match given user ID."), 401)
response.headers['Content-Type'] = 'application/json'
return response
...
However, the flow stops at if result['user_id'] != gplus_id:, saying "Token's user ID doesn't match given user ID.". result['user_id'] is a valid users ID, but gplus_id is None.
The line gplus_id = request.args.get('gplus_id') is expecting the GET args to contain 'gplus_id', but they only contain 'state'. Is this a problem with my javascript connectServer function? Should I include 'gplus_id' there? Surely I don't know it at that point. Or something else?
Similar to this question, I believe this is an issue with incomplete / not up to date / inconsistent documentation.
Where https://developers.google.com/+/web/signin/server-side-flow suggests that gplus_id will be returned in the GET arguments, this is not the case for the flow I was using.
I found my answer in https://github.com/googleplus/gplus-quickstart-python/blob/master/signin.py, which includes this snippet:
# An ID Token is a cryptographically-signed JSON object encoded in base 64.
# Normally, it is critical that you validate an ID Token before you use it,
# but since you are communicating directly with Google over an
# intermediary-free HTTPS channel and using your Client Secret to
# authenticate yourself to Google, you can be confident that the token you
# receive really comes from Google and is valid. If your server passes the
# ID Token to other components of your app, it is extremely important that
# the other components validate the token before using it.
gplus_id = credentials.id_token['sub']