Include parameters in return login OAuth2 - django

I am using a third party library to retrieve a token through social networks, which uses python-social-auth-oauth and django-toolkit.
Beyond the normal parameters, I would like to add the list of groups that the user is checked.
Current return:
{"scope":"write read
groups","token_type":"Bearer","expires_in":36000,"refresh_token":"xxx","access_token":"xxx"}

make a custom class, which in the end includes the list of groups.
settings.py
OAUTH2_PROVIDER = {
'OAUTH2_VALIDATOR_CLASS': 'apps.userTest.validator.CustomOAuth2Validator'
}
apps.userTest.validator.CustomOAuth2Validator.py
from datetime import timedelta
from django.conf import settings
from django.utils import timezone
from oauth2_provider.models import AccessToken, RefreshToken
from oauth2_provider.oauth2_validators import OAuth2Validator
from oauth2_provider.settings import oauth2_settings
class CustomOAuth2Validator(OAuth2Validator):
def save_bearer_token(self, token, request, *args, **kwargs):
"""
It's messy. It is 90% code from parent function. I didn't find a way to reduce it.
I tried and I failed :'(
Sin Count += 1
Save access and refresh token, If refresh token is issued, remove old refresh tokens as
in rfc:`6`
"""
if request.refresh_token:
# remove used refresh token
# Copied as is from parent. I don't know why they're even caring to delete this! - Dheerendra
try:
RefreshToken.objects.get(token=request.refresh_token).revoke()
except RefreshToken.DoesNotExist:
assert () # TODO though being here would be very strange, at least log the error
expires = timezone.now() + timedelta(seconds=oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS)
token['expires_in'] = oauth2_settings.ACCESS_TOKEN_EXPIRE_SECONDS
if request.response_type == 'token':
expires = timezone.now() + timedelta(seconds=settings.IMPLICIT_ACCESS_TOKEN_EXPIRES_SECONDS)
token['expires_in'] = settings.IMPLICIT_ACCESS_TOKEN_EXPIRES_SECONDS
if request.grant_type == 'client_credentials':
request.user = None
access_token = AccessToken(
user=request.user,
scope=token['scope'],
expires=expires,
token=token['access_token'],
application=request.client)
access_token.save()
if 'refresh_token' in token:
refresh_token = RefreshToken(
user=request.user,
token=token['refresh_token'],
application=request.client,
)
if request.grant_type == 'authorization_code':
refresh_tokens = RefreshToken.objects.all().filter(user=request.user,
application=request.client).order_by('-id')
if len(refresh_tokens) > 0:
refresh_token = refresh_tokens[0]
# Delete the old access_token
refresh_token.access_token.delete()
if len(refresh_tokens) > 1:
# Enforce 1 token pair. Delete all old refresh_tokens
RefreshToken.objects.exclude(pk=refresh_token.id).delete()
refresh_token.access_token = access_token
refresh_token.save()
token['refresh_token'] = refresh_token.token
token['groups'] = request.user.group_list

Related

Django redirect and modify GET parameters

I am implementing magic tokens and would like clean URLs. As a consequence, I would like to remove the token from the URL upon a successful user authentication. This is my attempt:
def authenticate_via_token(get_response):
def middleware(request):
if request.session.get('authenticated', None):
pass
else:
token = request.GET.get('token', None)
if token:
mt = MagicToken.fetch_by_token(token)
if mt:
request.session['authenticated'] = mt.email
if not request.GET._mutable:
request.GET._mutable = True
request.GET['token'] = None
request.GET._mutable = False
else:
print("invalid token")
response = get_response(request)
return response
return middleware
IE, I would like to send /products/product-detail/3?token=piyMlVMrmYblRwHcgwPEee --> /products/product-detail/3
It's possible that there may be additional GET parameters and I would like to keep them. Any input would be appreciated!
This is the solution I ended up going for:
from django.urls import resolve, reverse
import urllib
def drop_get_param(request, param):
'helpful for redirecting while dropping a specific parameter'
resolution = resolve(request.path_info) #simulate resolving the request
new_params = request.GET.copy() # copy the parameters
del new_params[param] # drop the specified parameter
reversed = reverse(resolution.url_name, kwargs=resolution.kwargs) # create a base url
if new_params: #append the remaining parameters
reversed += '?' + urllib.parse.urlencode(new_params)
return reversed

POST on /oauth/token always use client_secret_basic

I'm trying to add oauth2 (server) on my flask app and I have some issues with the /oauth/token endpoint with client_secret_post.
My app does POST the following to it as a form:
client_id=XXX
client_secret=YYY
grant_type=client_credentials
token_endpoint_auth_method=client_secret_post
redirect_uri=http://localhost:8081/oauth-callback
And I get in the logs:
DEBUG:authlib.oauth2.rfc6749.authenticate_client:Authenticate None via "client_secret_basic" failed
127.0.0.1 - - [23/Jun/2019 18:05:26] "POST /oauth/token HTTP/1.0" 401 -
The token_endpoint_auth_method doesn't seems to change anything and it always returns {"error": "invalid_client"}.
I have tried adding TOKEN_ENDPOINT_AUTH_METHODS = ['client_secret_post'] to my class AuthorizationCodeGrant(grants.AuthorizationCodeGrant): without effects (also none of the loggers print anything).
What did I missed there ?
I have implemented things in my app more or less like the oauth2 flask example, here is some extracts:
app.py:
from app_oauth import config_oauth
...
def create_app(...):
...
config_oauth(app)
...
app_oauth.py:
from authlib.flask.oauth2 import AuthorizationServer, ResourceProtector
from authlib.flask.oauth2.sqla import (
create_query_client_func,
create_save_token_func,
create_revocation_endpoint,
create_bearer_token_validator,
)
from authlib.oauth2.rfc6749 import grants
from werkzeug.security import gen_salt
from models import db, User
from models import OAuth2Client, OAuth2AuthorizationCode, OAuth2Token
from flask import current_app
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
def create_authorization_code(self, client, user, request):
current_app.logger.debug("create auth code")
code = gen_salt(48)
item = OAuth2AuthorizationCode(
code=code,
client_id=client.client_id,
redirect_uri=request.redirect_uri,
scope=request.scope,
user_id=user.get_user_id(),
)
db.session.add(item)
db.session.commit()
return code
def parse_authorization_code(self, code, client):
current_app.logger.debug("parse auth code")
item = OAuth2AuthorizationCode.query.filter_by(
code=code, client_id=client.client_id).first()
if item and not item.is_expired():
return item
def delete_authorization_code(self, authorization_code):
current_app.logger.debug("delete auth code")
db.session.delete(authorization_code)
db.session.commit()
def authenticate_user(self, authorization_code):
current_app.logger.debug("auth user")
return User.query.get(authorization_code.user_id)
class PasswordGrant(grants.ResourceOwnerPasswordCredentialsGrant):
def authenticate_user(self, username, password):
current_app.logger.debug("password grant auth user")
user = User.query.filter_by(name=username).first()
if user.check_password(password):
return user
class RefreshTokenGrant(grants.RefreshTokenGrant):
def authenticate_refresh_token(self, refresh_token):
current_app.logger.debug("refresh token grant")
token = OAuth2Token.query.filter_by(refresh_token=refresh_token).first()
if token and not token.revoked and not token.is_refresh_token_expired():
return token
def authenticate_user(self, credential):
current_app.logger.debug("auth user grant user")
return User.query.get(credential.user_id)
query_client = create_query_client_func(db.session, OAuth2Client)
save_token = create_save_token_func(db.session, OAuth2Token)
authorization = AuthorizationServer(
query_client=query_client,
save_token=save_token,
)
require_oauth = ResourceProtector()
def config_oauth(app):
authorization.init_app(app)
# support all grants
authorization.register_grant(grants.ImplicitGrant)
authorization.register_grant(grants.ClientCredentialsGrant)
authorization.register_grant(AuthorizationCodeGrant)
authorization.register_grant(PasswordGrant)
authorization.register_grant(RefreshTokenGrant)
# support revocation
revocation_cls = create_revocation_endpoint(db.session, OAuth2Token)
authorization.register_endpoint(revocation_cls)
# protect resource
bearer_cls = create_bearer_token_validator(db.session, OAuth2Token)
require_oauth.register_token_validator(bearer_cls())
and my blueprint:
from app_oauth import authorization
...
#bp_api_v1_auth.route("/oauth/token", methods=["POST"])
def oauth_token():
return authorization.create_token_response()
Edit: after digging it looks like it is handled by ClientCredentialsGrant which does only client_secret_basic by default, I then added:
class ClientCredentialsGrant(grants.ClientCredentialsGrant):
TOKEN_ENDPOINT_AUTH_METHODS = [
'client_secret_basic', 'client_secret_post'
]
...
authorization.register_grant(ClientCredentialsGrant)
Which now validates but respond with {"error": "unauthorized_client"}
Finally nailed it: my OAuth2Client entry in database had only authorization_code and password, client_credentials was needed to validate.

Zoho CRM Python SDK v2 initialization problem for Django

Im trying to integrate the Zoho CRM v2 SDK with my Django app.
On the Django runserver, im able to get access tokens and using the refresh method and store them in the zcrm_oauthtokens.pkl file. The sdk then automatically refreshes the access token using the refresh token, so no problem here. However on my production server (heroku) im getting this error message:
2019-01-16T11:07:22.314759+00:00 app[web.1]: 2019-01-16 11:07:22,314 - Client_Library_OAUTH - ERROR - Exception occured while fetching oauthtoken from db; Exception Message::'NoneType' object has no attribute 'accessToken'
It seems to me that the tokens are being saved to file, but when the sdk try to access them it is looking for them in a DB and not the file specified in the token_persistence_path.
In my settings.py I have this:
ZOHO_CLIENT_ID = config('ZOHO_CLIENT_ID')
ZOHO_CLIENT_SECRET = config('ZOHO_CLIENT_SECRET')
ZOHO_REDIRECT_URI = config('ZOHO_REDIRECT_URI')
ZOHO_CURRENT_USER_EMAIL = 'jamesalexander#mylastwill.co.uk'
ZOHO_PATH = os.path.join(BASE_DIR, 'wills_online', 'zoho')
zoho_config = {'apiBaseUrl': "https://www.zohoapis.com",
'currentUserEmail': ZOHO_CURRENT_USER_EMAIL,
'client_id': ZOHO_CLIENT_ID,
'client_secret': ZOHO_CLIENT_SECRET,
'redirect_uri': ZOHO_REDIRECT_URI,
'token_persistence_path': ZOHO_PATH}
and in a views file I have this:
from zcrmsdk import *
import logging
from django.shortcuts import HttpResponse
from wills.models import PersonalDetails, ZoHoRecord, WillDocument
from wills_online.decorators import start_new_thread
from wills_online.settings import zoho_config
logger = logging.getLogger(__name__)
class ZohoRunOnce:
def __init__(self):
self.already_run = False
def run_once(self):
if not self.already_run:
print('zoho init run once')
ZCRMRestClient.initialize(zoho_config)
self.already_run = True
zoho_init = ZohoRunOnce()
zoho_init.run_once()
print(zoho_config['token_persistence_path'])
def zoho_callback():
return HttpResponse(200)
#start_new_thread
def zoho_personal_details(request):
""" updates or create a user account on zoho on profile completion """
personal_details_ob = PersonalDetails.objects.get(user=request.user)
zoho_ob = ZoHoRecord.objects.get(user=request.user)
try:
if zoho_ob.account:
record = ZCRMRecord.get_instance('Accounts', zoho_ob.account)
record.set_field_value('Account_Name', request.user.email)
record.set_field_value('Name', personal_details_ob.full_name)
record.set_field_value('Email', request.user.email)
record.set_field_value('Address_Line_1', personal_details_ob.address_line_1)
record.set_field_value('Address_Line_2', personal_details_ob.address_line_2)
record.set_field_value('Post_Town', personal_details_ob.post_town)
record.set_field_value('Post_Code', personal_details_ob.post_code)
record.set_field_value('Dob_Day', personal_details_ob.dob_day)
record.set_field_value('Dob_Month', personal_details_ob.dob_month)
record.set_field_value('Dob_Year', personal_details_ob.dob_year)
record.set_field_value('Gender', personal_details_ob.sex)
record.set_field_value('Marital_Status', personal_details_ob.marital_status)
record.set_field_value('Partner_Name', personal_details_ob.partner_full_name)
record.set_field_value('Partner_Gender', personal_details_ob.partner_gender)
record.set_field_value('Partner_Email', personal_details_ob.partner_email)
record.set_field_value('Children', personal_details_ob.children)
record.set_field_value('Pets', personal_details_ob.pets)
record.update()
else:
user = ZCRMUser.get_instance(name='James Alexander')
record = ZCRMRecord.get_instance('Accounts')
record.set_field_value('Account_Owner', user)
record.set_field_value('Account_Name', request.user.email)
record.set_field_value('Name', personal_details_ob.full_name)
record.set_field_value('Email', request.user.email)
record.set_field_value('Address_Line_1', personal_details_ob.address_line_1)
record.set_field_value('Address_Line_2', personal_details_ob.address_line_2)
record.set_field_value('Post_Town', personal_details_ob.post_town)
record.set_field_value('Post_Code', personal_details_ob.post_code)
record.set_field_value('Dob_Day', personal_details_ob.dob_day)
record.set_field_value('Dob_Month', personal_details_ob.dob_month)
record.set_field_value('Dob_Year', personal_details_ob.dob_year)
record.set_field_value('Gender', personal_details_ob.sex)
record.set_field_value('Marital_Status', personal_details_ob.marital_status)
record.set_field_value('Partner_Name', personal_details_ob.partner_full_name)
record.set_field_value('Partner_Gender', personal_details_ob.partner_gender)
record.set_field_value('Partner_Email', personal_details_ob.partner_email)
record.set_field_value('Children', personal_details_ob.children)
record.set_field_value('Pets', personal_details_ob.pets)
response = record.create()
# save account id to db for future updates
zoho_ob.account = response.details['id']
zoho_ob.save()
except ZCRMException as ex:
logger.log(1, ex.status_code)
logger.log(1, ex.error_message)
logger.log(1, ex.error_details)
logger.log(1, ex.error_content)
print(ex.status_code)
print(ex.error_message)
print(ex.error_content)
print(ex.error_details)
Ive tried running ZCRMRestClient.initialize(zoho_config) in settings.py, with no luck.
My method for getting the access token and refresh token, which seems to work is:
import os
import pprint
from sys import argv
import django
import requests
import zcrmsdk
from django.conf import settings
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'wills_online.settings')
django.setup()
def zoho_refresh_token(code):
""" supply a self client token from the zoho api credentials from web site """
zoho_config = {"apiBaseUrl": "https://www.zohoapis.com",
"currentUserEmail": settings.ZOHO_CURRENT_USER_EMAIL,
"client_id": settings.ZOHO_CLIENT_ID,
"client_secret": settings.ZOHO_CLIENT_SECRET,
"redirect_uri": settings.ZOHO_REDIRECT_URI,
"token_persistence_path": settings.ZOHO_PATH}
pprint.pprint(zoho_config)
print('working')
address = f'https://accounts.zoho.com/oauth/v2/token?code={code}&redirect_uri={settings.ZOHO_REDIRECT_URI}&client_id={settings.ZOHO_CLIENT_ID}&client_secret={settings.ZOHO_CLIENT_SECRET}&grant_type=authorization_code'
response = requests.post(address)
data = response.json()
pprint.pprint(data)
zcrmsdk.ZCRMRestClient.initialize(zoho_config)
oauth_client = zcrmsdk.ZohoOAuth.get_client_instance()
refresh_token = data['refresh_token']
print(type(refresh_token))
oauth_client.generate_access_token_from_refresh_token(refresh_token, settings.ZOHO_CURRENT_USER_EMAIL)
print(refresh_token)
print('finished')
if name == 'main':
zoho_refresh_token(argv[1])
This is driving me mad. Help would be greatly appreciated. This is my first post so go easy, lol.
For future reference, you will need to define persistence_handler_class and persistence_handler_path in your configuration dictionary. You will also need a handler class and a user-defined model to store the results. Sample code follows:
# settings.py
import zcrmsdk
configuration_dictionary = {
'apiBaseUrl': 'https://www.zohoapis.com',
'apiVersion': 'v2',
'currentUserEmail': ZOHO_CURRENT_USER_EMAIL,
'sandbox': 'False',
'applicationLogFilePath': '',
'client_id': ZOHO_CLIENT_ID,
'client_secret': ZOHO_CLIENT_SECRET,
'redirect_uri': ZOHO_REDIRECT_URI,
'accounts_url': 'https://accounts.zoho.com',
'access_type': 'online',
'persistence_handler_class': ZOHO_HANDLER_CLASS,
'persistence_handler_path': ZOHO_HANDLER_PATH,
}
zcrmsdk.ZCRMRestClient.initialize(configuration_dictionary)
# zoho.models.py
from django.db import models
from zcrmsdk.OAuthClient import ZohoOAuthTokens
class ZohoOAuthHandler:
#staticmethod
def get_oauthtokens(email_address):
oauth_model_instance = ZohoOAuth.objects.get(user_email=email_address)
return ZohoOAuthTokens(oauth_model_instance.refresh_token,
oauth_model_instance.access_token,
oauth_model_instance.expiry_time,
user_email=oauth_model_instance.user_email)
#staticmethod
def save_oauthtokens(oauth_token):
defaults = {
'refresh_token': oauth_token.refreshToken,
'access_token': oauth_token.accessToken,
'expiry_time': oauth_token.expiryTime,
}
ZohoOAuth.objects.update_or_create(user_email=oauth_token.userEmail, defaults=defaults)
class ZohoOAuth(models.Model):
refresh_token = models.CharField(max_length=250)
access_token = models.CharField(max_length=250)
expiry_time = models.BigIntegerField()
user_email = models.EmailField()
In this example ZOHO_HANDLER_CLASS = 'ZohoOAuthHandler' and ZOHO_HANDLER_PATH = 'zoho.models'
The first time you go to use this you will need a grant_token from https://accounts.zoho.com/developerconsole. For the scope use aaaserver.profile.READ,ZohoCRM.modules.ALL to start (see https://www.zoho.com/crm/developer/docs/api/oauth-overview.html#scopes)
Before you can use the api you'll need to run the code below in a django shell. This uses a grant token to generate your initial access and refresh tokens. Afterwards, the api should handle refreshing your access token.
grant_token = GRANT_TOKEN
import zcrmsdk
oauth_client = zcrmsdk.ZohoOAuth.get_client_instance()
oauth_tokens = oauth_client.generate_access_token(grant_token)

