Authenticating requests, especially with Google's API's is so incredibly confusing!
I'd like to make authorized HTTP POST requests through python in order to query data from the datastore. I've got a service account and p12 file all ready to go. I've looked at the examples, but it seems no matter which one I try, I'm always unauthorized to make requests.
Everything works fine from the browser, so I know my permissions are all in order. So I suppose my question is, how do I authenticate, and request data securely from the Datastore API through python?
I am so lost...
You probably should not be using raw POST requests to use Datastore, instead use the gcloud library to do the heavy lifting for you.
I would also recommend the Python getting started page, as it has some good tutorials.
Finally, I recorded a podcast where I go over the basics of using Datastore with Python, check it out!
Here is the code, and here is an example:
#Import libraries
from gcloud import datastore
import os
#The next few lines will set up your environment variables
#Replace "YOUR_RPOEJCT_ID_HERE" with the correct value in code.py
os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = "key.json"
projectID = "YOUR_RPOEJCT_ID_HERE"
os.environ["GCLOUD_TESTS_PROJECT_ID"] = projectID
os.environ["GCLOUD_TESTS_DATASET_ID"] = projectID
datastore.set_default_dataset_id(projectID)
#Let us build a message board / news website
#First, create a fake email for our fake user
email = "me#fake.com"
#Now, create a 'key' for that user using the email
user_key = datastore.Key('User', email)
#Now create a entity using that key
new_user = datastore.Entity( key=user_key )
#Add some fields to the entity
new_user["name"] = unicode("Iam Fake")
new_user["email"] = unicode(email)
#Push entity to the Cloud Datastore
datastore.put( new_user )
#Get the user from datastore and print
print( datastore.get(user_key) )
This code is licensed under Apache v2
Related
Solution
So I don't think its a surprise to anyone but Google's documentation is god awful. It's so scattered and the Python docs still reference their old depreciated library. Anyways.
So what I really needed to look at was this link "Enabling Server Side Access for your App". This is not linked to anywhere. Keep in mind this is entirely different than "Authenticating with a Backend Server"
This was a start. On the iOS side of things, we need to specify the server or backend's client_id.
...
GIDSignIn.sharedInstance().clientID = SBConstants.Google.IOS_CLIENT_ID
GIDSignIn.sharedInstance().serverClientID = SBConstants.Google.SERVER_CLIENT_ID
...
And capture serverAuthCode from the sign method inside your sign-in delegate.
...
self.googleUser.userID = user.userID
self.googleUser.token = user.authentication.idToken
self.googleUser.serverAuthCode = user.serverAuthCode
...
Now when you want to perform some action in the backend on behalf of the frontend, we pass the captured serverAuthCode and send it as a parameter.
That was the easy part. In the backend, Google seems to have 13 different OAuth2 libraries for Python documented. Their example uses oauth2client which of course is deprecated.
What we want to use is their 'new' library google-api-python-client.
When the auth_token is passed to the backend we need to check if the user already has an access token in our database. If it does, we need to refresh. Otherwise, we need to request a new access token based on the auth_code. After much trial and error, here is the code to do so:
# we have record of this user
# we have record of this user
if user.exists:
# create new credentials, and refresh
credentials = Credentials(
token=user.token,
refresh_token=user.refresh_token,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
token_uri='https://oauth2.googleapis.com/token')
# now we have an access token
credentials.refresh(requests.Request())
else:
# get the auth_token
token_obj = json.loads(request.body)
code = token_obj.get('auth_code')
# request access token given auth_token
auth_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes)
auth_flow.fetch_token(code=code)
# now have access token
credentials = auth_flow.credentials
A warning: Pass or fail, the auth_token is only good for one request. This totally burned me. This also means once you have a successful backend interaction, you must store the user's token information to then request a refresh not a new access token.
Hope this helps someone.
Original Post
Following the documentation here, I am trying to authenticate a user in my iOS app and pass their ID token to my backend. The backend handles the Google API interactions for the iOS app.
I am missing how to actually authenticate that user in the backend. I read over the docs here regarding ID tokens but I am confused on where the service account comes into play.
Current endpoint:
#api_view(['POST'])
#authentication_classes([TokenAuthentication])
#permission_classes([IsAuthenticated])
def google_token_info(request):
try:
token_obj = json.loads(request.body)
token = token_obj['id_token']
id_info = id_token.verify_oauth2_token(token, requests.Request(), settings.IOS_CLIENT_ID)
# create session here - how?
This is all working fine. The ID info returns the expected decrypted JWT contents, and I have the user's unique Google ID at this point.
While testing I had authentication set up via my backend. I had code like this:
def google_auth(request):
web_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes)
web_flow.redirect_uri = request.build_absolute_uri(reverse('api.auth:oauth_callback'))
auth_url, state = web_flow.authorization_url(access_type='offline', include_granted_scopes='true', prompt='consent')
request.session['state'] = state
return redirect(auth_url)
def oauth_callback(request):
success_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes, state=request.session.get('state'))
success_flow.redirect_uri = request.build_absolute_uri(reverse('api.auth:oauth_callback'))
auth_response = request.build_absolute_uri()
success_flow.fetch_token(authorization_response=auth_response)
credentials = success_flow.credentials
if not request.session.get('google_credentials'):
request.session['google_credentials'] = _credentials_to_dict(credentials)
return redirect(reverse('api.auth:success'))
Which setup session credentials for the user. I'm assuming I need something similar, but I am unsure how to create a session without actual credentials.
Solution
So I don't think its a surprise to anyone but Google's documentation is god awful. It's so scattered and the Python docs still reference their old depreciated library. Anyways.
So what I really needed to look at was this link "Enabling Server Side Access for your App". This is not linked to anywhere. Keep in mind this is entirely different than "Authenticating with a Backend Server"
This was a start. On the iOS side of things, we need to specify the server or backend's client_id.
...
GIDSignIn.sharedInstance().clientID = SBConstants.Google.IOS_CLIENT_ID
GIDSignIn.sharedInstance().serverClientID = SBConstants.Google.SERVER_CLIENT_ID
...
And capture serverAuthCode from the sign method inside your sign-in delegate.
...
self.googleUser.userID = user.userID
self.googleUser.token = user.authentication.idToken
self.googleUser.serverAuthCode = user.serverAuthCode
...
Now when you want to perform some action in the backend on behalf of the frontend, we pass the captured serverAuthCode and send it as a parameter.
That was the easy part. In the backend, Google seems to have 13 different OAuth2 libraries for Python documented. Their example uses oauth2client which of course is deprecated.
What we want to use is their 'new' library google-api-python-client.
When the auth_token is passed to the backend we need to check if the user already has an access token in our database. If it does, we need to refresh. Otherwise, we need to request a new access token based on the auth_code. After much trial and error, here is the code to do so:
# we have record of this user
# we have record of this user
if user.exists:
# create new credentials, and refresh
credentials = Credentials(
token=user.token,
refresh_token=user.refresh_token,
client_id=CLIENT_ID,
client_secret=CLIENT_SECRET,
token_uri='https://oauth2.googleapis.com/token')
# now we have an access token
credentials.refresh(requests.Request())
else:
# get the auth_token
token_obj = json.loads(request.body)
code = token_obj.get('auth_code')
# request access token given auth_token
auth_flow = flow.Flow.from_client_secrets_file(creds, scopes=scopes)
auth_flow.fetch_token(code=code)
# now have access token
credentials = auth_flow.credentials
A warning: Pass or fail, the auth_token is only good for one request. This totally burned me. This also means once you have a successful backend interaction, you must store the user's token information to then request a refresh not a new access token.
I am retrieving a ChromeOS device MAC address via the Google Admin Directory API using the device's Serial Number as reference, and am making my calls through
apiclient.
service = discovery.build('admin', 'directory_v1', developerKey=settings.API_KEY)
Here are the calls available for ChromeOS devices; my issue is that I require a Device ID in order to execute the following:
service.chromeosdevices().get(customerId=settings.CID, deviceId=obtained_id, projection=None).execute()
I can send a GET query via the following format:
https://www.googleapis.com/admin/directory/v1/customer/my_customer/devices/chromeos?projection=full&query=id:" + serial + "&orderBy=status&sortOrder=ascending&maxResults=10", "GET")
... but I'm trying to avoid using OAuth2 and just use my API key. Passing the key in a GET request doesn't work either, as it still returns a "Login Required" notice.
How do I squeeze the above query into an apiclient-friendly format? The only option I found via the above calls was to request every device we have (via list), then sift through the mountain of data for the matching Serial number, which seems silly and excessive.
I did notice I could call apiclient.http.HttpRequests, but I couldn't find a way to pass the API key through it either. There's new_batch_http_request, but I can't discern from the docs how to simply pass a URL to it.
Thank you!
Got it!
You can't use just a key for Directory API queries, you need a Service account.
I'm using google-auth (see here) since oauth2client is deprecated.
You also need to:
Delegate the necessary permissions for your service account (mine has the role of Viewer and has scope access to https://www.googleapis.com/auth/admin.directory.device.chromeos.readonly)
Delegate API access to it separately in the Admin Console (Security -> Advanced Settings -> Authentication)
Get your json client secret key and place it with your app (don't include it in your VCS)
Obtain your credentials like this:
credentials = service_account.Credentials.from_service_account_file(
settings.CLIENT_KEY,
scopes=settings.SCOPES,
subject=settings.ADMIN_USER)
where ADMIN_USER is the email address of an authorized Domain admin.
Then you send a GET request like so:
authed_session = AuthorizedSession(credentials)
response = authed_session.get(request_id_url)
This returns a Requests object you can read via response.content.
Hope it helps someone else!
Anyone know if this is possible?
I just want to automate dropping some documents into my onedrive for business account.
I tried
import onedrivesdk
from onedrivesdk.helpers import GetAuthCodeServer
from onedrivesdk.helpers.resource_discovery import ResourceDiscoveryRequest
redirect_uri = 'http://localhost:8080'
client_id = 'appid'
client_secret = 'mysecret'
discovery_uri = 'https://api.office.com/discovery/'
auth_server_url='https://login.live.com/oauth20_authorize.srf?scope=wl.skydrive_update'
#auth_server_url='https://login.microsoftonline.com/common/oauth2/authorize',
auth_token_url='https://login.microsoftonline.com/common/oauth2/token'
http = onedrivesdk.HttpProvider()
auth = onedrivesdk.AuthProvider(http,
client_id,
auth_server_url=auth_server_url,
auth_token_url=auth_token_url)
auth_url = auth.get_auth_url(redirect_uri)
code = GetAuthCodeServer.get_auth_code(auth_url, redirect_uri)
auth.authenticate(code, redirect_uri, client_secret, resource=resource)
# If you have access to more than one service, you'll need to decide
# which ServiceInfo to use instead of just using the first one, as below.
service_info = ResourceDiscoveryRequest().get_service_info(auth.access_token)[0]
auth.redeem_refresh_token(service_info.service_resource_id)
client = onedrivesdk.OneDriveClient(service_info.service_resource_id + '/_api/v2.0/', auth, http)
I registered an APP and got a secret and id. But when I ran this I got scope is invalid errors. Plus it tries to launch a webpage which isn't great for a command line kinda environment. I think this SDK might be outdated as well because originally this script had login.microsoftonline, but that wasn't reachable so I changed it to login.live.com.
I wrote this sample code you posted. You replaced the auth_server_URLwith the authentication URL for Microsoft Account authentication, which can only be used to access OneDrive (the consumer product). You need to continue using the login.microsoftonline.com URL to log into your OneDrive for Business account.
You are correct that this pops up a dialog. However, you can write a little supporting code so that only happens the first time you log into a particular app. Follow these steps (assuming you are using the default implementation of AuthProvider:
Use the sample code above up through the line auth.redeem_refresh_token()
The AuthProvider will now have a Session object, which caches the credentials of the current user and session. Use AuthProvider.save_session() to save the credentials for later.
Next time you start your app, use AuthProvider.load_session() and AuthProvider.refresh_token() to retrieve the previous session and refresh the auth token. This will all be headless.
Take note that the default implementation of SessionBase (found here) uses Pickle and is not safe for product use. Make sure to create a new implementation of Session if you intend to deploy this app to other users.
Onerive's website shows "Not Yet" on "OneDrive SDK for Python" to "OneDrive for Business"
https://dev.onedrive.com/SDKs.htm
The github sample codes did not work for me either, it tried to popup a window of authentication, but IE can not find the address:
http://('https//login.microsoftonline.com/common/oauth2/authorize',)?redirect_uri=http%3A%2F%2Flocalhost%3A8080&client_id=034xxxx9-9xx8-4xxf-bexx-1bc5xxxxbd0c&response_type=code
or removed all the "-" in client id
http://('https//login.microsoftonline.com/common/oauth2/authorize',)?redirect_uri=http%3A%2F%2Flocalhost%3A8080&client_id=034xxxx99xx84xxfbexx1bc5xxxxbd0c&response_type=code
Either way, I got the same result, IE did not show the popup with a line "This page can’t be displayed"
I'm trying to setup endpoints api (with google app engine, python), but I'm having some trouble getting user profile info. API is working, I can create entities through API Explorer on my localhost.
My goal is to allow user to register for my app by providing just an email, and authorizing the app to get the reset of the info from their profile. I have this endpoints method:
#User.method(http_method="POST",
auth_level=endpoints.AUTH_LEVEL.REQUIRED,
allowed_client_ids=[
endpoints.API_EXPLORER_CLIENT_ID
],
scopes=[
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://www.googleapis.com/auth/plus.me',
],
user_required=True,
request_fields=('email',),
response_fields=('id',),
name="register",
path="users")
def UserRegister(self, instance):
logging.info(os.getenv( 'HTTP_AUTHORIZATION' ))
# 'Beared __TOKEN__'
logging.info(endpoints.users_id_token._get_token(None))
# '__TOKEN__'
instance.put()
return instance
This works fine, I receive authorization token and user is created in datastore, but I can't figure out how to get the profile info. If I enter the token in OAuth2 API (through API Explorer):
POST https://www.googleapis.com/oauth2/v2/tokeninfo?access_token=__TOKEN__
I get token info with some data I need { "user_id": "__ID__", "verified_email": true, ...}, and if I use user_id in +API:
GET https://www.googleapis.com/plus/v1/people/__ID__
I can get the rest of the data I need (name, image, etc).
What do I need to do to achieve this in my UserRegister() method? I'd prefer to return just entity ID and do the rest of registration asynchronously, but that's another issue, I'll figure it out (; Just need some guidance how to call other endpoints from my code...
EDIT:
I've managed to figure out how to call other APIs (code on Gist), now only have one issue with Plus API:
I did some queries and eventually got anonymous quota error. Then I added key parameter and set it to WEB_CLIENT_ID or SERVICE_ACCOUNT:
WEB_CLIENT_ID is OAuth2 Client ID (type: Web Application) from console.developers.google.com/apis/credentials,
SERVICE_ACCOUNT is default App Engine service account - MY_APP#appspot.gserviceaccount.com...
and now I'm getting following error:
HttpError: <HttpError 400 when requesting https://www.googleapis.com/plus/v1/people/__VALID_USER_ID__?key=__WEB_CLIENT_ID__or__SERVICE_ACCOUNT__&alt=json returned "Bad Request">
When I use +API explorer I get results as expected:
REQUEST:
https://www.googleapis.com/plus/v1/people/__VALID_USER_ID__?key={YOUR_API_KEY}
RESPONSE:
200 OK + json data for user...
Anyone knows why is this happening?
Why am I getting BadRequest response?
Problem with BadRequest was that I didn't send authorization token... I did try to send it as access_token, but seams like +api docs are outdated - it should be oauth_token. When I included this parameter issue was resolved:
build('plus', 'v1').people().get(userId=user_id, key=SERVICE_ACCOUNT, oauth_token=token).execute()
HINT: Use http://localhost:8001/_ah/api/discovery/v1/apis/, and discoveryRestUrl property it has to see real properties of your API - this is where I found the answer.
oauth_token can be obtained like this:
token = os.getenv('HTTP_AUTHORIZATION').split(" ")[1]
# or like in my question:
token = endpoints.users_id_token._get_token(None)
I'd suggest HTTP_AUTHORIZATION variable, because users_id_token docs state that it's a:
Utility library for reading user information from an id_token.
This is an experimental library that can temporarily be used to extract
a user from an id_token. The functionality provided by this library
will be provided elsewhere in the future.
How to call other API Endpoints?
This is also an answer to my first question:
from googleapiclient.discovery import build
service = build('plus', 'v1')
request = service.people().get(userId=user_id, key=SERVICE_ACCOUNT, oauth_token=token)
response = request.execute()
data = dict(self.response.POST)
Code that worked for me is here.
NOTE: WEB_CLIENT_ID obtained from https://console.developers.google.com/apis/credentials (OAuth2 Client ID of type Web Application) will NOT work in this case. I had to use SERVICE_ACCOUNT - I didn't try to generate one through console, default service account I got from App Engine worked fine.
...things are much clearer now that I got this working. Hope it will help someone else (;
Can anyone advice me on a good library or else how to go about having a Python appengine based application using OAuth to authenticate to another server?
I have an application on appengine that expects user input. I would like the user to be able to upload an image, which I would put in imgur.com and would be able to show to the user back on my page. To be able to do that, I need to be able to authenticate to api.imgur.com hence the question.
Have a look to python-oauth2 project.
A Client example:
import oauth2 as oauth
# Create your consumer with the proper key/secret.
consumer = oauth.Consumer(key="your-twitter-consumer-key",
secret="your-twitter-consumer-secret")
# Request token URL for Twitter.
request_token_url = "http://twitter.com/oauth/request_token"
# Create our client.
client = oauth.Client(consumer)
# The OAuth Client request works just like httplib2 for the most part.
resp, content = client.request(request_token_url, "GET")
print resp
print content
I believe the simplegeo oauth2 does not play well with GAE. Mike Knapp's library on GitHub is nice and simple, no install needed.
maybe you can use imgur-api, http://code.google.com/p/imgur-api/wiki/ImageUploading