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)
I want to sign in users with the google-auth-library-ruby
gem.
In their guides "Google Sign-In for server-side apps " they have a good code example how to exchange the authorization code for an ID token, but it's only for Python (and Java):
credentials = client.credentials_from_clientsecrets_and_code(
CLIENT_SECRET_FILE,
['https://www.googleapis.com/auth/drive.appdata', 'profile', 'email'],
auth_code)
Does anybody know about the equivalent for Ruby?
PS. I'm familiar with the omniauth gem, but would like to use the google-auth-library-ruby gem if possible.
After some research, I found this collection of samples, where the googleauth gem is used. Here you have it:
client_id = Google::Auth::ClientId.new("your Google client ID", "your Google secret")
scope = ["email","profile"]
token_store = nil # there are actually already implemented File or Redis token stores
callback_url = "http://localhost:8000"
authorizer = Google::Auth::UserAuthorizer.new(client_id, scope, token_store, callback_url)
credentials = authorizer.get_credentials_from_code(code: "the auth code in the Sign In response")
I am trying to use Google Sheet in my application. This google sheet is not related to end user but will be under my own account. As per some tutorial on Google Sheets API I have created project on developer console but during OAuth 2.0 authentication, Google display login dialog to end user. As said before, this sheet is not related to end user so how can I fix my own Google credentials in code so that it just work with sheet, without asking user for his credentials?
Here is my code snippet
var clientSecret = new Google.Apis.Auth.OAuth2.ClientSecrets() { ClientId = "*******.apps.googleusercontent.com", ClientSecret = "*******" };
var credential = Google.Apis.Auth.OAuth2.GoogleWebAuthorizationBroker.AuthorizeAsync(clientSecret,
Scopes,
"myemail#google.com",
System.Threading.CancellationToken.None).Result;
var service = new Google.Apis.Sheets.v4.SheetsService(new Google.Apis.Services.BaseClientService.Initializer()
{
HttpClientInitializer = credential,
ApplicationName = ApplicationName,
});
Are you looking to publish the article publicly?
https://support.google.com/docs/answer/37579?hl=en
You can use this method, then embed the sheet in an iframe on your site
It looks there are no such ways yet. So finally I decided to use local database and then export content to Excel using ClosedXML library.
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',
...
)
I want use this API in rails.
It says should include an Authorization header.(use oauth2)
So I use google-api-ruby-client this lib like below.
I write below code by this sample.
#client = Google::APIClient.new
#client.authorization.client_id = CONSUMER_KEY
#client.authorization.client_secret = CONSUMER_SECRET
#client.authorization.scope = 'https://apps-apis.google.com/a/feeds/domain/'
#client.authorization.redirect_uri = "http://#{request.host}:#{request.port.to_s}
/google_app/oauth2callback"
redirect_to #client.authorization.authorization_uri.to_s
But it cause redirect_uri_mismatch error.
I don't know whether my usage is correct.
Note:
Before use this API, I have logined with Google Openid successfully.
might be a duplicate of OAuth 2.0 sample error when accessing Google API
the problem there is, that the javascript is missing the port-number.