set time to live for each session separately KVsession flask

KBsession stores the session TTL based on PERMANENT_SESSION_LIFETIME is there a way to override this for specific sessions
EDIT:
so I have two different API for login I need to give any user login from one of them an infinite session TTL, the other one will take PERMANENT_SESSION_LIFETIME value
note: KBsession back-end is redis
I think the best way is use Session Interface to create specific processing. This is just an example, but I hope you can understand approach.
from flask import Flask, session as flask_session, jsonify
flask_app = Flask(__name__)
# just a few user types
UNIQUE_USER_TYPE = 'unique'
DEFAULT_USER_TYPE = 'default'
#flask_app.route('/login-default')
def login_default():
flask_session['user_type'] = DEFAULT_USER_TYPE
return 'login default done'
#flask_app.route('/login-unique')
def login_unique():
flask_session['user_type'] = UNIQUE_USER_TYPE
return 'login unique done'
#flask_app.route('/session-state')
def get_session_state():
return jsonify(dict(flask_session))
class UserTypeSessionInterface(SecureCookieSessionInterface):
def get_expiration_time(self, app, session):
"""
I just override method. Just demonstration.
It's called from save_session() and open_session()
"""
if session.get('user_type') == UNIQUE_USER_TYPE:
# set 1 hour for unique users
delta = datetime.utcnow() + timedelta(hours=1)
else:
# set 3 hour for default users
delta = datetime.utcnow() + timedelta(hours=3)
# add datetime data into session
session['lifetime'] = delta.strftime('%Y-%m-%dT%H:%M:%S')
return delta
# use our custom session implementation
flask_app.session_interface = UserTypeSessionInterface()
Now run server, open new private window, /login-default and /session-state:
# default behaviour
{
"lifetime": "2018-11-06T16:22:21",
"user_type": "default"
}
Open one more private window, /login-unique and /session-state:
# unique behaviour
{
"lifetime": "2018-11-06T14:25:17",
"user_type": "unique"
}
So, session store tool doesn't matter(redis, cassandra or something else). All what you need is just implement open_session() and save_session():
class YourSessionProcessor(SessionInterface):
def open_session(self, app, request):
# just do here all what you need
pass
def save_session(self, app, session, response):
# just do here all what you need
pass
flask_app.session_interface = YourSessionProcessor()
Also you can use custom session class(just an example):
from flask.sessions import SessionMixin
from werkzeug.datastructures import CallbackDict
class CustomSession(CallbackDict, SessionMixin):
def __init__(self, initial=None, sid=None):
def on_update(self):
self.modified = True
CallbackDict.__init__(self, initial, on_update=on_update)
self.sid = sid
self.modified = False
# YourSessionProcessor
def open_session(self, app, request):
# you can find any useful data in request
# you can find all settings in app.config
sid = request.cookies.get(app.session_cookie_name)
# ... do here everything what you need
return CustomSession(sid=sid)
Hope this helps.

