I'm using django-all-access to implement OAuth authentication for Facebook, Twitter, and LinkedIn. Facebook and Twitter are working fine, LinkedIn is redirecting me to the wrong page.
Here's my setup (consumer keys and secrets are obviously obfuscated):
[
{
"pk": null,
"model": "allaccess.provider",
"fields": {
"name": "facebook",
"consumer_key": "xxx",
"consumer_secret": "xxx",
"authorization_url": "https://www.facebook.com/dialog/oauth",
"access_token_url": "https://graph.facebook.com/oauth/access_token",
"request_token_url": "",
"profile_url": "https://graph.facebook.com/me"
}
},
{
"pk": null,
"model": "allaccess.provider",
"fields": {
"name": "twitter",
"consumer_key": "xxx",
"consumer_secret": "xxx",
"authorization_url": "https://api.twitter.com/oauth/authenticate",
"access_token_url": "https://api.twitter.com/oauth/access_token",
"request_token_url": "https://api.twitter.com/oauth/request_token",
"profile_url": "https://api.twitter.com/1.1/account/verify_credentials.json"
}
},
{
"pk": null,
"model": "allaccess.provider",
"fields": {
"name": "linkedin",
"consumer_key": "xxx",
"consumer_secret": "xxx",
"authorization_url": "https://www.linkedin.com/uas/oauth2/authorization",
"access_token_url": "https://www.linkedin.com/uas/oauth2/accessToken",
"request_token_url": "",
"profile_url": "https://api.linkedin.com/v1/people/~"
}
}
]
Both Facebook and Twitter are using the correct authentication flow and registering users properly, but Twitter redirects me to the wrong page and is not registering users at all. Here's the LinkedIn flow (I removed most parameters, and left the redirect_uri):
https://www.linkedin.com/uas/oauth2/authorization?redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Faccounts%2Fcallback%2Flinkedin%2F&response_type=code
http://localhost:8000/accounts/callback/linkedin/
http://localhost:8000/accounts/login/
My first guess would be that my app settings are improperly configured in LinkedIn, so here are my settings:
OAuth 2.0 Redirect URLs: http://localhost:8000/accounts/callback/linkedin/,http://localhost:8000/accounts/profile/
OAuth 1.0 Accept Redirect URL: http://localhost:8000/accounts/profile/
My second guess would be that the profile_url parameter is wrong, which is https://api.linkedin.com/v1/people/~.
Can anybody help?
Best.
There were two things wrong with this. First, LinkedIn expects the access_token parameter to be named oauth2_access_token, which is not compliant with the RFC 6750. Also, LinkedIn does not return JSON by default, which is expected by the allaccess clients. As such, you'd also need to add format=json as a parameter in the call.
This can be achieved mostly by customising the OAuth2Client.request method, but in my case I went a little further. The allaccess framework sends the access token as a query parameter, which is usually discouraged because the tokens are then logged on the server, which might not be safe. Instead, both OAuth 1 and 2 support sending the token in the Authorization request header. OAuth 1 is a bit more complicated, while OAuth 2 requires only a bearer token.
As such, I customised the OAuth2Client class to handle both these situations.
from allaccess.clients import OAuth2Client as _OAuth2Client
from requests.api import request
class OAuth2Client(_OAuth2Client):
def request(self, method, url, **kwargs):
user_token = kwargs.pop('token', self.token)
token, _ = self.parse_raw_token(user_token)
if token is not None:
# Replace the parent method so the token is sent on the headers. This is
# safer than using query parameters, which is what allaccess does
headers = kwargs.get('headers', {})
headers['Authorization'] = self.get_authorization_header(token)
kwargs['headers'] = headers
return request(method, url, **kwargs)
def get_authorization_header(self, token):
return 'Bearer %s' % (token,)
class OAuth2LinkedInClient(OAuth2Client):
def request(self, method, url, **kwargs):
# LinkedIn does not return JSON by default
params = kwargs.get('params', {})
params['format'] = 'json'
kwargs['params'] = params
return super(OAuth2LinkedInClient, self).request(method, url, **kwargs)
OAuth2Client now sends the access token in the request headers instead of the query parameters. Also, the LinkedIn client adds the format query parameter and sets it to json. There's no need to replace OAuth 1 authentication as it already sends the token in the headers.
Unfortunately, that's not the whole deal. We now need to let allaccess know to use these clients, and we do that by customising the views. Here's my implementation:
from allaccess.views import OAuthRedirect as _OAuthRedirect
from allaccess.views import OAuthCallback as _OAuthCallback
from allaccess.views import OAuthClientMixin as _OAuthClientMixin
from django.core.urlresolvers import reverse
from authy.clients import OAuth2Client, OAuth2LinkedInClient
class OAuthClientMixin(_OAuthClientMixin):
def get_client(self, provider):
# LinkedIn is... Special
if provider.name == 'linkedin':
return OAuth2LinkedInClient(provider)
# OAuth 2.0 providers
if not provider.request_token_url:
return OAuth2Client(provider)
# Let allaccess chose other providers (those will be mostly OAuth 1)
return super(OAuthClientMixin, self).get_client(provider)
class OAuthRedirect(OAuthClientMixin, _OAuthRedirect):
# This is necessary because we'll be setting these on our URLs, we can no longer
# use allaccess' URLs.
def get_callback_url(self, provider):
return reverse('authy-callback', kwargs={ 'provider': provider.name })
class OAuthCallback(OAuthClientMixin, _OAuthCallback):
# We need this. Notice that it inherits from our own client mixin
pass
Now set the URLs to map to our own implementation:
from django.conf.urls import url
from .views import OAuthRedirect, OAuthCallback
urlpatterns = [
url(r'^login/(?P<provider>(\w|-)+)/$', OAuthRedirect.as_view(), name='authy-login'),
url(r'^callback/(?P<provider>(\w|-)+)/$', OAuthCallback.as_view(), name='authy-callback'),
]
There's one other issue left unsolved, however. The problem is that other classes also use clients. I could find the allaccess.models.AccountAccess.api_client method, for instance. I'm not sure if there are more. Now the problem is that our views might be using our clients, while other classes are using different clients. I'm not sure to what extend this could be a problem, but so for it has not bitten me and for now I'm proceeding with this code.
Finally, I would like to credit and thank Mark Lavin, the creator of the allaccess framework. I contacted him and it was his guidance which lead me to these conclusions.
Hope this helps someone else as well!
Farewell.
Related
I'm trying to create a python script which takes a (.csv with access tokens and a file) as input and uploads that file to multiple google drives whose access tokens are in that csv
but after sometime access tokens get expired and I have to get them again...just saw there's something called refresh and it refreshes access token
Is it possible to do this from python script, please explain.
Do refresh token expire?
import json
import requests
import pandas as pd
headers = {}
para = {
"name": "update",
}
files = {
'data': ('metadata', json.dumps(para), 'application/json; charset=UTF-8'),
'file': open("./update.txt", "rb")
}
tokens = pd.read_csv('tokens.csv')
for i in tokens.token:
headers={"Authorization": i}
r = requests.post(
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart",
headers=headers,
files=files
)
print(r.text)
In order to be able to get a new access_token programmatically using a refresh_token, you must have set access_type to offline when redirecting the user to Google's OAuth 2.0 server.
If you did that, you can get a new access_token if you do the following POST request to https://oauth2.googleapis.com/token:
POST /token HTTP/1.1
Host: oauth2.googleapis.com
Content-Type: application/x-www-form-urlencoded
client_id=your_client_id&
client_secret=your_client_secret&
refresh_token=refresh_token&
grant_type=refresh_token
The corresponding response would be something like:
{
"access_token": "1/fFAGRNJru1FTz70BzhT3Zg",
"expires_in": 3920,
"scope": "https://www.googleapis.com/auth/drive",
"token_type": "Bearer"
}
Note:
You can find code snippets for several languages in the reference I provide below, including Python, but considering you are not using the Python library, I think the HTTP/REST snippet I provided might be more useful in your situation.
Reference:
Refreshing an access token (offline access)
This is a bit of a newbie Q.
I'm using Python 3.6
I am trying to use the domain realestate api to write a scraper that collects data of houses/apartments for sale in my area, but i am having trouble getting the post request to work. I have registered and retrieved my client_id and secret_id for the authentication. The post request returns a status_code of 400
response = requests.post('https://auth.domain.com.au/v1/connect/token',
data = {'client_id':client_id,
"client_secret":client_secret,
"grant_type":"client_credentials",
"scope":"api_agencies_read api_listings_read",
"Content-Type":"application/json"})
token=response.json()
access_token=token["access_token"]
search_parameters = {
"listingType": "Sale",
"locations": [
{
"state": "NSW",
"suburb": "Balgowlah",
"postcode": 2093,
"includeSurroundingSuburbs": True
}
]
}
url = "https://api.domain.com.au/v1/listings/residential/_search"
auth = {"Authorization":"Bearer "+access_token}
request = requests.post(url, data=search_parameters, headers=auth)
details=request.json()
I know my authentication is correct, because i can use the Live API on the website to test the same request (i had to select the client, secret id and the project to allow direct access), and i get a valid access token from the code above.
access_token:
{'access_token': 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
'expires_in': 43200,
'token_type': 'Bearer'}
request.json():
{'errors': {'parameters': ['Undefined error.']},
'message': 'The request is invalid.'}
I've been able to implement the notebook from this post. So i can be sure my client and secret ids are connected to the domain project.
#furas had the solution:
look at the example closer :)
The example uses "Content-Type":"text/json" but you use "application/json" instead of "text/json"
I am going to use Firebase Auth and Database modules to create my web app. However, not all things that I want my app to do is possible to achieve on only front end. So I want to also use backend with Python's Bottle framework to handle requests and Pyrebase to get access to Firebase Database.
Let's say that after logging in I need to go to mainpage and see personalized content, for example my notes. They are structured this way in DB:
{
"notes": [{
"id": "1",
"title": "X",
"author": "user1"
},
{
"id": "2",
"title": "Y",
"author": "user2"
} and so on... ]
}
So how it's possible to implement showing only my articles on main page?
I understand that I need to filter my notes based on author value, but how to let Bottle understand who is currently logged in?
I've read there, that I should somehow send unique token to backend server to authenticate current user, but how to do that? Inserting Token in every link as GET parameter seems to be silly, but I see no other way to implement that.
Start by organizing your database so that each note becomes a child object:
{
"notes": {
"id1": {
"id": "id1",
"title": "X",
"author": "user1",
},
"id2": {
}
}
}
Then this particular interaction can be implemented entirely in the client-side. Just execute a query to filter the notes you want. For example in a JS client:
var uid = firebase.auth().currentUser.uid;
var query = ref.orderByChild('author').equalTo(uid);
// Listen for query value events
If you want to run this on a backend server, and you want to ensure that only logged in users are allowed to execute it, then you must pass the ID token from the client app to the server on each request. Here's how to implement the server-side logic using the Python Admin SDK:
import firebase_admin
from firebase_admin import auth
from firebase_admin import db
token = '....' # Extract from the client request
try:
decoded = auth.verify_id_token(token)
uid = decoded.uid
ref = db.reference('path/to/notes')
notes = ref.order_by_child('author').equal_to(uid).get()
# Process notes response
except ValueError as ex:
print(ex)
# Send error to client
I'm trying to get the FB messenger API working using Python's Flask, adapting the following instructions: https://developers.facebook.com/docs/messenger-platform/quickstart
So far, things have been going pretty well. I have verified my callback and am able to receive the messages I send using Messenger on my page, as in the logs in my heroku server indicate the appropriate packets of data are being received by my server. Right now I'm struggling a bit to send responses to the client messenging my app. In particular, I am not sure how to perform the following segment from the tutorial in Flask:
var token = "<page_access_token>";
function sendTextMessage(sender, text) {
messageData = {
text:text
}
request({
url: 'https://graph.facebook.com/v2.6/me/messages',
qs: {access_token:token},
method: 'POST',
json: {
recipient: {id:sender},
message: messageData,
}
}, function(error, response, body) {
if (error) {
console.log('Error sending message: ', error);
} else if (response.body.error) {
console.log('Error: ', response.body.error);
}
});
}
So far, I have this bit in my server-side Flask module:
#app.route('/', methods=["GET", "POST"])
def chatbot_response():
data = json.loads(req_data)
sender_id = data["entry"][0]["messaging"][0]["sender"]["id"]
url = "https://graph.facebook.com/v2.6/me/messages"
qs_value = {"access_token": TOKEN_OMITTED}
json_response = {"recipient": {"id": sender_id}, "message": "this is a test response message"}
response = ("my response text", 200, {"url": url, "qs": qs_value, "method": "POST", "json": json_response})
return response
However, running this, I find that while I can process what someone send my Page, it does not send a response back (i.e. nothing shows up in the messenger chat box). I'm new to Flask so any help would be greatly appreciated in doing the equivalent of the Javascript bit above in Flask.
Thanks!
This is the code that works for me:
data = json.loads(request.data)['entry'][0]['messaging']
for m in data:
resp_id = m['sender']['id']
resp_mess = {
'recipient': {
'id': resp_id,
},
'message': {
'text': m['message']['text'],
}
}
fb_response = requests.post(FB_MESSAGES_ENDPOINT,
params={"access_token": FB_TOKEN},
data=json.dumps(resp_mess),
headers = {'content-type': 'application/json'})
key differences:
message needs a text key for the actual response message, and you need to add the application/json content-type header.
Without the content-type header you get the The parameter recipient is required error response, and without the text key under message you get the param message must be non-empty error response.
This is the Flask example using fbmq library that works for me:
echo example :
from flask import Flask, request
from fbmq import Page
page = fbmq.Page(PAGE_ACCESS_TOKEN)
#app.route('/webhook', methods=['POST'])
def webhook():
page.handle_webhook(request.get_data(as_text=True))
return "ok"
#page.handle_message
def message_handler(event):
page.send(event.sender_id, event.message_text)
In that scenario in your tutorial, the node.js application is sending an HTTP POST request back to Facebook's servers, which then forwards the content on to the client.
So far, sounds like your Flask app is only receiving (AKA serving) HTTP requests. The reason is that that's what the Flask library is all about, and it's the only thing that Flask does.
To send an HTTP request back to Facebook, you can use any Python HTTP client library you like. There is one called urllib in the standard library, but it's a bit clunky to use... try the Requests library.
Since your request handler is delegating to an outgoing HTTP call, you need to look at the response to this sub-request also, to make sure everything went as planned.
Your handler may end up looking something like
import json
import os
from flask import app, request
# confusingly similar name, keep these straight in your head
import requests
FB_MESSAGES_ENDPOINT = "https://graph.facebook.com/v2.6/me/messages"
# good practice: don't keep secrets in files, one day you'll accidentally
# commit it and push it to github and then you'll be sad. in bash:
# $ export FB_ACCESS_TOKEN=my-secret-fb-token
FB_TOKEN = os.environ['FB_ACCESS_TOKEN']
#app.route('/', method="POST")
def chatbot_response():
data = request.json() # flasks's request object
sender_id = data["entry"][0]["messaging"][0]["sender"]["id"]
send_back_to_fb = {
"recipient": {
"id": sender_id,
},
"message": "this is a test response message"
}
# the big change: use another library to send an HTTP request back to FB
fb_response = requests.post(FB_MESSAGES_ENDPOINT,
params={"access_token": FB_TOKEN},
data=json.dumps(send_back_to_fb))
# handle the response to the subrequest you made
if not fb_response.ok:
# log some useful info for yourself, for debugging
print 'jeepers. %s: %s' % (fb_response.status_code, fb_response.text)
# always return 200 to Facebook's original POST request so they know you
# handled their request
return "OK", 200
When doing responses in Flask, you have to be careful. Simply doing a return statement won't return anything to the requester.
In your case, you might want to look at jsonify(). It will take a Python dictionary and return it to your browser as a JSON object.
from flask import jsonify
return jsonify({"url": url, "qs": qs_value, "method": "POST", "json": json_response})
If you want more control over the responses, like setting codes, take a look at make_response()
I'm currently trying to implement a user notification system using Websockets via Crossbar/Autobahn. I have done multiple tests and gone through the documentation, however, I'm not sure if there's a solution to having the following workflow work:
User signs in with web app -- this is done through JWT
Frontend establishes a websocket connection to a running crossbar instance.
Frontend attempts to subscribe to a URI specifically for the user's notifications: i.e. com.example.notifications.user.23 or com.example.user.23.notifications'. Where23` is the user id.
User's JWT is checked to see if user is allowed to access subscription.
When activity is generated and causes a notification, the backend publishes the user-specific URIs.
For step 3, I can't tell if the current support auth methods have what I need. Ideally, I would like an auth method which I can customize (in order to implement a JWT authenticator within Crossbar) that I can apply to a URI pattern, but NOT give access to the entire pattern to the subscribing user. This is partially solved by the dynamic auth methods, but is missing the latter half:
For example (my ideal workflow):
User attempts to subscribe to a URI com.example.user.23.notifications.
URI matches com.example.user..notifications (wildcard pattern in http://crossbar.io/docs/Pattern-Based-Subscriptions/)
Auth token is validated and user is given access to only com.example.user.23.notifications.
Is the above achievable in a simple way? From what I can tell, it may only be possible if I somehow generate a .crossbar/config.json which contains URI permutations of all user ids...and automatically generate a new config for each new user -- which is completely not a reasonable solution.
Any help is appreciated!
Use authorizer.
See http://crossbar.io/docs/Authorization/#dynamic-authorization
Register a dynamic authorizer for the user role that session was assigned when joining/authenticating:
{
"name": "authorizer",
"permissions": [
{
"uri": "com.example.authorize",
"register": true
}
]
},
{
"name": "authenticator",
"permissions": [
{
"uri": "com.example.authenticate",
"register": true
}
]
},
{
"name": "user",
"authorizer": "com.example.authorize"
},
...
"components": [
{
"type": "class",
"classname": "example.AuthenticatorSession",
"realm": "realm1",
"role": "authenticator",
"extra": {
"backend_base_url": "http://localhost:8080/ws"
}
},
{
"type": "class",
"classname": "example.AuthorizerSession",
"realm": "realm1",
"role": "authorizer"
}
]
Write a class
class AuthorizerSession(ApplicationSession):
#inlineCallbacks
def onJoin(self, details):
print("In AuthorizerSession.onJoin({})".format(details))
try:
yield self.register(self.authorize, 'com.example.authorize')
print("AuthorizerSession: authorizer registered")
except Exception as e:
print("AuthorizerSession: failed to register authorizer procedure ({})".format(e))
def authorize(self, session, uri, action):
print("AuthorizerSession.authorize({}, {}, {})".format(session, uri, action))
if session['authrole'] == u'backend': # backnend can do whatever
return True
[Authorization logic here]
return authorized