django all-auth enable 2FA using email - django-allauth

As per the installation instructions at https://django-allauth-2FA.readthedocs.io/en/latest/ have implemented all steps and have no idea how to enable email based OTP for login for existing users. Even added a manual adapter.py as below:
from allauth.account.adapter import DefaultAccountAdapter
from urllib.parse import urlencode
from allauth.account.adapter import DefaultAccountAdapter
from allauth.exceptions import ImmediateHttpResponse
from django.http import HttpResponseRedirect
from django.urls import reverse
from allauth_2fa.utils import user_has_valid_totp_device
class NoNewUsersAccountAdapter(DefaultAccountAdapter):
"""
Adapter to disable allauth new signups
Used at equilang/settings.py with key ACCOUNT_ADAPTER
https://django-allauth.readthedocs.io/en/latest/advanced.html#custom-redirects """
def is_open_for_signup(self, request):
"""
Checks whether or not the site is open for signups.
Next to simply returning True/False you can also intervene the
regular flow by raising an ImmediateHttpResponse
"""
return False
def has_2fa_enabled(self, user):
"""Returns True if the user has 2FA configured."""
return user_has_valid_totp_device(user)
def login(self, request, user):
# Require two-factor authentication if it has been configured.
if self.has_2fa_enabled(user):
# Cast to string for the case when this is not a JSON serializable
# object, e.g. a UUID.
request.session["allauth_2fa_user_id"] = str(user.id)
redirect_url = reverse("two-factor-authenticate")
# Add "next" parameter to the URL.
view = request.resolver_match.func.view_class()
view.request = request
success_url = view.get_success_url()
query_params = request.GET.copy()
if success_url:
query_params[view.redirect_field_name] = success_url
if query_params:
redirect_url += "?" + urlencode(query_params)
raise ImmediateHttpResponse(response=HttpResponseRedirect(redirect_url))
# Otherwise defer to the original allauth adapter.
return super().login(request, user)
But no OTP's are fired. When I manually created a TOTP device a window appears for asking OTP after login but don't know where and how the email with OTP is sent.
Any help or lead please

Related

How to get a user's new Tweets with a Telegram Python bot while run_polling?

I'm currently developing a Telegram bot using telegram-python-bot and tweepy.
I want to create a feature that allows users of the bot to add their Twitter ID list via Telegram and have their new Tweets sent to them in real-time.
I want that the bot should be application.run_polling() to receive commands from the user, and at the same time, forwarding new tweets from Twitter users in users individual list.
When I read the tweepy documentation, I realized that I can get real-time tweets with fewer api requests if I fetch them through MyStream(auth=auth, listener=None).
But I don't know how to get both functions to work on the same file at the same time.
version
nest_asyncio-1.5.6 python_telegram_bot-20.0 tweepy-4.12.1
def main() -> None:
application = Application.builder().token("...").build()
add_list = ConversationHandler(
entry_points=[CallbackQueryHandler(input_id, pattern='input_id')],
states={ADD :[MessageHandler(filters.TEXT & ~filters.COMMAND, add)],},
fallbacks=[CallbackQueryHandler(button,pattern='back')])
application.add_handler(CommandHandler("on", on))
application.add_handler(add_list)
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("list", list_setting))
application.add_handler(CommandHandler("admin", admin))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CallbackQueryHandler(button))
application.run_polling()
if __name__ == "__main__":
main()
This is my main statement and I made it work until the SIGINT(ctrl+c) came in via application.run_polling().
I want to combine the above code to run and do the following at the same time.
import tweepy
consumer_key = "..." # Twitter API Key
consumer_secret = "..." # Twitter API Secret Key
access_token = "..." # Twitter Access Key
access_token_secret = "..." # Twitter Access Secret Key
usernames = ['...']
auth = tweepy.OAuth1UserHandler(
consumer_key, consumer_secret, access_token, access_token_secret
)
# Convert screen names to user IDs
user_ids = []
for username in usernames:
user = tweepy.API(auth).get_user(screen_name=username)
user_ids.append(str(user.id))
# Create a custom stream class
class MyStream(tweepy.Stream):
def __init__(self, auth, listener=None):
super().__init__(consumer_key, consumer_secret, access_token, access_token_secret)
def on_status(self, status):
tweet_url = f"https://twitter.com/{status.user.screen_name}/status/{status.id_str}"
print(f"{status.user.screen_name} tweeted: {status.text}\n{tweet_url}")
# send message to telegram
# Create a stream object with the above class and authentication
myStream = MyStream(auth=auth, listener=None)
# Start streaming for the selected users
myStream.filter(follow=user_ids)
I also tried to use thread's interval function or python-telegram-bot's job_queue.run_repeating function,
but these seem problematic for forwarding messages in real time.
I'm desperately looking for someone to help me with this😢.

