logo
post image

Implement Google OAuth Login with Deno

This tutorial explains how to implement "Login with Google" in a Deno app using Google OAuth APIs.

Google OAuth APIs are implemented as per Using OAuth 2.0 for Web Server Applications.

Our Application Structure

Our application contains several files, each having a separate role.

app.js
google_login.js
settings.json
views
  -- index.html
  -- google-auth.html
  • app.js : the main app script
  • google_login.js : implements Google OAuth APIs as an ES6 module
  • settings.json : holds Google API keys
  • views/index.html : HTML template file for the home page
  • views/google-auth.html : HTML template file which displays user information after a successful Google login

Our Application Routes

  • / : Route to our home page. Displays content from views/index.html template file.
  • /google-auth : Route to the page holding user information after a successful Google login. Displays content from views/google-auth.html template file.

We will be implementing the app with http://localhost:8080 as the domain.

Getting Google API Keys

  • Go to Google APIs Console and create a new API project.

    Set the User Type as External

  • Fill out the required form fields in the OAuth consent screen page

  • In the Scopes page, add 2 scopes — userinfo.email & userinfo.profile. These scopes are required to get email & basic profile information of the user.

  • There is no need to add anything in the Test users page as we have only requested for non-sensitive scopes. No app verification is need for these scopes, and can be launched to production immediately.

  • Finish the final steps for OAuth consent screen.

  • Now create credentials for "OAuth client ID". Application type should be set to "Web Application".

  • Add a URL for Authorized redirect URIs. Set this as http://localhost:8080/google-auth for the current app.

After getting the API keys, save then in settings.json file.

{ 
	"client_id": "xxxxxxxxxxx", 
	"client_secret": "xxxxxxxxxxx", 
	"redirect_url": "xxxxxxxxxxx" 
}

Basic Understanding of Google OAuth 2.0

  1. The home page of the app (index.html) contains the Google OAuth login url. User will be redirected to Google's OAuth 2.0 server on clicking this.

    https://accounts.google.com/o/oauth2/v2/auth?
     scope=https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email&
     redirect_uri=http://localhost:8080/google-auth&
     response_type=code&
     client_id=xxxxxxxxxxx&
     access_type=online
    
  2. Google will then prompt the user for consent.
  3. Upon user consent (or denial) Google will redirect the user to the redirect url page of the application (/google-auth in this case). A parameter named code is also appended to the redirect url (so the url becomes http://localhost:8080/google-auth?code=xxxxxx).
  4. Our application then needs to use this code to make an API call to get access token from Google.
  5. Then using the access token, another API call needs to be made to get profile information of the user.

Implementing Google OAuth APIs

We need to implement 2 Google API calls as per the OAuth protocol. One would get the access token. The second would get the user profile information.

We implement both these API calls in the google_login.js file. This file can be later imported in the main app script as an ES6 module.

/* client_id : google app client id
client_secret : google app client secret
redirect_url : google app redirect url
code : oauth code that was received */
export async function getAccessToken(client_id, client_secret, redirect_url, code) {
	let post = 'client_id=' + client_id + 
				'&redirect_uri=' + redirect_url + 
				'&client_secret=' + client_secret + 
				'&code=' + code + 
				'&grant_type=authorization_code';

	let response = await fetch('https://www.googleapis.com/oauth2/v4/token', {
		method: 'POST',
		headers: {
			'Content-Type': 'application/x-www-form-urlencoded'
		},
		body: post
	});

	if(response.status != 200)
		throw new Error('Error : Failed to receieve access token'); 

	let json_response = await response.json();
	let access_token = json_response['access_token'];

	return access_token;
}
/* access_token : access token */
export async function getProfileInfo(access_token) {
	let response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo?fields=name,email,id,picture,verified_email', {
		method: 'GET',
		headers: {
			'Authorization': 'Bearer ' + access_token
		}
	});

	if(response.status != 200)
		throw new Error('Error : Failed to get user information'); 

	let json_response = await response.json();

	return json_response;
}

Main Application Script

app.js is our main application script.

  • We need to import the necessary modules. In additional to the standard module that implements the server, we are also importing the third-party dejs module which is required for rendering HTML template files.

    We are also importing the google_login.js module that implements Google API calls.

    import { serve } from "https://deno.land/std@0.90.0/http/server.ts";
    import { renderFileToString } from "https://deno.land/x/dejs@0.9.3/mod.ts";
    import { getAccessToken, getProfileInfo } from "./google_login_api.js";
    
  • We need to read the API keys.

    const settings = JSON.parse(await Deno.readTextFile("settings.json"));
  • We need to prepare the Google OAuth login url, and urlencode it.

    let google_oauth_url = new URL('https://accounts.google.com/o/oauth2/v2/auth');
    google_oauth_url.searchParams.set('scope', 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email');
    google_oauth_url.searchParams.set('redirect_uri', settings.redirect_url);
    google_oauth_url.searchParams.set('response_type', 'code');
    google_oauth_url.searchParams.set('client_id', settings.client_id);
    google_oauth_url.searchParams.set('access_type', 'online');
    
    // google oauth url
    google_oauth_url = google_oauth_url.toString();
    
  • We then start the server at http://localhost:8080/ and handle the routing.

    const server = serve({ hostname: '0.0.0.0', port: 8080 });
    
    for await (const request of server) {
        let request_url = new URL(request.url, 'http://localhost:8080');
       
        switch(request_url.pathname) {
        	case '/':
        		// send google oauth url to the template
        		let html = await renderFileToString('views/index.html', { google_oauth_url: google_oauth_url });
        		request.respond({ status: 200, body: html });
        		break;
    
        	case '/google-auth':
        		try {
        			let access_token = await getAccessToken(settings.client_id, settings.client_secret, settings.redirect_url, request_url.searchParams.get('code'));
        			let profile_info = await getProfileInfo(access_token);
        			
        			// send profile info to the template
        			let html = await renderFileToString('views/google-auth.html', { profile_info: profile_info });
    	    		request.respond({ status: 200, body: html });
        		}
    			catch(error) {
    				// send error message to the template
    				let html = await renderFileToString('views/google-auth.html', { error: error.message });
    	    		request.respond({ status: 200, body: html });
    			}
    
        		break;
        }
    }
    

Our HTML Templates

  • The views/index.html template file just displays the Google OAuth login url (which is passed to it as a template variable).

    <body>
    
    <a href="<%= google_oauth_url %>">Login with Google</a>
    
    </body>
    
  • The views/google-auth.html template file displays the user profile information, or a message in case of an error (these are passed as template variables).

    <body>
    
    <% if(typeof error != 'undefined') { %>
    	<h3><%= error %></h3>
    <% } else { %>
    	<table>
    		<tr>
    			<td>Name</td>
    			<td><%= profile_info['name'] %></td>
    		</tr>
    		<tr>
    			<td>ID</td>
    			<td><%= profile_info['id'] %></td>
    		</tr>
    		<tr>
    			<td>Email</td>
    			<td><%= profile_info['email'] %></td>
    		</tr>
    		<tr>
    			<td>Picture</td>
    			<td><img src="<%= profile_info['picture'] %>" /></td>
    		</tr>
    	</table>
    <% } %>
    
    </body>