How to get logged in user's uid from session in Django?

I have implemented a registration/login/authentication system using this Django guide.
But, how would I access a user's information from my views so I can send the user's information to a template file?
I want to be able to access a user's ID so I can submit a form with the user's ID attached to the form.
In case anyone wants to actually extract a user ID from an actual Session object (for whatever reason - I did!), here's how:
from django.contrib.sessions.models import Session
from django.contrib.auth.models import User
session_key = '8cae76c505f15432b48c8292a7dd0e54'
session = Session.objects.get(session_key=session_key)
session_data = session.get_decoded()
print session_data
uid = session_data.get('_auth_user_id')
user = User.objects.get(id=uid)
Credit should go to Scott Barnham
There is a django.contrib.auth.models.User object attached to the request; you can access it in a view via request.user. You must have the auth middleware installed, though.
This:
def view(request):
if request.user.is_authenticated:
user = request.user
print(user)
# do something with user
An even easier way to do this is to install django-extensions and run the management command print_user_for_session.
And this is how they do it:
https://github.com/django-extensions/django-extensions/blob/master/django_extensions/management/commands/print_user_for_session.py
In case hwjp solution doesn't work for you ("Data is corrupted"), here is another solution:
import base64
import hashlib
import hmac
import json
def session_utoken(msg, secret_key, class_name='SessionStore'):
key_salt = "django.contrib.sessions" + class_name
sha1 = hashlib.sha1((key_salt + secret_key).encode('utf-8')).digest()
utoken = hmac.new(sha1, msg=msg, digestmod=hashlib.sha1).hexdigest()
return utoken
def decode(session_data, secret_key, class_name='SessionStore'):
encoded_data = base64.b64decode(session_data)
utoken, pickled = encoded_data.split(b':', 1)
expected_utoken = session_utoken(pickled, secret_key, class_name)
if utoken.decode() != expected_utoken:
raise BaseException('Session data corrupted "%s" != "%s"',
utoken.decode(),
expected_utoken)
return json.loads(pickled.decode('utf-8'))
s = Session.objects.get(session_key=session_key)
decode(s.session_data, 'YOUR_SECRET_KEY'))
credit to: http://joelinoff.com/blog/?p=920