How to integrate Google One Tap login with django-allauth?

How does one integrate the Google One Tap login experience with django-allauth?
django-allauth is integrated and working great for simple username/password logins.
I have Google OneTap's nicer user experience recognizing the user's authenticated Google account and offering to continue via that, sending a JWT auth token to Django.
Trying to find the simplest / cleanest way to register the new user account with the OneTap token and treat them as authenticated.
Appreciate any suggestions.
Refs:
https://developers.google.com/identity/one-tap/web
https://github.com/pennersr/django-allauth
Hacked something together, not as slick as one click login, (takes one extra step)
See more details here
https://twitter.com/DataLeonWei/status/1368021373151375361
All I did was changing the google redirect URL to the existing user log-in page with Google.
And add an additional view and replace google's data-login_uri with this view's URL.
#csrf_exempt
def google_one_tap_login(request):
login_url = PUBLIC_DOMAIN_NAME + '/accounts/google/login/'
return HttpResponseRedirect(login_url)
If someone has a better solution, please let me know.
My current hack is implemented on both sqlpad and instamentor, please feel free to check them out and see it in action.
Override allauth's account/login.html template and render the Google button (remember to replace <GOOGLE_APP_CLIENT_ID> and <HOMEPAGE>):
<div class="g_id_signin" data-type="standard" data-shape="pill"
data-theme="outline" data-text="signin_with" data-size="large"
data-logo_alignment="left"></div>
<div id="g_id_onload"
data-client_id="<GOOGLE_APP_CLIENT_ID>"
data-context="signin"
data-ux_mode="redirect"
data-login_uri="<HOMEPAGE>{% url 'google-login' %}?next={{ request.GET.next }}"
data-auto_prompt="false"></div>
<script src="https://accounts.google.com/gsi/client" async defer></script>
Install google-auth if you haven't already:
pip install google-auth
Register the google-login endpoint in your urls.py:
path('google-login', views.google_login, name='google-login'),
Define the google-login endpoint in your views.py, where you verify the Google ID token before redirecting to allauth's login URL for Google:
import logging
from django.conf import settings
from django.contrib import messages
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from google.oauth2 import id_token
from google.auth.transport import requests
from urllib import parse
#csrf_exempt
#require_POST
def google_login(request):
body_unicode = request.body.decode('utf-8')
body_params = parse.parse_qs(body_unicode)
csrf_token_cookie = request.COOKIES.get('g_csrf_token')
if not csrf_token_cookie:
return HttpResponseBadRequest('No CSRF token in Cookie.')
csrf_token_body = body_params.get('g_csrf_token')
if not csrf_token_body:
return HttpResponseBadRequest('No CSRF token in post body.')
if csrf_token_cookie != csrf_token_body[0]:
return HttpResponseBadRequest('Failed to verify double submit cookie.')
next_url = request.GET['next']
try:
token = body_params.get('credential')[0]
# noinspection PyUnusedLocal
idinfo = id_token.verify_oauth2_token(token, requests.Request(), settings.GOOGLE_APP_CLIENT_ID)
except ValueError as e:
logging.error(e)
return HttpResponseBadRequest('Failed to verify Google auth credentials.')
return redirect(settings.HOMEPAGE + '/accounts/google/login/?next=' + next_url)

Account Kit returning previous number and account kit ID when a new number is verified

