Google (OAuth2) Sign-in for Web Applications

Kiran Madabhushi
10 min readMar 16, 2020

How to use (OAuth 2) Google Sign-in for Web Applications

I read many articles about using OAuth2 and everything goes about talking the terminology but never provided info about how to get an application to use Google for user authentication. So I jumped on and implemented the feature to use Google Sign-in for my web application.

Web Application with a Backend Server

In this section, I will take you through implementing the Google Sign-in when you have a backend server in your application. Backend server takes care of the authentication logic and front-end (browser) just shows the web pages as sent by the backend server. I interchangeably use web server/application/backend server in this article to mean the same thing: an application running on a server serving web pages.

Typical Frontend / Backend Application Interaction

In the setup here, a browser on your laptop is accessing pages on the web server that’s hosting your application https://www.myapp.com. (During development we will use https://www.myapp.com:9443 with www.myapp.com resolving to 127.0.0.1 using /etc/hosts on your laptop). The application expects that the user must have been authenticated to access pages. It checks this by expecting a cookie from the browser. If it does not find the cookie, web server sends a page — a login form asking for username and password to the browser. User enters the information and sends it back to the web server (using POST to https://www.myapp.com:9443/auth). Application validates the user credentials from its local database of users and sends a cookie to the browser along with the contents of /page1. Now whenever the browser accesses other pages on the server, the cookie is sent with the requests. Web server validates the cookie and responds with pages or back to the authentication screen depending on the validity of the cookie.

Google Sign-in

Instead of the web server sending the login form asking for user credentials, it can send (by using a redirect) the Google sign in form. Just like the server’s login form has info about where to send the form back (POST action), the Google sign-in form can be told to call a URL after user authentication succeeds. Instead of passing username and password, Google calls the URL with a code called authorization_code. The web server takes this code obtained from Google (all these are happen on browser) and verifies this with Google and exchanges this code for a new token called access_token.

