Google Sign-in implementation for Websauna.
- Persistent offline access to scope
- On-the-air installs of android apps
- Everything supported by Google Sign-in
This implementation follows the “Google Sign-In for server-side apps”[fn:1] at the exception of verifying user-submitted code with Google.
See below comment “# validate token with Google API Client Library for details.
import httplib2
import logging
from apiclient import discovery
from oauth2client import client
from websauna.system.core import messages
from websauna.system.http import Request
from websauna.system.core.route import simple_route
from websauna.system.user.utils import get_user_registry
from websauna.system.user.utils import get_login_service
logger = logging.getLogger(__name__)
client_secrets = "/home/self/git/myapp/client_secret.json"
client_id = ""
scopes = [
"profile",
"email",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/webmasters",
"https://www.googleapis.com/auth/user.phonenumbers.read"
]
# one-time-code step #7
@simple_route("/signin",
route_name="signin",
renderer="myapp/signin.html", require_csrf=False)
def signin(request: Request, method="POST"):
"""Implements the Google Sign-in one-time-code flow.
Diagram:
- https://developers.google.com/+/images/server_side_code_flow.png
References:
- https://developers.google.com/identity/sign-in/web/server-side-flow
- https://developers.google.com/identity/sign-in/web/android-app-installs
"""
# read the ID token from POST data
try:
# byte/octet-stream
token = request.body_file.read()
logger.info("received Google Sign-in token")
logger.debug("token: {}".format(token))
except Exception as e:
logger.error("exception: {}".format(str(e)))
return {}
try:
# exchange token for an access_token
credentials = client.credentials_from_clientsecrets_and_code(
client_secrets, scopes, token)
logger.debug("Received credentials token: {}".format(credentials.to_json())
# validate token with Google API Client Library
try:
logging.info('Verifying token')
idinfo = client.verify_id_token(
credentials.token_response["id_token"], client_id)
logger.debug(str(idinfo))
# If multiple clients access the backend server:
if idinfo["aud"] not in [client_id]:
raise crypt.AppIdentityError("Unrecognized client.")
if idinfo["iss"] not in ["accounts.google.com",
"https://accounts.google.com"]:
raise crypt.AppIdentityError("Wrong issuer.")
logging.info("Successfully validated access token")
except crypt.AppIdentityError as e:
logger.error(str(e))
return {}
# userid = idinfo["sub"]
http_client = httplib2.Http()
http_client = credentials.authorize(http_client)
except client.FlowExchangeError as e:
logger.error("exception: {}".format(str(e)))
return {}
logger.info("refreshed credentials on http_client")
logger.debug("refresh token = {}".format(str(credentials.refresh_token)))
logger.info("confirmed of a user with Google")
user_registry = get_user_registry(request)
service = discovery.build("oauth2", "v2", http=http_client)
signin_data = service.userinfo().get().execute()
# retrieve user details
logging.debug("received signing data: {}".format(
str(dict(signin_data))))
# get existing local account
user = user_registry.get_by_email(signin_data.get("email"))
# or create a new one with Google user email
if not user:
# sign_up expects user_data matching a default JSONB structure
user_details = {
"email": signin_data.get("email"),
"full_name": signin_data.get("name"),
"first_login": True,
"social": signin_data
}
user = user_registry.sign_up(
registration_source="google", user_data=user_details)
if not user:
return locals()
logger.info("a Google user became a local user ({0})...".format(
signin_data.get("email")))
login_service = get_login_service(request)
resp = login_service.authenticate_user(user, login_source="google")
# TODO: store ``credentials.to_json()'' in DB
return resp
{# Load JavaScript files.
The include location of this template may alter between ``<head>`` and the end of ``<body>``.
#}
<!-- one-time-code step #3 -->
<script>
function start() {
gapi.load('auth2', function() {
auth2 = gapi.auth2.init({
client_id: '572921623645-krj6b328g6s9mu0p679qhbk18smd707g.apps.googleusercontent.com',
scope: 'profile email https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/webmasters https://www.googleapis.com/auth/user.phonenumbers.read'
});
});
}
</script>
<script src="{{ 'websauna.system:static/jquery-2.0.3.min.js'|static_url }}"></script>
<script src="{{ 'websauna.system:static/bootstrap.min.js'|static_url }}"></script>
<!-- one-time-code step #2 -->
<script src="https://apis.google.com/js/client:platform.js?onload=start" async defer></script>
<script type="text/javascript">
$(document).ready
$('#signInButton').click(function() {
// signInCallback defined in step 6.
auth2.grantOfflineAccess({'scope': 'profile email https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/webmasters https://www.googleapis.com/auth/user.phonenumbers.read', 'redirect_uri': 'postmessage', }).then(signInCallback);
});
function signInCallback(authResult) {
if (authResult['code']) {
$.ajax({
type: 'POST',
url: '/signin',
contentType: 'application/octet-stream; charset=utf-8',
success: function(result) {
// success
},
processData: false,
data: authResult['code']
});
setTimeout(function() {
window.location = "/phone";
}, 1234);
} else {
// There was an error.
}
}
</script>
{# Pull JS for widgets #}
{% if request.on_demand_resource_renderer %}
{% for js_url in request.on_demand_resource_renderer.get_resources("js") %}
<script src="{{ js_url }}"></script>
{% endfor %}
{% endif %}
<div id="signInButton">
<button>Sign in with Google</button>
</div>
<form method="POST" action="{{'logout'|route_url}}">
<input name="csrf_token" type="hidden" value="{{ request.session.get_csrf_token() }}">
<button id="nav-logout" class="btn btn-link" onclick="auth2.signout()">
<i class="fa fa-sign-out"></i>
Log out
</button>
</form>
import oauth2client.client, json, httplib2
from apiclient import discovery
json_credentials_string='<json credentials from storage>'
creds = json.loads(json_credentials_string)
oauth2_credentials = oauth2client.client.OAuth2Credentials(access_token=creds['access_token'],client_id=creds['id_token']['sub'], client_secret=creds['client_secret'], refresh_token=creds['refresh_token'], token_expiry=creds['token_expiry'], token_uri=creds['token_uri'], user_agent=b'Python-httplib2/0.9.2 (gzip)')
super_oauth2_credentials = oauth2_credentials.from_json(json.dumps(creds))
h = httplib2.Http()
h = super_oauth2_credentials.authorize(h)
# Test the tokens
webmasters_service = discovery.build('webmasters', 'v3', http=h)
# raises an exception if not valid
webmasters_service.sites().list().execute()
- Login for first time
- Authorize request dialog
- credentials are persisted to redis
- Logout
- Login
- no authorization required
- credentials are retrieved from redis
- credentials are tested against google webmasters api
- no exception
- Delete redis hkey
redis.get_redis(request.registry).delete('signin_credentials_json')
- Logout
[fn:1] https://developers.google.com/identity/sign-in/web/server-side-flow