Bad Request from the browser, Flask and jQuery - python

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)

Related

Jquery not detecting Flask session

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
}

Why is a CSRF POST request to 'localhost:8000' successful, but '127.0.0.1:8000' is not?

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']

How do i authenticate to and get catalog from Azure Data Catalog using Rest API in Python

I would like to get the name of the catalog from Azure Data Catalog using API. When I tried using the following command to get catalog from Azure Data Catalog
requests.get("https://management.azure.com/subscriptions/{id}/resourceGroups/{group_name}/providers/Microsoft.DataCatalog/catalogs/{catalogname}")
as mentioned in the link https://learn.microsoft.com/en-us/rest/api/datacatalog/data-catalog-data-catalog
It throws the following error
Response [400]
Looks like I have to authenticate first. How do I authenticate prior to getting catalog?
Adding the new answer in python
for getting the auth context in python you could do the following
here is the settings for the parameters we need it while calling graph api.
RESOURCE = "https://graph.microsoft.com" # Add the resource you want the access token for
TENANT = "Your tenant" # Enter tenant name, e.g. contoso.onmicrosoft.com
AUTHORITY_HOST_URL = "https://login.microsoftonline.com"
CLIENT_ID = "Your client id " # copy the Application ID of your app from your Azure portal
CLIENT_SECRET = "Your client secret" # copy the value of key you generated when setting up the application
# These settings are for the Microsoft Graph API Call
API_VERSION = 'v1.0'
here is the code for logging in
AUTHORITY_URL = config.AUTHORITY_HOST_URL + '/' + config.TENANT
REDIRECT_URI = 'http://localhost:{}/getAToken'.format(PORT)
TEMPLATE_AUTHZ_URL = ('https://login.microsoftonline.com/{}/oauth2/authorize?' +
'response_type=code&client_id={}&redirect_uri={}&' +
'state={}&resource={}')
def login():
auth_state = str(uuid.uuid4())
flask.session['state'] = auth_state
authorization_url = TEMPLATE_AUTHZ_URL.format(
config.TENANT,
config.CLIENT_ID,
REDIRECT_URI,
auth_state,
config.RESOURCE)
resp = flask.Response(status=307)
resp.headers['location'] = authorization_url
return resp
here is how you can retrieve the token
auth_context = adal.AuthenticationContext(AUTHORITY_URL)
token_response = auth_context.acquire_token_with_authorization_code(code, REDIRECT_URI, config.RESOURCE,
config.CLIENT_ID, config.CLIENT_SECRET)
and then you can create a endpoint for your azure data catalog api. Here is the http header for the same-
http_headers = {'Authorization': 'Bearer ' + token_response['accessToken'],
'User-Agent': 'adal-python-sample',
'Accept': 'application/json',
'Content-Type': 'application/json',
'client-request-id': str(uuid.uuid4())}
and then finally you can call the api. Here endpoint being the data catalog API URL.
data = requests.get(endpoint, headers=http_headers, stream=False).json()
Hope it helps.
To call a Data Catalog REST operation, create an instance of AuthenticationContext and call AcquireToken. AuthenticationContext is part of the Active Directory Authentication Library NuGet package. To install the Active Directory Authentication Library NuGet package in Visual Studio,run
"Install-Package Microsoft.IdentityModel.Clients.ActiveDirectory"
from the NuGet Package Manager Console.
Here is the code to get the token for the same.
static async Task<AuthenticationResult> AccessToken()
{
if (authResult == null)
{
//Resource Uri for Data Catalog API
string resourceUri = "https://api.azuredatacatalog.com";
//To learn how to register a client app and get a Client ID, see https://msdn.microsoft.com/en-us/library/azure/mt403303.aspx#clientID
string clientId = clientIDFromAzureAppRegistration;
//A redirect uri gives AAD more details about the specific application that it will authenticate.
//Since a client app does not have an external service to redirect to, this Uri is the standard placeholder for a client app.
string redirectUri = "https://login.live.com/oauth20_desktop.srf";
// Create an instance of AuthenticationContext to acquire an Azure access token
// OAuth2 authority Uri
string authorityUri = "https://login.windows.net/common/oauth2/authorize";
AuthenticationContext authContext = new AuthenticationContext(authorityUri);
// Call AcquireToken to get an Azure token from Azure Active Directory token issuance endpoint
// AcquireToken takes a Client Id that Azure AD creates when you register your client app.
authResult = await authContext.AcquireTokenAsync(resourceUri, clientId, new Uri(redirectUri), new PlatformParameters(PromptBehavior.Always));
}
return authResult;
}
Here is the sample code to get the data asset base on id
// The Get Data Asset operation retrieves data asset by Id
static JObject GetDataAsset(string assetUrl)
{
string fullUri = string.Format("{0}?api-version=2016-03-30", assetUrl);
//Create a GET WebRequest as a Json content type
HttpWebRequest request = WebRequest.Create(fullUri) as HttpWebRequest;
request.KeepAlive = true;
request.Method = "GET";
request.Accept = "application/json;adc.metadata=full";
try
{
var response = SetRequestAndGetResponse(request);
using (var reader = new StreamReader(response.GetResponseStream()))
{
var itemPayload = reader.ReadToEnd();
Console.WriteLine(itemPayload);
return JObject.Parse(itemPayload);
}
}
catch (WebException ex)
{
Console.WriteLine(ex.Message);
Console.WriteLine(ex.Status);
if (ex.Response != null)
{
// can use ex.Response.Status, .StatusDescription
if (ex.Response.ContentLength != 0)
{
using (var stream = ex.Response.GetResponseStream())
{
using (var reader = new StreamReader(stream))
{
Console.WriteLine(reader.ReadToEnd());
}
}
}
}
}
return null;
}
Here is how you can set the request, token and get the response.
static HttpWebResponse SetRequestAndGetResponse(HttpWebRequest request, string payload = null)
{
while (true)
{
//To authorize the operation call, you need an access token which is part of the Authorization header
request.Headers.Add("Authorization", AccessToken().Result.CreateAuthorizationHeader());
//Set to false to be able to intercept redirects
request.AllowAutoRedirect = false;
if (!string.IsNullOrEmpty(payload))
{
byte[] byteArray = Encoding.UTF8.GetBytes(payload);
request.ContentLength = byteArray.Length;
request.ContentType = "application/json";
//Write JSON byte[] into a Stream
request.GetRequestStream().Write(byteArray, 0, byteArray.Length);
}
else
{
request.ContentLength = 0;
}
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
// Requests to **Azure Data Catalog (ADC)** may return an HTTP 302 response to indicate
// redirection to a different endpoint. In response to a 302, the caller must re-issue
// the request to the URL specified by the Location response header.
if (response.StatusCode == HttpStatusCode.Redirect)
{
string redirectedUrl = response.Headers["Location"];
HttpWebRequest nextRequest = WebRequest.Create(redirectedUrl) as HttpWebRequest;
nextRequest.Method = request.Method;
request = nextRequest;
}
else
{
return response;
}
}
}
Basically you need to get the bearer token and pass it as a request parameter to get the catalog using azure data catalog api.
for further code sample, please browse below code repository.
https://github.com/Azure-Samples/data-catalog-dotnet-get-started
Hope it helps.