For the web server to send (redirect) Google sign-in form we need to register our application with Google so Google can expect an authentication request from our application. During the registration process, provide the application URL where the code (similar to POST action in the login form e.g https://www.myapp.com:9443/auth) is sent back to the web server. In Google terms this is called redirect_uri. Once the registration is complete, Google gives the application a client_id and client_secret. When the application sends (redirects) the browser to the Google sign in form, it must attach the client_id and also the redirect_uri where the authorization code must be sent (e.g https://www.myapp.com:9443/auth, even though we registered this URL with Google earlier we must provide that URL again while redirecting to the sign-in form, details later).

Register Application with Google

Go to https://console.developers.google.com/
Click “Create Project” on the page and enter the name “test-my-app”
We are going to use this project only for authenticating our app. We are not going to access any other Google services. So don’t worry about APIs and Services.
On the left side, click “OAuth consent screen”. Here we need to choose what kind of users we want to allow for authentication. If you are GSuite user you can restrict the app to be used only by the users in your organization (Internal) or anyone in the world with a Google account (External). For this tutorial we will choose “External” as I don’t have an organization.
Give a name to the application (test-my-app), you can upload a logo that will be shown in the sign-in form.
The “Authorized Domains” is a mandatory field that defines the top level domains that can send requests for login. In our example this would be myapp.com. (localhost or an ip address are not allowed). Click “Save”. You can have multiple domain names added here for all your applications or edit this later to add more domains.
On the left side click “Credentials” and then “Create Credentials” -> “OAuth Client ID”. Choose “Web Application” and in the name type in “test-my-app”. You don’t need to specify Javascript Origins. In the “Authorized redirect URIs”, provide a URL that Google calls after a user has signed in. (Think of this as equivalent to the POST action on the login form). For this example provide this as https://www.myapp.com:9443/auth. (We will be running our app on port 9443 during development). You can add multiple redirect URIs here (Once you deploy the application on a hosted site and get a FQDN, put those URIs here). The list of URIs here is a check list only. Google does not really use this to redirect. When the app sends (redirects) the login form, one of the information it attaches is a redirect URI. Google checks if the attached URI in the sign in form is an approved one that’s defined during the app registration. You can add different URIs for dev, staging, production etc. Once you click “Create” you get a client_id and client_secret. You can either note these down now or you can access that later from the “Client Credentials” — “OAuth2.0 Client IDs” section.
Client_Id looks like xxxxxxxxxx.apps.googleusercontent.com
Client_secret a-bunch-of-letters-numbers-symbols

Web Application

I will implement the web application using a psuedo-code and later provide python3 code. When the browser requests for “https://www.myapp.com/page1”, the application code on the web server handles the request:

function handle_browser_request(page_name) {
if browser_request.has_cookie('auth') {
return content_of(page_name)
} else {
return login_form()
}
}

If it finds a cookie with name ‘auth’ in the request, it returns the page. Otherwise a login form is sent back to the browser. Without Google sign, the login form would have two text fields (username, password) and submit button with form method as POST and action to https://www.myapp.com:9443/auth. For the Google sign in, instead of sending a form, a redirect to Google sign in is sent with some parameters. I split the URL into multiple lines for easy readability

function login_form() {
redirect_to https://accounts.google.com/o/oauth2/v2/auth?
client_id=<client_id_from_app_registration>&
redirect_uri=https://www.myapp.com:9443/auth&
response_type=code&
scope=email profile openid&
access_type=offline&
state=some-random-text
}

client_id and redirect_uri parameters are explained in the above sections. response_type can be either code or token. For our example here we use code. code just gives you authorization and says the login has succeeded but does not give you any other information. token is typically used in single page apps and is not applicable here. scope is the set of parameters we are asking from Google. state — Application can use this information to keep track of the current application’s state (like the url user requested or any other information). access_type can always be offline. Check the Google documentation for details. The redirected URL is displayed by the browser and shows the Google login screen, user enters the username and password, and after verifying, Google redirects to the redirect_uri parameter with some query string args attached.

https://www.myapp.com:9443/auth?
state=state-text-that-we-passed-above&
code=<some-very-long-string-from-google>&
scope=<scopes that we requested above>

This call goes back to the web server (via browser) and is handled by the application just like the page handler. (Either write a separate handler for ‘/auth’ or write a special condition in the handle_browser_request above to check if page_name is ‘/auth’)

function handle_request('/auth') {
code = request.url_params['code']
# some thing more needs to be done here, explained later
}

This takes care of the login/authorization part from the browser/user. The code that Google sent to the web server is called authorization code. But the authorization code does not give any information about the user. From the application point of view how does it trust this code and ensure that that it’s really from Google? Anyone can type in a random code on the browser url.

Application has to make one more call to Google to get some thing called an ‘access_token’. This happens on the application side and user is not involved in this process. The authorization code can be used just once to get the access_token. Once you get the access_token, there are other API calls that can be called to get user information. So far we were using the Google urls to sign in. But Google also provides access to other APIs like calendar, maps, drive etc. The authorization code keeps track of those permissions, but access_token is the one that can be used to perform API related actions.

To get access_token from the authorization_code use the following URL. The data must be sent to the URL using POST. (We will enhance the above auth handler function to do all this stuff).

POST https://oauth2.googleapis.com/token
-F 'code=<authorization-code-from-above'
-F 'client_id=<client-id>'
-F 'client_secret=<client-secret>'
-F 'grant_type=authorization_code'
-F 'redirect_uri=https://www.myapp.com/auth'

client_id — same as the one used previously
client_secret — the one provided by Google during app registration
grant_type — tells Google that the code that we are sending is an ‘authorization_code’
redirect_uri — you can either use the same url (application will not use this, but Google needs it)

The response body of this call is JSON data:

{
“access_token”: “<token-text>”,
“expires_in”: 3599,
“refresh_token”: “<token-used-to-refresh-the-access-token>”,
“scope”: “<scopes that were sent during login>",
“token_type”: “Bearer”,
“id_token”: “<huge-text-of-id-token>”
}

id_token, is the one that has info about the user. This token is of the type JWT (JSON Web Token).There are utilities available in all the programming languages to decode this. You can use this online editor to decode this and has the following data (some fields might be missing or more added depending on the user that’s logged in).

{
"iss": "https://accounts.google.com",
"azp": "<typically-the-client_id>",
"aud": "<client_id>",
"sub": "106975238426057471685",
"hd": "<domain-name-of-gsuite-user>",
"email": "<email-id-of-the-user>",
"email_verified": true,
"at_hash": "<hash>",
"name": "<Full Name of the user",
"picture": "<jpg pic>",
"given_name": "First Name",
"family_name": "Last Name",
"locale": "en",
"iat": 1584073158,
"exp": 1584076758
}

access_token can be used for other API calls. The token must be passed in the header as Bearer token. For example to get the user info dynamically

curl https://openidconnect.googleapis.com/v1/userinfo
-H 'authorization: Bearer <access-token>'

Going back to the previous function where the URL was called after the login by the user (redirected to ‘/auth’), the function is updated to get access_token.

function handle_request('/auth') {
code = request.url_params['code']
# get the access token
token_url = "https://oauth2.googleapis.com/token"
form_data = (
client_id=<client_id>,
client_secret=<client_secret>,
code=<authorization_code>,
grant_type="authorization_code",
redirect_uri="https://www.myapp.com/auth"
)
rsp_json = post_to_google(token_url, form_data)
# rsp_json.id_token, rsp_json.access_token
# send a cookie here.
send_cookie(rsp_json.id_token)
save_user_name_access_token_to_db_or_session()
}

Here is a sample python implementation using flask. (www.myapp.com resolves to 127.0.0.1 using /etc/hosts)

import flask
import requests
from urllib.parse import urlencode

# client_id and client_secret are given by google after the
# app registration
CLIENT_ID = "<client-id-from-app-registration>"
CLIENT_SECRET = "<client-secret>"
# the URL google redirects (with auth code) after user login
REDIRECT_URL = "https://www.myapp.com:9443/auth"
app = flask.Flask(__name__)
app.secret_key = "random-text-used-by-flask"

# handles the requests for the main page
# https://www.myapp.com:443
# If the request does not have a cookie, send a login form which is
# a redirect to google sign in
@app.route('/')
def index():
# check there is a cookie named 'auth'
if flask.session.get('auth'):
return 'Hello World'
else:
# redirect the user to google sign in page
data = {
'client_id': CLIENT_ID,
'redirect_uri': REDIRECT_URL,
'response_type': 'code',
'scope': "email profile openid",
'access_type': 'offline',
'prompt': 'consent',
}
# encode the above data as query string and append to the
# authentication url
auth_url = "https://accounts.google.com/o/oauth2/v2/auth?"
auth_url += urlencode(data)
return flask.redirect(auth_url)

# once user logs in, Google redirects user to the REDIRECT_URL
# that's handled by '/auth' handler defined below
@app.route('/auth')
def authenticate():
# Google redirected URL has a few query string params one of
# which is the "code"
# read the auth code from the url query string
auth_code = flask.request.args['code']
# exchange code for a toke (can be done only once)
token_url = 'https://oauth2.googleapis.com/token'
data = {
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'code': auth_code,
'grant_type': 'authorization_code',
'redirect_uri': REDIRECT_URL
}
rsp = requests.post(token_url, data=data)
if rsp.ok:
# send a cookie to the client, say with user name
id_token = rsp.json()['id_token']
flask.session['auth'] = id_token
# save the access token to a local db. not implementing
# that part here. when the browser sends the id_token
# as part of the cookie, you can retrieve the access token
# matching that cookie and verify it the user is still valid
# save_to_db(id_token, rsp.json()['access_token'],
# rsp.json()['refresh_token'])
return flask.redirect(flask.url_for('index'))if __name__ == "__main__":
app.run(debug=True, port='9443', ssl_context='adhoc')

Refreshing the access token
Once you got the token and saved it in the local database, you need to refresh the token once in a while. The default validity of the token from Google is 1hr. Since the authorization code cannot be used any more to get new tokens, Google provides a way to get a new token. Along with id_token and access_token, it also sent you a refresh token. Before the access token expires (in 1hr) you need to make another API call using the refresh token to get a new access token.

POST https://oauth2.googleapis.com/token
client_id=<client_id>
client_secret=<client_secret>
grant_type="refresh_token"
refresh_token=<refresh_token_from_previous_access_token_request>

The above call happens in the background without user intervention. You can either run a cron job to refresh all the saved access tokens or do it on demand when a page is requested and the access token is about to expire. Finally if nothing is done, user can always be redirected the login page.

Hope this helps in implementing OAuth 2 using Google for your website. In the next article I will write about using AWS Cognito for your application. Cognito can manage all the users for the application usings it’s local database or it can also federate it to use other social providers like Google, Facebook. Stay put.

References

  1. https://developers.google.com/identity/protocols/oauth2/web-server
  2. App Registration https://console.developers.google.com

--

--