I am testing Account Kit (Basic Web version - phone number verification) on a Django (Python) based web app. One thing I try is logging with multiple accounts on localhost, and trying to link a different number to each one successively. If a number has already successfully attached to a previous account, I show an "already taken" error prompt. Standard stuff.
I've been noticing that I sporadically get the "already taken" error prompt on unused numbers as well. Investigating deeper, I found that although I had input and verified (via SMS) a new number, the account kit ID and mobile number returned to me was the previous pair.
I can't tell why this is happening. Can someone help me in debugging this? In case it matters, my authorization flow uses the app secret.
Following are some relevant snippets. First, the Account Kit Manager class I've written:
from myproj.account_kit_settings import FAID, AKAS
class AccountKitManager(object):
obj = None
def __init__(self, app_id, app_secret):
self.app_secret = app_secret
self.app_access_token = 'AA|{0}|{1}'.format(app_id, app_secret)
def get_user_cred(self, auth_code):
if not self.obj:
self.set_user_cred(auth_code)
return self.obj
def set_user_cred(self, auth_code, url=None):
if not url:
url = 'https://graph.accountkit.com/v1.2/access_token?grant_type=authorization_code&code={0}&access_token={1}&appsecret_proof={2}'.\
format(auth_code,self.app_access_token,self.get_appsecret_proof(self.app_access_token))
data = self.retrieve_data(url)
data = self.evaluate_data(data)
string_obj = self.retrieve_user_cred(data["access_token"])
self.obj = self.evaluate_data(string_obj)
def retrieve_user_cred(self, user_access_token, url=None):
if not url:
url = 'https://graph.accountkit.com/v1.2/me/?access_token={0}&appsecret_proof={1}'.\
format(user_access_token,self.get_appsecret_proof(user_access_token))
return self.retrieve_data(url)
def retrieve_data(self, url):
return requests.get(url).text
def evaluate_data(self, data):
return ast.literal_eval(data)
def get_appsecret_proof(self, access_token):
h = hmac.new(self.app_secret.encode('utf-8'),msg=access_token.encode('utf-8'),digestmod=hashlib.sha256)
return h.hexdigest()
Next, here's how I use it:
mobile_data = AccountKitManager(FAID, AKAS)
def account_kit_handshake(csrf, state, status, auth_code):
if csrf == state and status=='PARTIALLY_AUTHENTICATED':
user_data = mobile_data.get_user_cred(auth_code)
if FAID == user_data["application"]["id"]:
return user_data["id"], user_data["phone"]
else:
# app id mismatch
return None, None
else:
# csrf mismatch, or could not authenticate
return None, None
def get_requirements(request):
status = request.GET.get('status', None)
auth_code = request.GET.get('code', None)
state = request.GET.get('state', None)
return account_kit_handshake(request.session["csrf"], state, status, auth_code)
def verify_consumer_number(request,*args,**kwargs):
AK_ID, MN_data = get_requirements(request)
request.session.pop("csrf",None)
if AK_ID and MN_data:
if someone_elses_number(MN_data['national_number'], request.user.id):
return render(request,"used_number.html",{})
else:
save_consumer_credentials.delay(AK_ID, MN_data, request.user.id)
return redirect("classified_listing")
else:
return render(request,"unverified_number.html",{})
UPDATE: Seems the user access token isn't always being returned. This could be a problem with variable scope.
The problem emanated from the scope of the AccountKitManager class instance. It was being set globally (i.e. see mobile_data variable in my code). Making this variable local solved the problem.

Manually Invoking email verification

