The access token for google typically lasts 1 hour. How do I refresh the token without having the user login again?
github code for project: https://github.com/theptrk/learn_gcal
django-allauth Google login is set up the normal way with an added scope
THIRD_PARTY_APPS = [
# ...
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
# This allows you to assign a site to a "social application"
SITE_ID = 1
SOCIALACCOUNT_PROVIDERS = {
"google": {
"SCOPE": [
# minimum scopes
"profile",
"email",
# additional scopes
"https://www.googleapis.com/auth/calendar.readonly",
],
"AUTH_PARAMS": {
"access_type": "offline",
},
}
}
# https://django-allauth.readthedocs.io/en/latest/configuration.html
# Use this for additional scopes: This defaults to false
SOCIALACCOUNT_STORE_TOKENS = True
Creating the credentials is different than the docs since the docs use a "secrets file" instead of getting tokens from the db
# get user and client credentials
token = SocialToken.objects.get(
account__user=request.user, account__provider="google"
)
client_id = env("GOOGLE_CLIENT_ID", default="")
client_secret = env("GOOGLE_CLIENT_SECRET", default="")
# create Credentials object
from google.oauth2.credentials import Credentials
creds=Credentials(
token=token.token,
refresh_token=token.token_secret,
token_uri="https://oauth2.googleapis.com/token",
client_id=client_id,
client_secret=client_secret,
)
Trying to retrieve events is totally fine if the access token is not expired
# retrieve google calendar events
from googleapiclient.discovery import build
from datetime import datetime
service = build("calendar", "v3", credentials=creds)
try:
events_result = (
service.events()
.list(
calendarId="primary",
timeMin=datetime.now().date().isoformat() + "Z",
maxResults=30,
singleEvents=True,
orderBy="startTime",
)
.execute()
)
except Exception as e:
print("Google API Error")
if e.status_code == 403:
if e.reason == "Request had insufficient authentication scopes.":
print("user needs to grant calendar scopes")
print("Token is likely expired at this point")
# I've tried:
# creds.refresh(...) but it hasnt worked
Related
I am failing to send over my personal banking data via a flask webhook from the Nordigen API to Dialogflow via fulfilment as only null is being received within the Dialogflow payload:
{
"fulfillmentText": "Your text response",
"fulfillmentMessages": [
{
"payload": [
null
]
}
]
}
The webhook error message is: Webhook call failed. Error: Failed to parse webhook JSON response: Expect a map object but found: [null].
When I just send the data as a fulfillmentText I receive "fulfillmentText": null.
I have tested my webhook with postman and there - as well as locally and other webhook'esque tests - everything is fine as I receive my most recent banking data.
The overall flow is twofold and simple:
User gets the correct banking and user specific login link to a specified bank, copy & pastes it to perform banking login by query_text = 'login'.
After a successful banking login the user can fetch different kinds of banking data (like balance) by query_text = 'balance'.
I went crazy with overengineering the flask webhook as I tried out many different things like asynchronous functions, uploading my Flask app to Heroku or CORS. I have even implemented an OAuth2 process where the user would query_text = 'google auth' and initiate the OAuth2 process in a step 0) by creating OAuth2 credentials and the Flask-Dance Python package. (Even though I have hardcoded the OAuth2 redirect link but this shouldn't be an issue atm). I was even trying to trick Dialogflow by creating a small Sqlite3 db within my webhook to at least upload the data there and then use it but without success.
So my question is .. what am I missing here? Why do I receive my banking data everywhere else but not in Dialogflow. My intuition is telling me Google is blocking this data for whatever reason.
Honestly I just don't know how to continue and I would appreciate any helpful comments!
This is my Flask webhook:
from dialogflow_fulfillment import QuickReplies, WebhookClient, Payload
from flask import Flask, request, jsonify, make_response, session, render_template, redirect, url_for
from flask_cors import CORS, cross_origin
import json
from json import JSONEncoder
import os
import asyncio
import requests
import sqlite3
from app.src.handler_login import handler_login
from app.src.handler_balance import handler_balance
from app.banking_data.init_db import create_connection
from flask_dance.contrib.google import make_google_blueprint, google
from oauthlib.oauth2.rfc6749.errors import InvalidGrantError, TokenExpiredError, OAuth2Error
from google.cloud import dialogflow_v2beta1 as dialogflow
from google.oauth2 import service_account
from uuid import uuid4
from nordigen import NordigenClient
# NORDIGEN
# Credentials
secret_id="XXX"
secret_key="XXX"
# Configuration
institution_id = "XXX"
app = Flask(__name__)
# set Flask secret key
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "supersekrit")
# GOOGLE API & AUTHENTICATION
app.config["GOOGLE_OAUTH_CLIENT_ID"] = "XXX"
app.config["GOOGLE_OAUTH_CLIENT_SECRET"] = "XXX"
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = "1"
os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] = "1"
google_bp = make_google_blueprint(scope=["profile", "email"])
app.register_blueprint(google_bp, url_prefix="/login")
app.config['CORS_HEADERS'] = 'Content-Type'
cors = CORS(app, supports_credentials=True, resources={r"/webhook": {"origins": "*"}})
client = NordigenClient(
secret_id=secret_id,
secret_key=secret_key
)
client.generate_token()
# subclass JSONEncoder
class setEncoder(JSONEncoder):
def default(self, obj):
return list(obj)
#app.route("/")
def index():
if not google.authorized:
return redirect(url_for("google.login"))
try:
resp = google.get("/oauth2/v1/userinfo")
assert resp.ok, resp.text
return "You are {email} on Google".format(email=resp.json()["email"])
except (InvalidGrantError, TokenExpiredError) as e: # or maybe any OAuth2Error
return redirect(url_for("google.login"))
#app.route('/webhook', methods=['GET', 'POST', 'OPTION'])
async def webhook():
"""Handles webhook requests from Dialogflow."""
req = request.get_json(force=True)
query_text = req.get('queryResult').get('queryText')
if query_text:
if query_text == 'google auth':
if not google.authorized:
auth_link = 'MY HARD CODED GOOGLE AUTHENTICATION LINK HERE'
auth_link = {
"fulfillmentText": auth_link,
"source": 'webhook'
}
return auth_link
try:
resp = google.get("/oauth2/v1/userinfo")
assert resp.ok, resp.text
return "You are {email} on Google".format(email=resp.json()["email"])
except (InvalidGrantError, TokenExpiredError) as e: # or maybe any OAuth2Error
auth_link = 'MY HARD CODED GOOGLE AUTHENTICATION LINK HERE'
auth_link = {
"fulfillmentText": auth_link,
"source": 'webhook'
}
return auth_link
if query_text == 'login':
link = await handler_login(client, institution_id, session)
link = {
"fulfillmentText": link,
"source": 'webhook'
}
link = make_response(jsonify(link))
link.headers.add('Access-Control-Allow-Origin', '*')
return link
if query_text == 'balance':
balance = await handler_balance(client, session)
balance = {
"fulfillmentText": "Your text response",
"fulfillmentMessages": [
{
"text": {
"text": [
"Your text response"
]
}
},
{
"payload": {
balance
}
}
]
}
balance = json.dumps(balance, indent=4, cls=setEncoder)
balance = make_response(balance)
return balance
if __name__ == "__main__":
app.run(debug=True)
Here are two helper functions I have created that perform the creation of the login link the the fetching of my banking data via Nordigen:
from uuid import uuid4
async def handler_login(client, institution_id, session):
"""Handles the webhook request."""
# Initialize bank session
init = client.initialize_session(
# institution id
institution_id=institution_id,
# redirect url after successful authentication
redirect_uri="https://nordigen.com",
# additional layer of unique ID defined by you
reference_id=str(uuid4())
)
link = init.link
session["req_id"] = init.requisition_id
return link
async def handler_balance(client, session):
if "req_id" in session:
# Get account id after you have completed authorization with a bank
# requisition_id can be gathered from initialize_session response
#requisition_id = init.requisition_id
accounts = client.requisition.get_requisition_by_id(
requisition_id=session["req_id"]
)
# Get account id from the list.
account_id = accounts["accounts"][0]
#account_id = accounts["id"]
# Create account instance and provide your account id from previous step
account = client.account_api(id=account_id)
# Fetch account metadata
#meta_data = account.get_metadata()
# Fetch details
#details = account.get_details()
# Fetch balances
balance = account.get_balances()
balance = balance["balances"][0]
balance = balance["balanceAmount"]["amount"]
#balance = json.loads(balance)
# Fetch transactions
#transactions = account.get_transactions()
#agent.add(Payload({'balance': balance}))
return balance
Feel free to comment if you need any more input!
I want to implement the same scenario for use in Android Kotlin which is given in this url.
For Web Application
I have created login with google for web application by follow this link. Here is my Google Login View in views.py for access token (as one user have explained here )
class GoogleLogin(SocialLoginView):
adapter_class = GoogleOAuth2Adapter
And It's working for me as I expected.
For Android Application
Now, Somehow I have managed a code for this google scenario.
Here is my Google Client login View.view.py code
class GoogleClientView(APIView):
def post(self, request):
token = {'id_token': request.data.get('id_token')}
print(token)
try:
# Specify the CLIENT_ID of the app that accesses the backend:
idinfo = id_token.verify_oauth2_token(token['id_token'], requests.Request(), CLIENT_ID)
print(idinfo)
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise ValueError('Wrong issuer.')
return Response(idinfo)
except ValueError as err:
# Invalid token
print(err)
content = {'message': 'Invalid token'}
return Response(content)
When I am requesting POST method with IdToken then this is providing below information and ofcourse we need this.
{
// These six fields are included in all Google ID Tokens.
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"azp": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
"aud": "1008719970978-hb24n2dstb40o45d4feuo2ukqmcc6381.apps.googleusercontent.com",
"iat": "1433978353",
"exp": "1433981953",
// These seven fields are only included when the user has granted the "profile" and
// "email" OAuth scopes to the application.
"email": "testuser#gmail.com",
"email_verified": "true",
"name" : "Test User",
"picture": "https://lh4.googleusercontent.com/-kYgzyAWpZzJ/ABCDEFGHI/AAAJKLMNOP/tIXL9Ir44LE/s99-c/photo.jpg",
"given_name": "Test",
"family_name": "User",
"locale": "en"
}
Till here, everything is working good for me.But here I want to create account/login for a the user using above info and when account/login is created then need a key in return response.
below What I had tried
class GoogleClientView(APIView):
permission_classes = [AllowAny]
adapter_class = GoogleOAuth2Adapter
callback_url = 'https://example.com/user/accounts/google/login/callback/'
client_class = OAuth2Client
def post(self, request):
token = {'id_token': request.data.get('id_token')}
print(token)
try:
# Specify the CLIENT_ID of the app that accesses the backend:
idinfo = id_token.verify_oauth2_token(token['id_token'], requests.Request(), CLIENT_ID)
print(idinfo)
if idinfo['iss'] not in ['accounts.google.com', 'https://accounts.google.com']:
raise ValueError('Wrong issuer.')
return Response(idinfo)
except ValueError as err:
# Invalid token
print(err)
content = {'message': 'Invalid token'}
return Response(content)
Again It seems it's not creating any account/login for user and no key is getting in response.
So, for creating account/login, How can I implement the code?
Please ask for more information If I forgot to put any.
I am trying to implement SSO for one of my applications using flask-login and flask-dance. As a starting point I am using sample code given on Flask Dance website - https://flask-dance.readthedocs.io/en/v1.2.0/quickstarts/sqla-multiuser.html
Only change I did was - I replaced GitHub with my Azure AD credentials
Please find the code below:
import sys
from flask import Flask, redirect, url_for, flash, render_template
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.orm.exc import NoResultFound
from flask_dance.contrib.github import make_github_blueprint, github
from flask_dance.contrib.azure import make_azure_blueprint, azure
from flask_dance.consumer.storage.sqla import OAuthConsumerMixin, SQLAlchemyStorage
from flask_dance.consumer import oauth_authorized, oauth_error
from flask_login import (
LoginManager, UserMixin, current_user,
login_required, login_user, logout_user
)
# setup Flask application
app = Flask(__name__)
app.secret_key = "XXXXXXXXXXXXXX"
blueprint = make_azure_blueprint(
client_id="XXXXXXXXXXXXXXXXXXXXX",
client_secret="XXXXXXXXXXXXXXXXXXXXXXXX",
tenant="XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
)
app.register_blueprint(blueprint, url_prefix="/login")
# setup database models
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///multi.db"
db = SQLAlchemy()
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
# Your User model can include whatever columns you want: Flask-Dance doesn't care.
# Here are a few columns you might find useful, but feel free to modify them
# as your application needs!
username = db.Column(db.String(1028), unique=True)
email = db.Column(db.String(1028), unique=True)
name = db.Column(db.String(1028))
class OAuth(OAuthConsumerMixin, db.Model):
provider_user_id = db.Column(db.String(1028), unique=True)
user_id = db.Column(db.Integer, db.ForeignKey(User.id))
user = db.relationship(User)
# setup login manager
login_manager = LoginManager()
login_manager.login_view = 'azure.login'
#login_manager.user_loader
def load_user(user_id):
#print(User.query.get(int(user_id)))
return User.query.get(int(user_id))
# setup SQLAlchemy backend
blueprint.storage = SQLAlchemyStorage(OAuth, db.session, user=current_user,user_required=False)
# create/login local user on successful OAuth login
#oauth_authorized.connect_via(blueprint)
def azure_logged_in(blueprint, token):
if not token:
#print(token)
flash("Failed to log in with azure.", category="error")
return False
resp = blueprint.session.get("/user")
if not resp.ok:
#print(resp)
msg = "Failed to fetch user info from Azure."
flash(msg, category="error")
return False
azure_info = resp.json()
azure_user_id = str(azure_info["id"])
#print(azure_user_id)
# Find this OAuth token in the database, or create it
query = OAuth.query.filter_by(
provider=blueprint.name,
provider_user_id=azure_user_id,
)
try:
oauth = query.one()
except NoResultFound:
oauth = OAuth(
provider=blueprint.name,
provider_user_id=azure_user_id,
token=token,
)
if oauth.user:
login_user(oauth.user)
flash("Successfully signed in with Azure.")
else:
# Create a new local user account for this user
user = User(
# Remember that `email` can be None, if the user declines
# to publish their email address on GitHub!
email=azure_info["email"],
name=azure_info["name"],
)
# Associate the new local user account with the OAuth token
oauth.user = user
# Save and commit our database models
db.session.add_all([user, oauth])
db.session.commit()
# Log in the new local user account
login_user(user)
flash("Successfully signed in with Azure.")
# Disable Flask-Dance's default behavior for saving the OAuth token
return False
# notify on OAuth provider error
#oauth_error.connect_via(blueprint)
def azure_error(blueprint, error, error_description=None, error_uri=None):
msg = (
"OAuth error from {name}! "
"error={error} description={description} uri={uri}"
).format(
name=blueprint.name,
error=error,
description=error_description,
uri=error_uri,
)
flash(msg, category="error")
#app.route("/logout")
#login_required
def logout():
logout_user()
flash("You have logged out")
return redirect(url_for("index"))
#app.route("/")
def index():
return render_template("home.html")
# hook up extensions to app
db.init_app(app)
login_manager.init_app(app)
if __name__ == "__main__":
if "--setup" in sys.argv:
with app.app_context():
db.create_all()
db.session.commit()
print("Database tables created")
else:
app.run(debug=True,port=5011)
I have also done appropriate changes in HTML file for 'azure.login'.
So after running it as python multi.py --setup database tables are getting created
and after I run python multi.py Oauth dance is actually starting but in the end I am getting error like below:
HTTP Response:
127.0.0.1 - - [28/Oct/2020 10:17:44] "?[32mGET /login/azure/authorized?code=0.<Token>HTTP/1.1?[0m" 302 -
127.0.0.1 - - [28/Oct/2020 10:17:44] "?[37mGET / HTTP/1.1?[0m" 200 -
Am I missing something? Is it a good idea to use Flask Dance and Flask Login to have SSO with Azure AD? Or I should go with MSAL only along with Flask Session?
Kindly give your valuable inputs..
Since you use Azure AD as the Flask dance provider, we need to use Microsoft Graph to get user's information. The URL should be https://graph.microsoft.com/v1.0/me. So please update the code resp = blueprint.session.get("/user") to resp = blueprint.session.get("/v1.0/me") in method azure_logged_in. Besides, please note that the azure ad user's information has different names. We also need to update the code about creating users.
For example
#oauth_authorized.connect_via(blueprint)
def azure_logged_in(blueprint, token):
if not token:
# print(token)
flash("Failed to log in with azure.", category="error")
return False
resp = blueprint.session.get("/v1.0/me")
# azure.get
if not resp.ok:
# print(resp)
msg = "Failed to fetch user info from Azure."
flash(msg, category="error")
return False
azure_info = resp.json()
azure_user_id = str(azure_info["id"])
# print(azure_user_id)
# Find this OAuth token in the database, or create it
query = OAuth.query.filter_by(
provider=blueprint.name,
provider_user_id=azure_user_id,
)
try:
oauth = query.one()
except NoResultFound:
oauth = OAuth(
provider=blueprint.name,
provider_user_id=azure_user_id,
token=token,
)
if oauth.user:
login_user(oauth.user)
flash("Successfully signed in with Azure.")
else:
# Create a new local user account for this user
user = User(
# create user with user information from Microsoft Graph
email=azure_info["mail"],
username=azure_info["displayName"],
name=azure_info["userPrincipalName"]
)
# Associate the new local user account with the OAuth token
oauth.user = user
# Save and commit our database models
db.session.add_all([user, oauth])
db.session.commit()
# Log in the new local user account
login_user(user)
flash("Successfully signed in with Azure.")
# Disable Flask-Dance's default behavior for saving the OAuth token
return False
For more details, please refer to here and here
I'm trying to create user login authentication in my django app via Active Directory using django-auth-ldap. The problem is that I cannot bind to the AD using username (which is sAMAccountName LDAP equivalent). Part of my settings.py below:
import ldap
from django_auth_ldap.config import LDAPSearch
AUTHENTICATION_BACKENDS = [
'django_auth_ldap.backend.LDAPBackend',
]
AUTH_LDAP_START_TLS = False
AUTH_LDAP_ALWAYS_UPDATE_USER = False
AUTH_LDAP_SERVER_URI = 'ldap://ip_address:389'
AUTH_LDAP_BIND_DN = ''
AUTH_LDAP_BIND_PASSWORD = ''
AUTH_LDAP_USER_SEARCH = LDAPSearch('DC=example,DC=com', ldap.SCOPE_SUBTREE, '(sAMAccountName=%(user)s)')
AUTH_LDAP_CONNECTION_OPTIONS = {
ldap.OPT_REFERRALS: 0,
}
Console log:
ERROR search_s('DC=example,DC=com', 2, '(sAMAccountName=user)') raised OPERATIONS_ERROR({'desc': 'Operations error', 'info': '00000000: LdapErr: DSID-0C090627, comment: In order to perform this operation a successful bind must be completed on the connection., data 0, vece'})
DEBUG search_s('DC=example,DC=com', 2, '(sAMAccountName=%(user)s)') returned 0 objects:
DEBUG Authentication failed for user: failed to map the username to a DN.
Any idea why this is not working?
Anonymous read access is not enabled by default. To perform the search operation, populate AUTH_LDAP_BIND_DN and AUTH_LDAP_BIND_PASSWORD with a valid account. I generally create dedicated "system" accounts (i.e. not a real person's account because your authentication starts failing every time the user changes their password).
I see that it is possible to list all members that belong to a specific group, as documented here:
https://developers.google.com/admin-sdk/directory/v1/guides/manage-group-members#get_all_members
But is it possible to get a list of groups that a user belongs to? I was hoping that Users.get would contain this information, or the Members API would have something similar, but I don't see it.
So I've found the solution in the Developer Guide, although it does not exist in the API documentation!!
https://developers.google.com/admin-sdk/directory/v1/guides/manage-groups#get_all_member_groups
The best way to achieve this is by using the google admin sdk api.
groups.list()
groups.list() with details
For example if you want to do it using the python sdk, you can use the following
from __future__ import print_function
import logging
import os.path
import csv
import json
from googleapiclient.discovery import build
from google_auth_oauthlib.flow import InstalledAppFlow
from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
# If modifying these scopes, delete the file token.json.
### https://developers.google.com/admin-sdk/directory/v1/guides/authorizing
### https://developers.google.com/admin-sdk/directory/v1/quickstart/python
SCOPES = ['https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.group']
"""
########################################################################################################################################################
# Logging level set for the script
# https://realpython.com/python-logging/
########################################################################################################################################################
"""
logging.basicConfig(level=logging.INFO)
class GSuite_management(object):
"""
########################################################################################################################################################
# GSuite_management CLASS
# --> This class will have methods to manage the memebers of organization using Gsuite Admin SDK
########################################################################################################################################################
"""
service = None
def __init__(self):
"""
GSuite_management Constrouctor
"""
creds = None
# The file token.json stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
if os.path.exists('token.json'):
creds = Credentials.from_authorized_user_file('token.json', SCOPES)
# If there are no (valid) credentials available, let the user log in.
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(
'credentials.json', SCOPES)
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
with open('token.json', 'w') as token:
token.write(creds.to_json())
self.service = build('admin', 'directory_v1', credentials=creds)
def list_all_groups_a_user_is_a_part_of(self, userEmail):
"""
This method will list all the groups a user is a part of and return them as a list
"""
listOfEmailGroups=[]
try:
results = self.service.groups().list(domain="yourdomain.com",userKey=userEmail, maxResults=400).execute()
logging.debug(results)
groups = results.get('groups', [])
if not groups:
print('No groups in the domain.')
else:
for group in groups:
logging.info(u'{0} {1} {2}'.format(group['email'],group['name'], group['directMembersCount']))
listOfEmailGroups.append(group['email'])
except Exception as e:
logging.error("Exiting!!!")
SystemExit(e)
return listOfEmailGroups
We can use any google-admin-sdk library like google-api-nodejs-client to query groups of a particular user
This method is using GET https://admin.googleapis.com/admin/directory/v1/groups?userKey=userkey endpoint linked in jekennedy's answer
/**
* Retrieve all groups for a member
*
* #param {google.auth.OAuth2} auth An authorized OAuth2 client.
* #param {string} userEmail primary email of the user
*/
function listGroupsOfMember(auth, userEmail) {
const admin = google.admin({version: 'directory_v1', auth});
admin.groups.list({
userKey: userEmail,
maxResults: 10,
orderBy: 'email',
}, (err, res) => {
if (err) {
console.log(err)
return console.error('The API returned an error:', err.message);
}
const groups = res.data.groups;
if (groups.length) {
console.log('Groups:');
groups.forEach((group) => {
console.log(group);
});
} else {
console.log('No groups found.');
}
});
}
Example usage:
const {google} = require('googleapis');
const path = require('path');
// acquire an authentication client using a service account
const auth = await google.auth.getClient({
keyFile: path.join(__dirname, '../serviceaccount.json'),
scopes: [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.group',
],
});
// impersonate super admin
auth.subject = "admin#foo.me"
listGroupsOfMember(auth, "username#foo.me")