User Authentication in MongoDB with Google Sign-In (Tutorial)

   MongoDB has Quickly become my nonrelational database platform of choice for its high performance, broad developer support, and generous free tier. As is the case with many database engines, user management, and access control can get quite complex, especially when the software stack draws on other resources, like microservices or cloud storage. Fortunately, we can leverage so-called “federated identity providers” like Google, Facebook, and Amazon to decouple user authentication from the rest of the application.

In this tutorial, we’ll walk through all the steps necessary to build a secure MongoDB-powered application from scratch. Users will need to sign in with their Google account to read from the database.

We must first set up our cloud-based authentication flow, which includes coordination between Google Identity, AWS IAM, and, of course, our MongoDB cluster before we can write a single line of code.

Creating a Google Apps Account

The first step is to create a Google account for our application. This will allow us to customize the user experience by accessing customers' profile information, in addition to leveraging existing security infrastructure (and legitimizing the login page with a familiar UI).

To get started, go to the Google Cloud Console and create a new project with a descriptive name.


Once your new project is up and running, go to the sidebar and search for "APIs & Services," then select "Create an External User" under the "OAuth consent screen" button. You'll need to offer an application name and an email address for user assistance on the first page of the setup wizard.
]
We'll define our application scope on the next page, which specifies which components of a user's Google account we have permission to view or edit. Because we may seek users' contact information, calendars, location, or other personal stuff, this is one of the most important aspects of the entire process. Our program will be able to read a user's email address as well as their profile, which includes their name and avatar, in this demo.



Important note: If you're working in a production setting, you'll need to submit a Privacy Policy and other legal documents that spelling in out how this personal information will be used.
Assign at least one test user after that. Log-in access will be limited to these accounts until our app is officially approved for public use by Google.
We'll need to generate a Client ID for our Google app once we've set up the OAuth consent page. Navigate to the "Credentials" tab in the sidebar while still in APIs & Services. Select "OAuth client ID" under "Create Credentials."




Initialize the client as a web application and add http://localhost:8000 as an authorized JavaScript origin, since the login request will be made from our local development server (more on this later).


This will generate your app’s client ID (something like 1234567890.apps.googleusercontent.com). We will need this in a couple different places, so paste it somewhere for easy access.
This completes the Google Console part of the process. Now onto AWS.


The Google Console portion of the procedure is now complete. Now it's time to talk about AWS.
In AWS, create an IAM Role.
Log into your AWS Console Account after your application has been registered with Google. Click "Create Role" after searching for "IAM" and selecting "Roles" in the sidebar. Select "Web Identity" as the type of trusted entity and Google as the identity provider (a similar flow can be used for Facebook, Amazon, and other federated identity providers). Under Audience, paste your Google Client ID. For the rest of the Role configuration, accept the defaults.

Take note of the Role ARN (e.g. arn:AWS:iam::AWS ACCOUNT: role/roleName) once your Role has been created. We'll need it in the next step.

This completes the AWS setup required for this demo, but I will make the note that by adjusting the Policy Document attached to this Role, we can connect our application to other AWS resources, like S3 buckets and Lambda functions.
Configuring the MongoDB Database
Next, we need to launch and configure our cloud-based MongoDB cluster. Log into MongoDB Atlas and create an organization.



Within your Atlas organization, create a new project and give it a name.
Finally, create a database. For this demo, I’ll be using a free Shared Cluster.
For this cluster, be sure you use AWS as the cloud provider and the M0 Sandbox Tier (which is also free). You shouldn't need to link a credit card unless you've chosen to upgrade your capacity, which isn't required for our easy demonstration.


Grab a cup of coffee while your cluster is deployed. When it’s ready, click the “Collections” tab. If you’re unfamiliar with MongoDB syntax, “Collections” are akin to SQL tables, and “Documents” can be thought of as JSON-based entries or rows within tables. The first time you create a new collection, you’ll have the option to use a sample dataset or add your own. We’ll opt for the latter.

Assume our app is a fruit market that keeps track of the names, pricing, and remaining amounts of various fruits. In the "test" database, we'll add a few documents to the "marketplace" collection. The web interface is the quickest way to do this, however, mass insertions can also be done via the MongoDB shell or one of their many supported drivers. Because the focus of this lesson is on authentication, I won't go into detail about MongoDB's schema or usage.


Let's connect our database to our IAM Role now. Go to the "Database Access" tab on the side tab and create a new database user. Select "AWS IAM" for the Authentication Method and "IAM Role" from the "AWS IAM Type" dropdown menu.
Changing user privileges is another complicated topic that deserves its own blog. This user will have read-only access to the "marketplace" collection we've just created.
MongoDB may restrict access to certain IP addresses in addition to user-based authentication. To assist resist threats like DDoS attacks, we would only allow requests that come from our application's servers in a production environment. Because we'll be running the demo program locally, merely add your current IP address under "Network Access," following the concept of least privilege.
Putting everything together in a demo app, We're now ready to write some code now that the setup is complete. Signing in with our Google account should yield an ID token that can be used to retrieve credentials for the AWS IAM Role if all goes well. Our application's server will then read from the database using those credentials.




We sign in, retrieve the credentials, send them to our backend, then use the result to change the webpage on the client-side (the following snippet is taken from our index.html file).