We've been using django-allauth for quite some time now in production. We can enable account email verification which works great. But we now have a REST api that allows users to register through the API and the workflow doesn't go through django-allauth. Is it possible to manually invoke the django-allauth email verification feature or do we need to use a custom solution?
I'll just post my answer here as I've been searching for adding email verification with Django Built-in Authentication (And using a Custom Auth Model), I used the method mentioned by Marcus, I'll just add all the other stuff around it for anyone who wants to do the same.
First: Install django-allauth as described here
Second: Add your email configurations in the settings.py file :
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com' #I used gmail in my case
EMAIL_HOST_USER = <Your Email>
EMAIL_HOST_PASSWORD = <Your Password>
EMAIL_PORT = 587
DEFAULT_FROM_EMAIL = <Default Sender name and email>
Third: Add configurations for verification and default login url, you'll find the documentation of all config parameters here, note that in my example I'm using a custom user model as mentioned, that's why I'm setting ACCOUNT_EMAIL_REQUIRED to True & ACCOUNT_USER_MODEL_USERNAME_FIELD and ACCOUNT_USERNAME_REQUIRED to False, also the LOGIN_URL,ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL andLOGIN_REDIRECT_URL parameters are used after the user clicks on the confirmation link sent by email to him
ACCOUNT_EMAIL_VERIFICATION='mandatory'
ACCOUNT_CONFIRM_EMAIL_ON_GET=True
ACCOUNT_EMAIL_REQUIRED=True
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_USERNAME_REQUIRED = False
ACCOUNT_AUTHENTICATION_METHOD = 'email'
LOGIN_URL='app:login_user'
LOGIN_REDIRECT_URL='app:login_user'
ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL='app:login_user'
Fourth: After your signup form, save the user instance with is_active parameter set to False, then call the method:
from allauth.account.utils import *
send_email_confirmation(request, user, True)
Finally: Receive the signal after the user confirms his email, and set is_active to True
from allauth.account.signals import email_confirmed
from django.dispatch import receiver
# Signal sent to activate user upon confirmation
#receiver(email_confirmed)
def email_confirmed_(request, email_address, **kwargs):
user = MyUser.objects.get(email=email_address.email)
user.is_active = True
user.save()
Finally, you would want to change the default site name from Django Admin as it will be included in the email sent.
I had the same problem, and the solution I've found was to call the original send_email_confirmation method from allauth. I am using DRF3 for my API.
from allauth.account.utils import send_email_confirmation
...
def some_view(request):
user = ...
...
#using request._request to avoid TypeError on change made in DRF3 (from HTTPRequest to Request object)
send_email_confirmation(request._request, user)
...
I hope this helps you.

How do I migrate users from OpenID to Google OAuth2/OpenID Connect using Python Social Auth?