How do I link my site running on Google App Engine to use Google+ login? [duplicate]

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']

Is it OK to pass both token and client_id to the client when Channel API is used?

I need to create an application, where GAE server will always talk with just one client (i.e. one message should be always sent just to one client).
I do the following -
Python:
def get(self):
# generate token, when page is loaded
client_id = uuid.uuid4().hex
token = channel.create_channel(client_id)
template_values = {'token': token,
'client_id': client_id
}
self.response.out.write(template.render('page.html', template_values))
def post(self):
# reply to the client
...
client_id = self.request.get('id')
channel.send_message(client_id, message)
Javascript:
sendMessage = function(field) {
$.ajax({
type: "POST",
url: "/",
data: "f=" + field + "&id=" + "{{ client_id }}", // WARNING!
contentType: "application/x-www-form-urlencoded",
success: function(data) {
}
});
};
onOpened = function() {
connected = true;
sendMessage('opened');
};
onMessage = function(msg) {
alert(msg.data);
};
onError = function(err) {
alert(err);
};
onClose = function() {
alert("close");
};
// open new session
channel = new goog.appengine.Channel('{{ token }}'); // WARNING!
socket = channel.open();
socket.onopen = onOpened;
socket.onmessage = onMessage;
socket.onerror = onError;
socket.onclose = onClose;
It works well, but with such scenario both token and client_id are passed to the client. Is it OK?
There's no technical reason not to do this. If you're worried about security, the token is far more valuable: an attacker who could listen to your traffic could take the token and listen to channel messages in a different context. The clientid wouldn't let them do that.
But I do have a question: why not return the message in the POST response, rather than sending a message over the channel? Or is the sample code just simplified for the example?

Categories