/ javascript snippet from index.html
<script src="https://apis.google.com/js/platform.js" async defer></script>
<script src="https://sdk.amazonaws.com/js/aws-sdk-2.953.0.min.js">
</script><meta name="google-signin-client_id" content="{GOOGLE_CLIENT_ID}">
<script>
var AWS = require("aws-sdk");
AWS.config.region = 'us-east-1';
function signinCallback(googleUser) {
var profile = googleUser.getBasicProfile();
console.log('ID: ' + profile.getId()); // Do not send to your backend! Use an ID token instead.
console.log('Name: ' + profile.getName());
console.log('Image URL: ' + profile.getImageUrl());
console.log('Email: ' + profile.getEmail());
document.getElementById('profile-email').innerHTML = profile.getEmail();
document.getElementById('profile-name').innerHTML = profile.getName();
document.getElementById('profile-image').setAttribute('src', profile.getImageUrl());
document.getElementById('profile-card').hidden = false;
document.getElementById('signin-button').hidden = true;
document.querySelector('.fruit-list').hidden = false;
AWS.config.credentials = new AWS.WebIdentityCredentials({
RoleArn: '{ROLE_ARN}',
ProviderId: null, // this is null for Google
WebIdentityToken: googleUser.getAuthResponse().id_token
});
// Obtain AWS credentials
AWS.config.credentials.get(async function(){
// Access AWS resources here.
var accessKeyId = AWS.config.credentials.accessKeyId;
var secretAccessKey = AWS.config.credentials.secretAccessKey;
var sessionToken = AWS.config.credentials.sessionToken;
const response = await fetch('http://localhost:8000/fruits', {
method: 'POST',
body: JSON.stringify({
'AccessKeyId': accessKeyId,
'SecretAccessKey': secretAccessKey,
'SessionToken': sessionToken
}),
headers: {
'Content-Type': 'application/json'
}
});
const myJson = await response.json(); //extract JSON from the http response
const fruits = JSON.parse(myJson['fruits']);
console.log(typeof fruits);
console.log(fruits);
var str = ''
var arrayLength = fruits.length;
for (var i = 0; i < arrayLength; i++) {
str += '<li class="list-group-item d-flex justify-content-between align-items-center">' + fruits[i]['name'] +
'<button type="button" class="btn btn-primary position-relative">$' + fruits[i]['price'] +
'<span class="position-absolute top-0 start-100 translate-middle badge rounded-pill bg-danger">\n' +
fruits[i]['quantity'] +
'</span>' +
'</button></li>'
// str += '<li class="list-group-item d-flex justify-content-between align-items-center">' + fruits[i]['name'] + ' <b>Price: $' + fruits[i]['price'] + '</b> ' + '<span class="badge bg-primary rounded-pill">' + fruits[i]['quantity'] + '</span></li>'
}
document.getElementById('fruit-list').innerHTML = str;
});
}
function signOut() {
var auth2 = gapi.auth2.getAuthInstance();
auth2.signOut().then(function () {
console.log('User signed out.');
document.getElementById('profile-card').hidden = true;
document.getElementById('fruit-list').innerHTML = null;
document.getElementById('signin-button').hidden = false;
});
}
</script>





We're sending a POST request containing the AWS credentials to the /fruits endpoint within the signinCallback function (again, we're simplifying things for this demo by using an HTTP server — in production, you'd want to encrypt this connection in transit with HTTPS). Our FastAPI-based web server will handle this request as follows:


# /fruits endpoint from main.py
@app.post("/fruits")
async def fruits(request: Request):
body = await request.json()
access_key_id = urllib.parse.quote_plus(body['AccessKeyId'])
secret_key_id = urllib.parse.quote_plus(body['SecretAccessKey'])
session_token = urllib.parse.quote_plus(body['SessionToken'])
uri = f"mongodb+srv://{access_key_id}:{secret_key_id}@cluster0.bjrye.mongodb.net/myFirstDatabase" \
f"?authSource=%24external&authMechanism=MONGODB-AWS&retryWrites=true&w=majority" \
f"&authMechanismProperties=AWS_SESSION_TOKEN:{session_token}"
mongo_client = pymongo.MongoClient(uri)
mongo_db = mongo_client.test
fruit_list = []
fruits = mongo_db['marketplace'].find({}, {'_id': 0, 'name': 1, 'price': 1, 'quantity': 1})
for fruit in fruits:
fruit_list.append(fruit)
return JSONResponse({"fruits": json}) While I chose a Pythonic implementation, MongoDB supports drivers in hundreds of programming languages, so you could construct a scalable server in Go, Node.js, or Rust instead. The "fruits" of our labor: a straightforward application that is both personalized and secure (image by author). That's all there is to it! This infrastructure can easily be modified to handle other identity providers or other AWS resources, despite being a simple sample. My Github website has the complete sample code. Note from the editors of Towards Data Science: While we enable independent authors to publish articles that follow our rules and guidelines, we do not endorse each author's work. You should obtain professional counsel before relying on an author's work. For further information, please see our Reader Terms.


Comments

Popular posts from this blog

All You Need to Know About Amazon Web Services ECU vs. vCPU

Utilizing Amazon DynamoDB and AWS KMS, a serverless password manager

Explained: How to Configure a Static Website Using S3 and Cloudfront