Google is deprecating the OpenID endpoint I was using (v1.0 I think, via the django_openid_auth module) and I need to update my app and migrate my users' accounts to use Google OAuth2.
I've changed the app to use python-social-auth and have it authenticating with social.backends.google.GoogleOAuth2 successfully.
I've written a pipeline function to find associated OpenID urls from the old table and this is working for the other backends I care about but Google:
def associate_legacy_user(backend, response, uid=None, user=None,
*args, **kwargs):
if uid and not user:
# Try to associate accounts registered in the old openid table
identity_url = None
if backend.name == 'google-oauth2':
# TODO: this isn't working
identity_url = response.get('open_id')
else:
# for all other backends, see if there is a claimed_id url
# matching the identity_url use identity_url instead of uid
# as uid may be the user's email or username
try:
identity_url = response.identity_url
except AttributeError:
identity_url = uid
if identity_url:
# raw sql as this is no longer an installed app
user_ids = sql_query.dbquery('SELECT user_id '
'FROM django_openid_auth_useropenid '
'WHERE claimed_id = %s',
(identity_url,))
if len(user_ids) == 1:
return {'user': User.objects.get(id=user_ids[0]['user_id'])}
As best I can tell from reading Google's migration guide, I need to add an openid.realm to the request, which I've done as follows in settings.py:
SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS \
= {'openid.realm': 'http://example.com/'}
But this doesn't seem to be returning the open_id value in the response passed into my pipeline function.
I seem to be stuck on Step 3:
I tried sub-classing the backend to change the RESPONSE_TYPE to add id_token but that returned an empty response:
import social.backends.google
class CustomGoogleOAuth2(social.backends.google.GoogleOAuth2):
RESPONSE_TYPE = 'code id_token'
I tried building an additional request to https://www.googleapis.com/oauth2/v3/token similar to this example, but I don't really know how to go about putting that together and debugging it.
Some more details:
My old claimed_ids for Google OpenID users look like: https://www.google.com/accounts/o8/id?id=AItOawmAW18QuHDdn6PZzaiI5BWUb84mZzNB9eo
I'm happy to use social.backends.google.GoogleOpenIdConnect or a similar alternative backend if that's an easier solution. And while it seems to be closer to what the Google docs are talking about, I wasn't able to get it to work when I tried:
I get a 400 Error: invalid_request Parameter not allowed for this message type: nonce
I can get past the nonce error using social.backends.google.GoogleOpenIdConnect by adding id_token to the RESPONSE_TYPE but then I get an AuthMissingParameter error in my /complete/google-openidconnect/ endpoint as the request's GET and POST are empty. (Tried 'code id_token', 'token id_token', 'id_token', ...)
I don't want to use social.backends.google.GooglePlusAuth as that doesn't integrate as nicely with my current login form.
Worst case, I should be able to use social.pipeline.social_auth.associate_by_email, but I only have email addresses for maybe 80% of the users so that leaves quite a few who will have a new account and need support to associate it manually.
Try as I might, I can't find any examples of people doing a similar migration with python-social-auth, but it must be happening to lots of people.
Any ideas?
Solution works for python social auth 0.1.26
In new versions (0.2.*) of python social auth, there is GoogleOpenIdConnect, but it does not work fine (at least I did not succeed). And my project has some legacy, so I can't use new version of social.
I wrote custom GoogleOpenIdConnect backend:
import datetime
from calendar import timegm
from jwt import InvalidTokenError, decode as jwt_decode
from social.backends.google import GoogleOAuth2
from social.exceptions import AuthTokenError
class GoogleOpenIdConnect(GoogleOAuth2):
name = 'google-openidconnect'
ACCESS_TOKEN_URL = 'https://www.googleapis.com/oauth2/v3/token'
DEFAULT_SCOPE = ['openid']
EXTRA_DATA = ['id_token', 'refresh_token', ('sub', 'id')]
ID_TOKEN_ISSUER = "accounts.google.com"
def user_data(self, access_token, *args, **kwargs):
return self.get_json(
'https://www.googleapis.com/plus/v1/people/me/openIdConnect',
params={'access_token': access_token, 'alt': 'json'}
)
def get_user_id(self, details, response):
return response['sub']
def request_access_token(self, *args, **kwargs):
"""
Retrieve the access token. Also, validate the id_token and
store it (temporarily).
"""
response = self.get_json(*args, **kwargs)
response['id_token_parsed'] = self.validate_and_return_id_token(response['id_token'])
return response
def validate_and_return_id_token(self, id_token):
"""
Validates the id_token according to the steps at
http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation.
"""
try:
id_token = jwt_decode(id_token, verify=False)
except InvalidTokenError as err:
raise AuthTokenError(self, err)
# Verify the token was issued in the last 10 minutes
utc_timestamp = timegm(datetime.datetime.utcnow().utctimetuple())
if id_token['iat'] < (utc_timestamp - 600):
raise AuthTokenError(self, 'Incorrect id_token: iat')
return id_token
Notes:
get_user_id – An identifier for the user, unique among all Google accounts and never reused.
request_access_token – there is I add id_token_parsed to response, and it will be used in pipeline.
validate_and_return_id_token – validate of jwt is disabled, because in google developers console I have registered Client ID as web application so, I have no certificates for validate this data.
Then I created pipelines:
def social_user_google_backwards(strategy, uid, *args, **kwargs):
"""
Provide find user that was connect with google openID, but is logging with google oauth2
"""
result = social_user(strategy, uid, *args, **kwargs)
provider = strategy.backend.name
user = result.get('user')
if provider != 'google-openidconnect' or user is not None:
return result
openid_id = kwargs.get('response', {}).get('id_token_parsed', {}).get('openid_id')
if openid_id is None:
return result
social = _get_google_openid(strategy, openid_id)
if social is not None:
result.update({
'user': social.user,
'is_new': social.user is None,
'google_openid_social': social
})
return result
def _get_google_openid(strategy, openid_id):
social = strategy.storage.user.get_social_auth('openid', openid_id)
if social:
return social
return None
def associate_user(strategy, uid, user=None, social=None, *args, **kwargs):
result = social_associate_user(strategy, uid, user, social, *args, **kwargs)
google_openid_social = kwargs.pop('google_openid_social', None)
if google_openid_social is not None:
google_openid_social.delete()
return result
And changed my SOCIAL_AUTH_PIPELINE and AUTHENTICATION_BACKENDS settings:
AUTHENTICATION_BACKENDS = (
...
#'social.backends.open_id.OpenIdAuth' remove it
'social_extension.backends.google.GoogleOpenIdConnect', # add it
...
)
and
SOCIAL_AUTH_PIPELINE = (
'social.pipeline.social_auth.social_details',
'social.pipeline.social_auth.social_uid',
'social.pipeline.social_auth.auth_allowed',
# 'social.pipeline.social_auth.social_user', remove it
'social_extension.pipeline.social_user_google_backwards', # add it
'social.pipeline.user.get_username',
...
# 'social.pipeline.social_auth.associate_user', remove it
'social_extension.pipeline.associate_user', # add it
'social.pipeline.social_auth.load_extra_data',
...
)

Resources