Build GraphQL Authentication Resolvers for Google Auth
We'll continue to update the GraphQL resolver functions we've prepared to allow users to log-in & log-out of our application.
Get the project source code below, and follow along with the lesson material.
Download Project Source CodeTo set up the project on your local machine, please follow the directions provided in the README.md
file. If you run into any issues with running the project source code, then feel free to reach out to the author in the course's Discord channel.
This lesson preview is part of the TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two course and can be unlocked immediately with a single-time purchase. Already have access to this course? Log in here.
Get unlimited access to TinyHouse: A Fullstack React Masterclass with TypeScript and GraphQL - Part Two with a single-time purchase.
[00:00 - 00:19] We've set up the functions needed to interact with the Google Node client to either generate an authentication URL or get user information for a user logging in. We'll now begin to establish our GraphQL schema and resolvers for the fields our client application can interact with to handle this functionality.
[00:20 - 00:33] We'll first modify our GraphQL schema and introduce some new type definitions. We'll leave our auth URL as type string since this field will return a URL string.
[00:34 - 00:50] We will want the client to pass a code argument to the login mutation to help conduct the login process in our server. Good convention often finds people specifying the arguments of a mutation within an input object.
[00:51 - 01:10] We will create a new input object type to represent the input for the login mutation called login and we'll state that it is to contain a required code of type string. We'll state that the login mutation field is to accept an input argument of login inputs.
[01:11 - 01:34] We'll also say that the login mutation should not require the login inputs since in the next couple of lessons we'll investigate how logins can be made with the presence of a cookie. The login and log out field mutations when resolved should return a viewer object.
[01:35 - 01:43] Viewer is an object that represents the actual person looking or using our app. That is to say the person viewing our app.
[01:44 - 01:57] We'll create this viewer object type to represent what this viewer type contains. It will have an ID which is a unique identifier since every user in our database is to have a unique ID.
[01:58 - 02:10] It will have a token for login session information. This would help encountering cross site request forgery attacks with which we 're going to learn more in module 5.
[02:11 - 02:26] It will have an avatar which will be the viewer's avatar image URL. It will have a has wallet field which is a boolean value to indicate if the viewer has connected to a payment processor and it will have a did request field.
[02:27 - 02:41] A boolean value to indicate if we've already attempted to obtain viewer's info. Now these fields or the majority of these fields would be needed in the client side of our application at different points as we begin to build our app.
[02:42 - 02:53] We'll get a better understanding of these fields once we start to write more of our implementation. All the fields of viewer except for the did request field are optional.
[02:54 - 03:10] This is because the person viewing the app could be logged out or doesn't have an account on our platform. However, we still would want our client to know that we've attempted to obtain the viewer's information but have failed which is why the did request field is a non option boolean value.
[03:11 - 03:25] This will make more sense later. We'll then say the login and logout fields would return a viewer instance when successful.
[03:26 - 04:00] We'll create the corresponding TypeScript definition for the viewer object in our TypeScript code and we'll do this in the lib types file since this viewer interface type will need to be accessed in multiple parts of our app. The only difference from our GraphQL schema definition is that this viewer interface will have an underscore id field instead of just id because we're going to use the same underscore id field from our Mongo database and only return it as id and are soon to be created resolver function in the viewer object type.
[04:01 - 04:44] It would also have a wallet id field instead of the has wallet property because wallet id will be the actual wallet id from Stripe with which we'll set up in a later lesson and we don't want to pass this sensitive information to the client which is why we resolve it to the client as a has wallet field that is true if the viewer has a wallet id or false if the wallet id does not exist. Both the underscore id and wallet id conversions to their corresponding GraphQL fields can be done in the viewer object GraphQL resolver functions with which we'll do right now.
[04:45 - 05:10] If we recall trivial resolvers don't need to be declared but we'll need to declare the resolver functions for the id and has wallet fields for our GraphQL viewer object type. In the id resolver it will take a viewer input which is of type viewer from our TypeScript types definition file with which will import and it will return the value of viewer. underscore id.
[05:11 - 05:38] The has wallet field will take the same viewer input and return true if the wallet id value exists otherwise we'll simply have it return undefined. We could have it return false but in our context we'll say the viewer either has a has wallet property set to true or it's undefined if the wallet id does not exist which should give us a similar outcome.
[05:39 - 06:14] We'll now modify our entry point query auth URL resolver to return the auth URL from our Google object we created in the API folder. We'll import the Google object and use a try catch pattern to catch any errors and this is where we'll just throw a new error statement with custom messaging if an error is to occur.
[06:15 - 06:31] We'll now modify our mutation login resolver function to take the code return from Google server and exchange it for user information for the user that intends to log in . This is going to take a decent amount of code so we'll try and explain everything step by step.
[06:32 - 07:12] The login mutation expects an input that contains a code property so we'll define an interface type for this input in a types file kept within the viewer folder we 'll call the interface log in args and we'll state that the input arguments could either be an object that has a code of string type or the input object might just be null if it's not provided. This login mutation will be fired in two cases.
[07:13 - 07:27] One where the user signs in with the Google auth URL and the other will be based on the user's cookie session. What we're going to write right now is only focus on the logging in and retrieving user information with the help of the Google auth URL.
[07:28 - 07:37] The code is only applied in this situation. We'll check for the presence of the code argument and set it to a constant of the same name.
[07:38 - 07:48] If it doesn't exist we'll set the value of the constant to null. Next we're going to create a random string to use as a session token.
[07:49 - 07:59] To do this we'll use the crypto module available in the node ecosystem. The crypto module provides cryptographic functionality to help handle encrypted data.
[08:00 - 08:14] We'll import the crypto module and create a random hex string of 16 bytes with the help of the module. We'll assign this value to a token constant.
[08:15 - 08:35] This token string will be randomly generated every single time a user is logged in and this value is returned to the client application. The client will eventually use this token on every request it intends to make with which will be used to authorize the request to prevent cross-site request forgery.
[08:36 - 09:09] We're going to investigate this some more in the next coming lessons and the next coming modules but for now we'll store this token or this randomly generated token in our database. If the code exists with which it will when the user assigned in via the Google Authflow we'll call a function that will shortly create called login via Google and will pass the code, token and database values to the function and will assign the results of this function to a constant we'll call viewer.
[09:10 - 09:46] We'll access the database object from the database available as context in our resolver functions. We'll then check if viewer doesn't exist which probably means we weren't able to sign the user in and get the appropriate information for the viewer.
[09:47 - 10:11] If that's the case we'll have our login mutation simply return an object with the did request field set to true so the client will recognize a request has been made and no viewer information is available. If the viewer does exist we'll return the viewer data we'll get from our abstracted login via Google function and we'll also state the did request field is set to true.
[10:12 - 10:31] If an error occurs anywhere in our tri-statement we'll throw an error with a custom message. To complete our login resolver functionality we'll need to build out our login via Google function.
[10:32 - 10:48] We'll create a dysfunction at the top of the file above our viewer resolvers map. This login via Google function will take a code and token arguments of type string and a database object of interface type database.
[10:49 - 11:04] When this asynchronous function is to be resolved successfully we expect it to return a user document from our user's collection for the user that's just signed in. If unsuccessful it will return undefined.
[11:05 - 11:20] The first thing we'll do is use the login function we created in the Google object within our API folder and we'll pass the code along. We'll destruct the user data from this function when it resolves successfully.
[11:21 - 11:38] If this user instance doesn't exist we'll throw an error and set and say Google login error. If the user is available we can begin to access the fields from this user object that we'll need such as the user email, display name and avatar.
[11:39 - 11:53] Unfortunately it appears the information we're looking for in this user data object is nested multiple levels deep in fields that could be conditionally defined or undefined . So we're going to have to write a decent amount of code to get what we're looking for.
[11:54 - 12:02] We'll first get the list of user names, photos and emails. and then we'll do the same.
[12:03 - 12:07] And then we'll do the same. And we'll do the same thing.
[12:08 - 12:13] And then we'll do the same thing. And then we'll do the same thing.
[12:14 - 12:18] And then we'll do the same thing. And then we'll do the same thing.
[12:19 - 12:24] And then we'll do the same thing. And then we'll do the same thing.
[12:25 - 13:10] From the list of user names we'll get the display name from the first user name . From the list of user names we'll also get the user ID of the first user as follows.
[13:11 - 13:46] We'll get the user avatar from the URL from the first item in the photos list. And finally we'll get the user email from the first email in the emails list.
[13:47 - 14:03] If either the user ID, user name, user avatar or user email constants are unavailable, we'll throw an error. We're going to provide this code in the lesson documentation.
[14:04 - 14:22] And the main takeaway here is we're just trying to get the ID, name, avatar and email fields of the user signing in with Google OAuth. Once we have the information about the user that has signed in, we'll first attempt to check if this user exists in our database.
[14:23 - 14:34] And if it does, we'll update it to the latest information we got from Google. Luckily, there's a Mongo function called find1 and update that can easily accomplish this task.
[14:35 - 15:01] We'll set up the find1 and update function and assign the result to an update res constant. Our find1 and update function works as follows.
[15:02 - 15:15] Underscore ID user ID is the filter object. Mongo will select the first document that matches this parameter in which the underscore ID field of the document matches the user ID value.
[15:16 - 15:23] The second is the update object. This object specifies how to update the selected document.
[15:24 - 15:40] If we found a document with the matching ID, we'll simply update the name, avatar and contact fields of our documents with the latest Google data. In addition, we'll update the token field with the most recent randomly generated session token.
[15:41 - 16:00] And return original false basically means we want to return the updated document, not the original document and assign it to the update res constant. If we weren't able to find an already existing user in our users collection, we 'll want to insert and add a new user into our database.
[16:01 - 16:20] We can access the return value from the value field of the update res object and if it doesn't exist, we'll now look to insert a new document. To insert a new document, we'll use the insert1 function that Mongo provides to insert a new document to the user's collection.
[16:21 - 16:37] Insert1 takes a document and just inserts it into the collection. We'll want to insert a document that matches the setup for how a user document is to be shaped.
[16:38 - 16:49] We'll specify the underscore ID, token, name, avatar and contact fields. We'll set income to zero since a new user will have no income and bookings and listings will be empty arrays.
[16:50 - 17:06] The document that has been inserted can be accessed with the ops array within the returned statements. We'll get the first item from the array since we're only inserting a single document and set it to the viewer let variable.
[17:07 - 17:25] And at the end of our login via Google function, we'll simply return the viewer object that arrives from either updating a document or inserting a new document. Our login mutation will now do as intended and authenticates with Google OAuth.
[17:26 - 17:41] Either updates or inserts the viewer in the database and return the viewer and viewer details for the client to receive. We'll ensure the return type of the login mutation is to be a promise that when resolved is to be the viewer object.
[17:42 - 17:54] The last thing we'll do is update our logout resolver function. This will be simple and since we're not dealing with cookies just yet, we return an empty viewer object with only the did request field set to true.
[17:55 - 18:11] We'll keep it within a try catch statement to keep things consistent. Okay, that was a lot of code.
[18:12 - 18:30] The resolvers for authenticating with Google OAuth are by far our most complicated resolver functions. At this moment with our server running, when we head to the GraphQL Playground and when we query the OAuth URL, we'll actually be able to see the authenticated URL Google returns to us.
[18:31 - 18:42] If we were to place the URL in a new tab and not being logged into Google, we 'll be presented with the consent form that says we can sign in to the tiny house application. How cool is that?
[18:43 - 18:54] In the next lesson, we'll begin to work on the client side of our app to consume what we've done on the server and we'll also be able to verify how our login mutation will behavioral behavior.