I have the following class and am currently testing it via running a file which calls the methods in this file. I then use a mixture of print statements and checking the blog to make sure the code has worked.
I'd really like to write some pytest unit tests for this and automate it all, but how can I do this? Also, if authentication doesn't exist or had become invalid it opens the browser and prompts the user to input the auth code. Later on this will be handled by a gui form. Pytest doesn't take user input, and rightly so; it wouldn't be automated.
class BloggerInterface(object):
"""Connects to blogger api and authorises client."""
def get_credentials(self):
"""Gets google api credentials, or generates new credentials
if they don't exist or are invalid."""
scope = 'https://www.googleapis.com/auth/blogger'
flow = oauth2client.client.flow_from_clientsecrets(
'client_secret.json', scope,
redirect_uri='urn:ietf:wg:oauth:2.0:oob')
storage = oauth2client.file.Storage('credentials.dat')
credentials = storage.get()
if not credentials or credentials.invalid:
auth_uri = flow.step1_get_authorize_url()
webbrowser.open(auth_uri)
auth_code = input('Enter the auth code: ')
credentials = flow.step2_exchange(auth_code)
storage.put(credentials)
return credentials
def get_service(self):
"""Returns an authorised blogger api service."""
credentials = self.get_credentials()
http = httplib2.Http()
http = credentials.authorize(http)
service = apiclient.discovery.build('blogger', 'v3', http=http)
return service
def get_blog(self, blog_id):
"""Gets the details ofthe blog withthe id blog_id"""
BlogDetails = collections.namedtuple('BlogDetails', 'blog_id, name, desc, url')
conn = self.get_service()
request = conn.blogs().get(blogId=blog_id, view='ADMIN')
response = request.execute()
name = response.get('name')
desc = response.get('description')
url = response.get('url')
blog = BlogDetails(blog_id=blog_id, name=name, desc=desc, url=url)
return blog
def get_posts(self, blog_id, status='live'):
"""Gets all posts from the blog with the id blog_id"""
posts = []
conn = self.get_service()
request = conn.posts().list(blogId=blog_id, view='ADMIN',
status=status)
#Responses are paginated, so a paging loop is required.
while request:
response = request.execute()
for post in response.get('items', []):
post_id = post.get('id')
title = post.get('title')
url = post.get('url')
status = post.get('status')
content = post.get('content')
posts.append({'post_id':post_id, 'title':title, 'url':url,
'status':status, 'content':content})
request = conn.posts().list_next(request, response)
return posts
def add_post(self, blog_id, post, is_draft=True):
"""Adds a new post to the blog with the id blog_id"""
conn = self.get_service()
#post is in the form {title, content, (labels), author_name, author_id.
title, content, author_name, author_id, labels = post
data = {
'kind': 'blogger#post',
'title': title,
'content': content,
'labels': labels,
'author': {'displayName':author_name, 'id':author_id}
}
request = conn.posts().insert(blogId=blog_id, body=data,
isDraft=is_draft)
response = request.execute()
post_id = response.get('id')
return post_id
Don't test the oauth2client or webbrowser projects. Test how your code reacts to input and output from other parts. Those are black boxes, which you replace with your own mocks, so you can see how your code responds to different return values.
Use the unittest.mock module to produce the mocks. If you are using Python < 3.3, install the backport mock project to do so.
For example, for BloggerInterface.get_credentials(), you mock out oauth2client.client.flow_from_clientsecrets(), oauth2client.file.Storage(), webbrowser.open() and input. You can then play with the response from storage.get() to force your code to use webbrowser.open(), and test if your code correctly tried to open a webbrowser, and then called storage.put() to store the credentials:
with mock.patch('oauth2client.client.flow_from_clientsecrets') as mockflow, \
mock.patch('oauth2client.file.Storage') as MockStorage, \
mock.patch('webbrowser.open') as mockwbopen, \
mock.patch('yourmodule.input') as mockinput:
# set the credentials to invalid
storage = MockStorage.return_value
credentials = storage.get.return_value
credentials.invalid = True
# run the method and see if we get what we want
result = BloggerInterface().get_credentials()
# check that the flow was initialised correctly
mockflow.assert_called_with(
'client_secret.json', 'https://www.googleapis.com/auth/blogger',
redirect_uri='urn:ietf:wg:oauth:2.0:oob')
MockStorage.assert_called_with('credentials.dat')
# With invalid credentials, the code should obtain a auth url from the
# flow, pass it to the browser. Then the authentication code should be taken
# from input and passed back to the flow for exchange. Test these
# interactions took place:
flow.step1_get_authorize_url.assert_called_once_with()
mockwbopen.assert_called_once_with(flow.step1_get_authorize_url.return_value)
flow.step2_exchange.assert_called_once_with(mockinput.return_value)
storage.put(flow.step2_exchange.return_value)
assert result == flow.step2_exchange.return_value
Related
I am using Google OAuth2 flow as described here: https://developers.google.com/identity/protocols/oauth2/web-server.
First I check if user credentials are already in a DB, if not the auth flow is initiated. After exchanging code to an access token, credentials are saved in a DB. There along with an access token and a refresh token a token expiration dateTime is saved, which is 1 hour (credentials.expiry).
Then I wanted to check how the access token would be refreshed. So I explicitly requested API after the token expiration time. The previously saved credentials are in the DB, so I get them from there. Its validity is checked, and the access token should be updated. However, by some reason, the token is valid and by making a test request to the Google API I am kinda can make requests, no error is raised.
I cannot get why token is valid, when it obviously is expired. I also checked the token validity here: https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=AT and got { "error_description": "Invalid Value" }.
My code:
def google_oauth_init_flow(request):
# get user id
user_id = request.GET.get('user_id', None)
print(user_id)
user = User.objects.get(id=user_id)
# set id to a state parameter
request.session['state'] = str(user.id)
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
os.path.abspath(os.path.join(os.path.dirname(__file__), KEY_FILE_LOCATION)),
scopes=SCOPES)
flow.redirect_uri = REDIRECT_URI
authorization_url, state = flow.authorization_url(
# Enable offline access so that you can refresh an access token without
# re-prompting the user for permission. Recommended for web server apps.
access_type='offline',
# Enable incremental authorization. Recommended as a best practice.
include_granted_scopes='true',
state=str(user.id)
)
return HttpResponseRedirect(authorization_url)
def google_oauth_exchange_token(request):
state = request.session.get('state', 'No state')
print(state)
flow = google_auth_oauthlib.flow.Flow.from_client_secrets_file(
os.path.abspath(os.path.join(os.path.dirname(__file__), KEY_FILE_LOCATION)),
scopes=SCOPES,
state=state)
flow.redirect_uri = REDIRECT_URI
authorization_response = request.build_absolute_uri()
flow.fetch_token(authorization_response=authorization_response)
credentials = flow.credentials
# save creds in DB
try:
save_credentials(credentials, state)
except IntegrityError as e:
if 'unique constraint' in e.message:
# duplicate detected
return HttpResponse('Violation of unique constraint')
def google_oauth_check_token(credentials, user_id):
# convert dict into credentials instance
if type(credentials) == dict:
credentials = init_creds_instance(credentials)
if credentials.expired:
print('Token is expired getting new one...')
# refresh credentials
request = google.auth.transport.requests.Request()
credentials.refresh(request)
# alternative method
# credentials.refresh(httplib2.Http())
# update token in DB
SystemServiceTokens.objects.filter(id=user_id).update(name='token', key=credentials.token)
return credentials
else: # always Token is valid
print('TOKEN', credentials.token)
print('EXPIRY', credentials.expiry)
print('REFRESH TOKEN', credentials.refresh_token)
print('Token is valid')
return credentials
def get_user_info(credentials):
user_info = build(serviceName='oauth2', version='v2', credentials=credentials)
return user_info.userinfo().get().execute()
def save_credentials(credentials, state):
user = User.objects.get(id=state)
google_oauth_check_token(credentials, user.id)
# get email
user_info = get_user_info(credentials)
model = SystemServiceTokens()
# check duplicate values
if not model.objects.filter(user_id=user.id, email=user_info['email'], service_id='ga').exists():
print("Entry contained in queryset")
model.token = credentials.token
model.refresh_token = credentials.refresh_token
model.expires_at = datetime.datetime.strftime(credentials.expiry, '%Y-%m-%d %H:%M:%S')
model.user = user
model.email = user_info['email']
model.service_id = 'ga'
return model.save()
else:
return HttpResponse('Unique constraint violation')
# No errors
def test_request_google(request):
credentials = SystemServiceTokens.objects.filter(user_id=9).first() # DEBUG ID
print(model_to_dict(credentials))
credentials = google_oauth_check_token(model_to_dict(credentials), 9)
# test
user_info = build(serviceName='oauth2', version='v2', credentials=credentials)
print(user_info.userinfo().get().execute())
drive = build('drive', 'v2', credentials=credentials)
If the python client library can detect a refresh token its going to refresh it for you. Assuming your code works correctly you should never have an access token expire the library will refresh it five minutes before it is due to expire.
My Python isn't the best but its probably this in the source code.
http.py#L1559
I am currently using Oauth2.0 to log into my Flask app. I have that working as intended. Now I want to use the same creds I got from logging in to be able to send requests to the Google drive api. Im not really sure where to start, the docs are difficult to follow.
note: I have activated the Google Drive api in the developer console.
Here is the code for the login callback.
globally:
GOOGLE_CLIENT_ID = config['google_client_id']
GOOGLE_CLIENT_SECRET = config['google_client_secret']
GOOGLE_DISCOVERY_URL = (
"https://accounts.google.com/.well-known/openid-configuration"
)
client = WebApplicationClient(GOOGLE_CLIENT_ID)
view function:
#bp.route('/login/callback')
def callback():
# Get authorization code Google sent back to you
code = request.args.get("code")
# Find out what URL to hit to get tokens that allow you to ask for
# things on behalf of a user
google_provider_cfg = get_google_provider_cfg()
token_endpoint = google_provider_cfg["token_endpoint"]
token_url, headers, body = client.prepare_token_request(
token_endpoint,
authorization_response=request.url,
redirect_url=request.base_url,
code=code
)
token_response = requests.post(
token_url,
headers=headers,
data=body,
auth=(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET),
)
# Parse the tokens!
client.parse_request_body_response(json.dumps(token_response.json()))
# Now that you have tokens (yay) let's find and hit the URL
# from Google that gives you the user's profile information,
# including their Google profile image and email
userinfo_endpoint = google_provider_cfg["userinfo_endpoint"]
uri, headers, body = client.add_token(userinfo_endpoint)
userinfo_response = requests.get(uri, headers=headers, data=body)
# You want to make sure their email is verified.
# The user authenticated with Google, authorized your
# app, and now you've verified their email through Google!
if userinfo_response.json().get("email_verified"):
unique_id = userinfo_response.json()["sub"]
users_email = userinfo_response.json()["email"]
picture = userinfo_response.json()["picture"]
username = userinfo_response.json()["given_name"]
else:
return "User email not available or not verified by Google.", 400
user = User.query.filter(User.google_sub==unique_id).first()
login_user(user)
def get_google_provider_cfg():
return requests.get(GOOGLE_DISCOVERY_URL).json()
My thoughts are maybe I have to set up a google drive endpoint to be able to send api requests to with the same token? Im not quite sure.
I am working with beacons and want to display all the registered beacons on a same web page by making the request in python.
I am confused, After setting up the scope of OAuth2, how to send the request or discovery.build() to get list of all the requests.
I am setting up the scope by this:
#portal.route('/oauth2callback')
def oauth2callback():
flow = client.flow_from_clientsecrets(
'client_secrets.json',
scope='https://www.googleapis.com/auth/userlocation.beacon.registry',
redirect_uri=flask.url_for('portal.oauth2callback', _external=True),
)
if 'code' not in flask.request.args:
auth_uri = flow.step1_get_authorize_url()
return flask.redirect(auth_uri)
else:
auth_code = flask.request.args.get('code')
credentials = flow.step2_exchange(auth_code)
flask.session['credentials'] = credentials.to_json()
return flask.redirect(flask.url_for('portal.index'))
#portal.route('/')
def index():
if 'credentials' not in flask.session:
return flask.redirect(flask.url_for('portal.oauth2callback'))
credentials = client.OAuth2Credentials.from_json(
flask.session['credentials']
)
if credentials.access_token_expired:
return flask.redirect(flask.url_for('portal.oauth2callback'))
else:
http_auth = credentials.authorize(httplib2.Http())
drive_service = discovery.build('# What should I write')
# Or, requests.get(What should I write)
Can some one help me how to get list of all registered beacons by making the request.
Hrm. I don't have much experience with Python, but I'm pretty sure there are Google Sign-In clients for python such as here on GitHub.
With this you can integrate a login flow to your app.
Then, when you make calls to the Proximity Beacon Admin API, to auth you just have to set the HTTP header:
Authorization: Bearer <<OAUTH_BEARER_TOKEN_HERE>>
and your call should be authenticated properly. Then you can do the python equivalent of like http.get() or http.post(), add that HTTP header, and you should be able to see responses.
It get done by sending request like:
sess = requests.Session()
req = sess.get('https://proximitybeacon.googleapis.com/v1beta1/beacons', headers={'Authorization': 'Bearer '+credentials.access_token})
Google have recently shifted to OAuth2.0 and we need to change our previous auth macanisms (i.e. from ProgrammaticLogin to OAuth2.0).
I can successfully access the albums and read the data / comments on photos. Its when i try to add new album/photo or try to write data i get the following error.
client = PhotosService(email="xxxx")
...
...
...
#After successfull OAuth
album = client.InsertAlbum(title="Temp album", summary="My summary", access="public")
This line causing the following error.
File "/Users/mac/destipak/env/lib/python2.7/site-packages/gdata/photos/service.py", line 358, in InsertAlbum
raise GooglePhotosException(e.args[0])
gdata.photos.service.GooglePhotosException: (403, 'Forbidden', 'Modification only allowed with api authentication.')
I wasn't quite sure but did you actually make the change over OAuth2? I used the following code and it worked.
def OAuth2Login(client_secrets, credential_store, email):
scope='https://picasaweb.google.com/data/'
user_agent='testingApp'
storage = Storage(credential_store)
credentials = storage.get()
if credentials is None or credentials.invalid:
flow = flow_from_clientsecrets(client_secrets, scope=scope, redirect_uri='urn:ietf:wg:oauth:2.0:oob')
uri = flow.step1_get_authorize_url()
webbrowser.open(uri)
code = raw_input('Enter the authentication code: ').strip()
credentials = flow.step2_exchange(code)
storage.put(credentials)
if (credentials.token_expiry - datetime.utcnow()) < timedelta(minutes=5):
http = httplib2.Http()
http = credentials.authorize(http)
credentials.refresh(http)
gd_client = gdata.photos.service.PhotosService(source=user_agent,
email=email,
additional_headers={'Authorization' : 'Bearer %s' % credentials.access_token})
return gd_client
album = gd_client.InsertAlbum('test', 'My Test Album', access='protected')
I did have to create an API Key in the Google developer portal and download the json secret but after doing that I was able to create an album successfully. This repo was very helpful https://github.com/MicOestergaard/picasawebuploader.
Hi I am trying to follow the Tweepy App Engine OAuth Example app in my app but am running into trouble.
Here is a link to the tweepy example code: http://github.com/joshthecoder/tweepy-examples
Specifically look at: http://github.com/joshthecoder/tweepy-examples/blob/master/appengine/oauth_example/handlers.py
Here is the relevant snippet of my code [Ignore the spacing problems]:
try:
authurl = auth.get_authorization_url()
request_token = auth.request_token
db_user.token_key = request_token.key
db_user.token_secret = request_token.secret
db_user.put()
except tweepy.TweepError, e:
# Failed to get a request token
self.generate('error.html', {
'error': e,
})
return
self.generate('signup.html', {
'authurl': authurl,
'request_token': request_token,
'request_token.key': request_token.key,
'request_token.secret': request_token.secret,
})
As you can see my code is very similar to the example. However, when I compare the version of the request_token.key and request_token.secret that are rendered on my signup page
I.e. the variables I output to the browser:
request_token.key
request_token.secret
Are not the same as the data stored in the datastore:
db_user.token_key = request_token.key
db_user.token_secret = request_token.secret
db_user.put()
As an example here is what I am seeing when testing:
Printed to the screen:
request_token.key: MocXJxcqzDJu6E0yBeaC5sAMSkEoH9NxrwZDDvlVU
request_token.secret: C7EdohrWVor9Yjmr58jbObFmWj0GdBHMMMrIkU8Fds
Values in the datastore:
token_key: 4mZQc90GXCqcS6u1LuEe60wQN53A0fj7wdXHQrpDo
token_secret: dEgr8cvBg9jmPNhPV55gaCwYw5wcCdDZU4PUrMPVqk
Any guidance on what I am doing wrong here?
Thanks!
Reference Links:
Here is a sample code to get Twitter followers-count for a single user using Tweepy (version 2.0) on Google App Engine (GAE) in Python (version 2.7).
# ----GAE MODULES-----------
import webapp2
from webapp2_extras import jinja2
from google.appengine.api import users
import tweepy
import urlparse
import logging
# ----JINJA2 TEMPLATE----------
class TemplateHandler(webapp2.RequestHandler):
#webapp2.cached_property
def jinja2(self):
return jinja2.get_jinja2(app=self.app)
def render_template(self, filename, **template_args):
logging.info('calling jinja2 render function %s %s', self, filename)
self.response.write(self.jinja2.render_template(filename, **template_args))
# ----CODE--------------------
class TwitterTweepyImplementation(TemplateHandler):
'''
All Tweepy related methods are handled in this class
'''
#All methods that expect HTTP GET
twitter_tweepy_impl_get_methods = {
'/tweepyimpl/oauthRedirect': 'redirect_to_twitter_for_user_to_enter_uname_and_pwd',
'/tweepyimpl/oauthCallback': 'handle_callback_from_twitter_after_user_authentication',
}
def get(self):
'''
All twitter specific get actions are handled here
'''
#identify page to display from the path in the URL
rcvd_url = self.request.path
#to keep the code a little easier to understand, there are no security checks or exception handling coded added in
#this code example, so please add those on your own.
#get destination method using key-value pair
dest_method = self.__class__.twitter_tweepy_impl_get_methods.get(rcvd_url, None)
if dest_method:
func = getattr(self, dest_method, None)
if func:
func()
return
def redirect_to_twitter_for_user_to_enter_uname_and_pwd(self):
"""
Twitter OAuth Redirection: redirects user to Twitter for entering user name and password
"""
logging.info('redirect_to_twitter_for_user_to_enter_uname_and_pwd')
auth = tweepy.OAuthHandler(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET, YOUR_OWN_REDIRECT_URL_AFTER_TWITTER_AUTHENTICATION)
'''YOUR_OWN_REDIRECT_URL_AFTER_TWITTER_AUTHENTICATION: you can set this everytime above or once at twitter.com from where
you get your Consumer Key and Consumer Secret. E.g., http://www.yourwebsite.com/tweepyimpl/oauthCallback'''
#get Twitter redirect url where user enters credentials (uname and pwd)
auth_url = auth.get_authorization_url(); #logging.info("auth_url = %s", auth_url);
#store temp credentials as browser cookies (these need to be stored in the browser so that after user completes authentication
#at Twitter.com, when user is redirected to the return URL above by Twitter (= YOUR_OWN_REDIRECT_URL_AFTER_TWITTER_AUTHENTICATION)
#your application server knows for which user this redirect is for).
self.response.set_cookie('token_key', auth.request_token.key)
self.response.set_cookie('token_secret', auth.request_token.secret)
#redirect user's browser to twitter auth URL where user can enter username and pwd
self.redirect(auth_url)
return
def handle_callback_from_twitter_after_user_authentication(self):
"""
Callback from Twitter after user enters user name and pwd at twitter.com URL
"""
logging.info('handle_callback_from_twitter_after_user_authentication')
#Twitter redirected browser here. Now read verifier and determine if user twitter authentication succeeded, failed, or was
#canceled by the user
auth = tweepy.OAuthHandler(TWITTER_CONSUMER_KEY, TWITTER_CONSUMER_SECRET)
verifier = self.request.get('oauth_verifier', None); #logging.info('verifier = %s', verifier)
#user canceled twitter oauth
if not verifier:
self.redirect('your_app_url') #add your own url here.
return
#fetch temp credentials from browser cookies (as set during redirect_to_twitter_for_user_to_enter_uname_and_pwd method).
token_key = self.request.cookies['token_key'];
token_secret = self.request.cookies['token_secret'];
#now exchange temp credentials for user specific access token
auth.set_request_token(token_key, token_secret)
#parse access token string to extract the key and the secret
access_token = auth.get_access_token(verifier=verifier); logging.info('access_token = %s', access_token)
params = urlparse.parse_qs(str(access_token), keep_blank_values=False)
access_key = params['oauth_token'][0]; logging.info('access_key = %s', access_key)
access_secret = params['oauth_token_secret'][0]; logging.info('access_secret = %s', access_secret)
#add access token information to the datastore for periodic fetch of Twitter information later on for this user, e.g., via a cron job.
user_obj = UserTwitterAccessTokenStorageDatabase.get_by_key_name(users.get_current_user().email())
user_obj.access_key = access_key
user_obj.access_secret = access_secret
user_obj.put()
auth.set_access_token(access_key, access_secret) #this statement you can use later on to fetch twitter data for any user whose
#access-key/secret you have stored in your database. For example, via a cron job.
#User does NOT need to be visiting your website for you to fetch twitter data for the user.
#use tweepy api now to get user data from Twitter
api = tweepy.API(auth)
me = api.me()
#display debug information
logging.info("me = %s", me)
logging.info('me.id_str = %s, name = %s, screen_name = %s', me.id_str, me.name, me.screen_name)
#get followers count for this user
user = api.get_user(me.id_str)
logging.info('num_followers = %s', user.followers_count)
#you have the required information - in this code example followers-count. now redirect user to your app determined URL
self.redirect('your_app_url') #add your own url here.
app = webapp2.WSGIApplication([
('/tweepyimpl/.*', TwitterTweepyImplementation)
], debug=const.DEBUG)
It seems you use twice request_token, request_token.key and request_token.secret. The second time ( in self.generate) you should read their values from your database and not request them again.