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.
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.
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.
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
Post a Comment