I'm using gspread in a python app to access some backend Google Sheets that act as a database for the application. The user is required to log into the app using an in app browser and an authlib OAuth2 session is created using the token returned in the url via this method. The initial log in and access works fine, but when the token times out after an hour I access the sheets anymore and the app breaks.
I can't work out how to refresh the token using authlib, the reason we picked the library is because it integrated with gspread and the it was supposed to autorefresh auth tokens for you. The code that I use for login is below but after an hour the gspread function open_by_key() fails because of the authentication problem.
I have tried recreating the gspread client, but that just uses the same token. I feel like there should be a way to refresh the token to extend its life for another hour but I can't find how to do that with authlib.
creds = OAuth2Session(clientId, clientSecret, scope=scope, redirect_uri=redirectUrl)
authUrl, state = creds.create_authorization_url('https://accounts.google.com/o/oauth2/v2/auth', response_type='token')
Load the authURL in the browser and get user to log in. Browser returns the authentication response as urlString
creds.fetch_access_token(authorization_response=urlString)
gc = gspread.Client(None, creds)
ss = gc.open_by_key(sheetKey)
todaysSheetName = datetime.datetime.now().strftime('%d-%m-%Y')
wks = ss.worksheet(todaysSheetName)
authUrl, state = creds.create_authorization_url('https://accounts.google.com/o/oauth2/v2/auth', response_type='token')
According to this code you are using, response_type=token is an implicit grant type flow. I guess there won't be a refresh_token in token with implicit flow. Here are my suggestions:
try change it to authorization code flow
use a database to save tokens
if the token is expired, use your previously saved token's refresh_token to exchange a new token.
The refresh_token method is simple:
refresh_token(url=None, refresh_token=None, body='', auth=None, headers=None, **kwargs)
Using your code, it should be:
refresh_token = your_previous_token.refresh_token
new_token = creds.refresh_token('https://accounts.google.com/...', refresh_token=refresh_token)
The problem is that there is no documentation on this part. But according to the API, it would be used like this. If it is not working, you can report it to Authlib issue tracker.
Related
I need to call other cloud functions/cloud run services from my cloud function. I would like to be authenticated if possible so to that effect I have been looking into how to create an AuthorizedSession using the credentials I get back from google.auth.default. My current code looks like so:
credentials, _ = google.auth.default(scopes=[
SERVICE_A_URL,
SERVICE_B_URL,
SERVICE_C_URL,
])
return AuthorizedSession(credentials)
When running this I get the following:
google.auth.exceptions.RefreshError: ('No access token in response.', {'id_token': '[ID_TOKEN]'})
Does anyone know how to get the AuthorizedSession to accept my credentials?
Authorized session works well with Google Cloud APIs which expect an access token. In the case of Cloud Functions and Cloud Run (and also App Engine behind IAP) you need to provide an identity token.
So, you can't with authorized session, you need to generate and ID token with the correct audience and then to add it in the authorization header of your request.
credentials, project_id = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
credentials.with_target_audience("SERVICE_A_URL")
from google.auth.transport import requests
credentials.refresh(requests.Request())
print(credentials.token)
request.get("SERVICE_A_URL+Parameters", headers={'Authorization': 'bearer {}'.format(credentials.token)})
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've purchased MS Office and they give me a space on onedrive.
Now I want to operate with that via ms graph API in my python console application (I used python onedrivesdk before but now it is said that it is deprecated).
I have registered an application and can see it via Azure AD on portal.azure.com.
Currently I'm trying to interact with my onedrive like this:
tenant_id = 'xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxx'
authority = f'https://login.microsoftonline.com/{tenant_id}'
scope = 'https://graph.microsoft.com/.default'
app = msal.ConfidentialClientApplication(self.client_id, authority=authority, client_credential=self.client_secret)
result = app.acquire_token_silent(["https://graph.microsoft.com/.default"], account=None)
if not result:
logging.info("No suitable token exists in cache. Let's get a new one from AAD.")
result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
authToken = result['access_token']
#### Make a call to the graph API
graphResponse = requests.get('https://graph.microsoft.com/beta/me/drive',headers={'Authorization':f'Bearer {authToken}'})
if graphResponse.status_code != 200:
print('Error code: ', graphResponse.status_code)
print(graphResponse.text)
I successfully get an access token, but when I try to call /me/drive
I get status_code = 400 with
Current authenticated context is not valid for this request. This
occurs when a request is made to an endpoint that requires user
sign-in. For example, /me requires a signed-in user. Acquire a token
on behalf of a user to make requests to these endpoints. Use the
OAuth 2.0 authorization code flow for mobile and native apps and the
OAuth 2.0 implicit flow for single-page web apps
I've add permissions for the application on the portal via "API permission -> Add permission", but I'm unable to grant admin consent (In another ms account I have full fledged azure subscription where I'm an admin) because I'm not an admin. But who is admin of this account my MS office assigned to?
According to the code you provided, you use OAuth 2.0 client credentials flow to complete Azure AD auth and get access token. The access token required by service principal. We cannot use the access token to call /me/drive endpoint. We just can use the access token to call /users/<UserObjectIdOrUserPrincipalName}>/drive endpoint. For more details, please refer to the document
So if you want to call /me/drive endpoint, I suggest you use the OAuth 2.0 authorization code flow. Regarding how to implement it in your application, please refer to the sample.
I'm trying to use the Lyft rides python API to access Lyft data. Specifically, I'm trying to access the ride estimate endpoint .
from lyft_rides.auth import ClientCredentialGrant
from lyft_rides.session import Session
from lyft_rides.client import LyftRidesClient
auth_flow = ClientCredentialGrant(client_id=MY_ID, client_secret=MY_SECRET, scopes="public")
session = auth_flow.get_session()
client = LyftRidesClient(session)
response = client.get_cost_estimates(start_latitude=start_lat, start_longitude=start_long, end_latitude=end_lat, end_longitude=end_long)
However, the surge rate in the response data is always 0, even during surge hours, and I've diagnosed that it's because I'm not utilizing the 3-legged authentication.
From the lyft developer docs,
3-Legged flow for accessing user-specific endpoints.
To make ride
requests or otherwise access user data, the user must grant you
access. Users who don't have a Lyft account will be prompted to create
a new account if they are directed through the following flow.
From the python docs,
Authorization
If you need access to a Lyft user’s account in order to make requests
on their behalf, you will go through a “3-legged” flow. In this case,
you will need the user to grant access to your application through the
OAuth 2.0 Authorization Code flow. See Lyft API docs.
The Authorization Code flow is a two-step authorization process. The
first step is having the user authorize your app and the second
involves requesting an OAuth 2.0 access token from Lyft. This process
is mandatory if you want to take actions on behalf of a user or access
their information.
from lyft_rides.auth import AuthorizationCodeGrant
auth_flow = AuthorizationCodeGrant(
YOUR_CLIENT_ID,
YOUR_CLIENT_SECRET,
YOUR_PERMISSION_SCOPES,
)
auth_url = auth_flow.get_authorization_url()
Navigate the user to the auth_url where they can grant access to your
application. After, they will be redirected to a redirect_url with the
format REDIRECT_URL?code=UNIQUE_AUTH_CODE. Use this redirect_url to
create a session and start LyftRidesClient.
session = auth_flow.get_session(redirect_url)
client = LyftRidesClient(session)
credentials = session.oauth2credential
Keep credentials information in a secure data store and reuse them to
make API calls on behalf of your user. The SDK will handle the token
refresh for you automatically when it makes API requests with a
LyftRidesClient.
Question
I'm trying to automate the python request within a script. Given that the 3rd leg of the authentication requires manually visiting a url and obtaining a code, is it possible to do this through a script?
[Full Disclosure: I'm one of Lyft's Developer Advocates]
The only way to get that data is by requesting therides.request scope is through the 3-legged OAuth flow (sorry about that). However, you only need to request this external authorization once if you ask for the offline scope as part of the initial authorization. If you have that scope requested initially, you can use refresh_tokens as outlined here and not get prompted for the external URL:
https://developer.lyft.com/docs/authentication#section-step-5-refreshing-the-access-token
If you're only using this script locally I'd recommend going through this authorization once and then building in refresh token logic into your script if your token has expired. Hope that helps!
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"