Google OAuth 2.0 With NodeJS (No Passport or googleapis)

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
single sign-on integrations with trusted providers such as google one of the easiest and most secure ways to build authentication into your application in this video we're going to build a google oauth integration so what we learn by watching this video you're going to learn how to get google oauth api keys you're going to learn how oauth works [Music] and you're going to learn how to build an oauth integration from scratch with node.js so what will we not be using we're not going to use passport and we're not going to use google api's npm package and the reason we're not going to use either of these is because if we don't you'll be able to see every single request that's happening behind the scenes and you're going to have a better understanding of how oauth works so before we get started let's take a quick look at this diagram that just shows the flow that we're going to build so the user is going to click a login link and when they click that login link they're going to be sent to a consent screen and this consent screen is hosted by google and once they give their consent for our application to have access to their account they're going to make a get request to our callback endpoint and this get request is going to include a code so then our application is going to use that code to get an id and access token then we're going to use that id and access token to get the google user we're going to upset the user into our database so upset means that we're going to try update the user and if they don't exist we're going to create them then we're going to create a session and we're going to create access and refresh tokens we're going to set some cookies for the user and then they're going to get redirected back to the user interface so this tutorial follows on from a rest api tutorial that we've built earlier on [Music] if you're coming from those tutorials welcome back thank you for sticking with me but if you haven't watched any of those tutorials that's okay you can still follow along i'll give you a github repo to clone so let's get started by cloning that repository so i have this repository here and it's under tom does tech slash rest api with user interface so i'm going to copy the ssh link here you might want to use https but i like to use ssh i'm going to come back to vs code i'm going to open a terminal and i'm going to type git clone and i'm going to clone this repository into a new folder called google oauth tutorial once that repository has finished cloning i'm going to cd into google oauth tutorial and i'm going to open this in a new vs code window so we're going to go ahead and start the server so to start the server you need to have a running instance of mongodb i'm going to cd into the server and i'm going to type yarn to install all the dependencies and while they're installing i'll split my window here and i'll install the dependencies for the user interface as well so cd into ui and we'll do the same command just type yarn or you could use npm install if you like so the client expects a specific version of node to be installed so it expects 12.22 or 14.17 i have this 14.17 installed with node version manager so i'm going to type nvm use 14.17.0 if you're not using node version manager i highly recommend it it is really easy for doing things like this if you'd like a tutorial on installing that with mac or windows please let me know and you can see there i've switched my node version so i'm just going to type yarn again and these dependencies will install so while these dependencies are installing let's go get our google auth credentials so i just come over to google and i just typed in google award and this first link here is some instructions on setting up a google oauth integration so you can see here this is about getting the access keys so let's skip all this and the one thing we need from this page is this google api console so we can click on that and you need to have a google account obviously so i'm going to give you a warning about this console page it is pretty annoying it often doesn't work and stuff is all over the place but it's better than other providers such as as you are so i'm going to click create project and i'm going to call my project google off and i'm not going to have an organization i'll explain a little bit about what organizations do in a minute but if you're creating a public-facing application you don't need to have an organization so my project has created here and so i'm going to come down to credentials and you can see up here we can create some credentials and they will go down in this oauth 2 client id section but before we can create credentials we need to create a consent screen and the consent screen if you remember from that diagram is where the user will get redirected to after they click the link on your website so we want to choose the user type so internal means that only users within the organization that you selected will be able to get access to this application and be able to log into your application if you want to make a publicly facing application which i assume you do you need to create an external integration so click create and i'm going to give my application a name so i'm just going to call this google or tutorial the user support email is just going to be your email address you need to upload a logo so i have this expertly designed logo here application home page so i'm just going to put in my localhost address i'm going to do localhost port 3000 and this is the port that our client will start on so for our privacy policy link i'm just going to say localhostport3000 slash privacy and for terms of service i'm just going to do the same but i'm going to call this tos so we don't need to add any authorized domains and for the developer contact information i'm just going to use the same email address tom does.tech90 gmail.com please don't send emails to this email address because i never look at it okay so we need to add some scopes so there's lots and lots of different scopes that you can add to your application but the only ones that we need now are going to be this all slash userinfo.profile and use info.email let's update that and let's click save and continue add a test user so this is a user that will be able to log into your application while it's still in sandbox mode so this is quite important so i'm going to add my email address um does tech 90 at email.com and this means that you can test your application without bringing it out of sandbox mode i'm going to click save and continue and let's go back to our dashboard so now we have our consent screen configured we can go back to credentials and i'm going to click create credentials and we want to create an award client id so the application type is going to be a web application i'm just going to call mine google oauth tutorial for ads and we need to add an authorized redirect url so i'm going to say http colin collins colon local host and port 1337 because this is our server so there's a few different ways you can do this you can redirect back to your client and from your client make another request from the server but i find it easier to make this get request straight from the consent screen it works out a little bit quicker so the redirect url is going to be the service address slash api slash sessions so you can make this path whatever you like obviously you can make it google redirect uri but i think this path makes sense because we're going to create a session we're going to do that with oauth and then here we have the provider if you have multiple providers such as azure you might then want to have a redirect url that is slash azure but we're going to make this one slash google so click create and here we have a client id and a client secret so i'm going to copy this client id and i'm going to come over to the server and in the config i'm going to add this client id so i'm going to call this google client id and then i'm going to do the same with the secret so this secret is obviously a secret you do not want to show everybody this like making a youtube tutorial where you display this publicly is probably a really bad idea so if you're going to commit this package you don't want to just hard code this into your config file and then send it up to your source control such as github you'll want to put this in an environment variable and store it only locally but i'm going to delete this application after i finish making this tutorial and so you won't be able to use this secret so in here i'm going to also make a google awards redirect url so i'm going to say google auth redirect url i'm going to make the value of this or 3 direct url the same as we did when we created the credentials let's say http localhost 133 and actually it's probably better if we come back into our client ids and we just copy it from here that's going to be much more reliable let's grab this and i'm going to paste it in here so now we have these credentials set up on the server on the client we just need to add the client id so you obviously don't want to put this client secret into your client application so we're using next.js so i'm going to create an environment variables file called n dot local this is supposed to be dot end.local so in here we're using next.js so this is just going to work out of the box and we need to prefix public environment variables with next public so i'm going to say next public underscore and copy that three times because we need three of these i say google client id equals the next one we need is a server endpoint server endpoint equals and we need the google oauth redirect url so i'm going to say google oh redirect url equals so this google oauth redirect url is the same as we put in when we created the credentials the server endpoint is just going to be this portion of the url and then we can get the client id from our server so it's going to be this long string here okay great so that is all of our credentials that we require so i'm going to start the server with the on dev and i can start the client with yarndev also so i'm going to split my window here and put a browser on the right here so we can see what we're working with in the browser i'm going to go to localhostport3000 and you can see in our client we just have this please login text here so we need to put a link on this page that goes to the consent screen so we need to build that link based off our client id and a bunch of scopes and stuff like that so i'm going to show you how to make a function to build this link so in the utils let's create a new file called get google url dot ts and i'm going to say function get google oauth url and i'm just going to export defaults get google oauth url so let's define the root url so i'm going to say const root url equals and this is going to be https accounts.google.com oh slash oauth2 version 2 slash auth and let's define some options so i'll say const options is equal to and options is going to be an object let's define our url search params so what we're doing here is we're defining some options in an object and then we're going to add those options to a query string so we need a way to build that query string so i'm going to say const qs so you used to be able to just import query string from query string like this but this is deprecated and we're going to use url search params instead so we don't need to import that so i'm going to say consques equals new url search params and we're going to pass in our options then i'm going to return and i'm going to use our templating strings i'm going to start off with the root url and then i'm going to put a question mark because we're going to have some query strings here and then i'm going to put in another template here and i'm going to say qs dot tostring and then i'm going to execute that so let's start defining our options so i'm going to say redirect uri is going to be process dot m and we get this from this next public google oauth redirect uri i'm gonna pop that in there and say client id is going to be process dot m dot and we're going to get this from next public google client id say access type is going to be offline response type is going to be code prompt is going to be consent and scopes is going to be an array and these are the scopes we defined when we created our consent screen so the first scope is going to be use info.profile and then the second scope is going to be userinfo.email so we need to join these scopes so let's call dot join and we're just going to join these with a space so typescript is complaining here it's saying the client id needs to be a string so we know the client id is going to be a string but it says here that it's going to be string or undefined so let's just say as string and we need to do the same or redirect url so i'm going to say as string and typescript is now happy so let's add a console log for these options console.log options and let's add another one for this q s constants console.log and i'm just doing this so you can see what this looks like and let's go to our index page and where it says please log in i'm going to add a link i'm just going to use an anchor tag and the href is going to be equal to our get google award url and we're just going to execute that function and i'm going to say log in with google let's open up our console so we can see those parameters inside the get url function you can see here we have our options let's compare that to this and so our client id has been successfully inserted and same as our redirect url and we have all these scopes and you can see here that these scopes have been joined let's have a look at our query string and you can see it's just a url search params constructor let's call call.tostring on that here so we can see what it actually looks like in the url and you can see here that it combines all of these options and it separates them with an and symbol let's remove these console logs and i'm going to click this login link and it should take us to a google oauth login page let's click this and so we have an error here missing required parameter scope so the reason for that is because we've called this scopes instead of scope so i've made this integration now probably 10 times in my career and i've never got this right the first time let's try this login link again and here we go we can log in with our google oauth account so let's click this link here and see what happens so i've opened up the network panel and i've removed my filters so i'm going to click this link and you can see we've returned a 404 and we've made this request down here and this is a get request to localhost port 1337 api and the interesting thing about this is we have this code on the query string the reason this is returning a 404 is because we obviously haven't created this endpoint so let's go do that now so i'm going to close out all my ui because we're finished with this for now and come into server and i'm going to open up source and i'm going to open up routes and i'm going to define a new route and this route needs to match that get request so i'm going to say app dot get api sessions slash or slash google and we don't want any middleware on this request so we want to create a handler that says google auth envelope so let's go create this handler and i'm going to create this inside of my sessions controller i'm going to collapse all of this and i'm going to say export async function google or worth handler and we have a request and response here so i'm going to copy that from above so i'm going to import this handler inside of the routes and this comes from the session controller so i'm going to import it here from controllers session.controller and it looks like our routes are happy so before we go any further let's have a think about what it is that this handler should do so we can refer back to our diagram to have a look so we've done the consent screen we've done the login link and now we're working on the callback here so we're going to get the id and access token from the code okay so we know we need to get the code and we looked at this url before and we know that that code exists in the query string we obviously need to get the code out of the query string and then we need to make a request to get the id and access token so let's go write that down so we need to get the code from the query string and then we need to get the id and access token with the code okay then we need to get the user with the tokens okay let's say get user with tokens then we need to upset the user and create a session so we're going to upset the user we're going to create a session then we're going to create access and refresh tokens and then the final part here is we're going to set a cookie and then we're going to redirect back to the client so set cookies redirect back to client okay this looks good so let's go start filling this out i'm going to say const code is equal to request dot query dot code and we know this is going to be a string so let's type this as string so get id and access tokens with the code so to do this i'm going to create a service inside of user service i'm going to come into user service i'm going to say export async function get google auth tokens and this is just going to take one argument and that is the code and then we can type the code here mode going to be a string so we need to make network requests so before we go any further let's install something to make these network requests so i'm going to say yarn add axios so while that's installing i'm going to define a url equal to https oauth 2.google apis dot com [Music] slash token so if you use the google api's npm package it probably has stuff to do this for you i can't remember it's been a while since i used it i tend to just do this all myself because i think it's not that difficult and say const values and we're doing a similar sort of thing that we did before so we're going to define a base url we're going to define some values and then we're going to turn those values into query strings and i'm going to send a request off to google i'm going to say code client id and we need the config module for this let's go make sure we've imported configs import on big from config and we're also going to import axios from axios while we're up here so i'm going to say config.get and we call this google client id we need our client secrets i'm gonna say client secret i'm gonna do the same thing config. you get google client secret we need our redirect uri so i'm going to say redirect uri it's config.get google or redirect uri and our grant type is going to be authorization underscore code okay so these are our values so now let's use these to make a network request and as we know network requests can fail so we need to wrap this in a try catch block so i'm going to say try and catch the error down here define error as any up here i'm going to say const res equals await axios dot post and we're going to post to our url just to find up here and our url is going to have a body of qs dot stringify and if you remember before qs comes from the query string package so let's import that and when i said it was deprecated i mean that it's deprecated on the client you can still use this inside of node and this comes from qs sorry and i also need to set some headers here we need to pass in values into our qs dot stringifier and we need to set some options and in options we need to set some headers and the one header we need to set is going to be content type so i'm going to say content type and this is going to be equal to application x www.form url encoded so this is just going to tell google oauth apis how we've packaged up this data so we've done it by a url encoding and finally we're just going to return res.data if we have an error message we want to log that error so i'm going to say log i'm going to import my logger.error i'm going to put in the error here and then i'm just going to say failed to batch google or tokens i'm going to throw new error and message okay this looks good let's go use this function inside of our controller and say const and then i'm going to destructure here i'm going to say equals await get google oauth tokens and we just have a code here so we know this function returns an id token and it also returns an access token so we've done this part here we can console log these here and just make sure this works i'm going to say console.log i'm going to add the entire object to the console log start the server back up now i'm dev now that the server has started back up we can click the log in with our google account so we have an error here it says google or 3direct url is not defined so let's have a look inside of our config module in default and we have google or three direct url here and i assume i just spelt this wrong go back into this function okay that was url not uri and we've also called this value not values and let's also tell typescript what this function should return so i'm going to say interface google tokens result and this is everything it's going to return it's going to return an access token and expires in which is going to be a number refresh token scope and an id token the two that we care about is this access token and id token so let's come type this so we have an async function here so we know this is going to return a promise and then we know the promise is going to return this object here so let's also tell axios what it is that we're expecting the request to return so i'm going to use this generic here i'm going to pop that in there so we've failed hard on the server so i'm just going to restart it here so let's click log in with google again i'm going to give my consent and we should expect some tokens to appear in the console here so we have a 403 error here so this means that we've sent our post request in user service and google apis has returned a 403 which is unauthorized so i'm not sure why it sent this response but we're going to figure it out so the first thing i'm going to do is to check these values here and they all look correct but let's console log them out and console log values make sure they're all good and then the next thing i'm going to do is i'm going to add a console.error here and i'm just going to console error and we're going to put our error in here let's save this and we can try again and the reason we're getting an unhandled rejection is because in our controller over here we're not catching these errors so let's do that because having unhandled errors is very bad practice and it's going to lead to a lot of problems so you're going to make sure you are handling these errors and getting the errors during development is actually kind of good because it means that you will be forced to handle them so if it does throw we're just going to return and say res.redirect and we want to redirect back to the client so i'm going to say config.yet and in our config we have this origin that we use for cause so i'm just going to redirect back to this origin here but if you don't have this origin set up because you didn't follow my previous tutorials that's okay i'll forgive you but you'll need to set up a path back to the client so i'm going to say slash slash error and you'll probably also want to log this error so log i'm just going to put in the error and let's be consistent here with our naming okay so we've got some good logging in place now let's go try this again so we've introduced another error here we have logger.default it's not a function so that's because we haven't put log.error we've just tried to call it defaults export here which is not valid okay so you can see our client has redirected back to oauth error so that's what we expect to happen and we have a bunch of stuff here so we have this big error object here so we have our code being populated we have a client id we have our client secret here and we have the redirect uri so the reason i logged the error is because you can see down here we get a response and then we get a data object and we get error and there's something hidden in here so let's try and get that out i'm going to say error dot response dot data dot error and hopefully that's the right path to the error so we're getting 403 and we get an error message here that is much better ssl is required to perform this action so my suspicion is that we're using http here and it should be https so this is a good example of why you should copy and paste urls and not try to type them out yourself because you're going to get them wrong or at least i am anyway all right let's try this again okay so we get an access token and we get this id token here so let's have a look in our controller you can see the next thing to do is to get the google user let's have a look at what these tokens contain so you can see that they're base64 encoded so i'm going to come over to jwt.io i'm going to paste in the contents of this token and you can see here that we get a google payload you can see here we have the email address we have a email verified boolean we have a name we have a picture given name family name all this good stuff here so we could use this to get the user that would be a valid approach and in fact that would be the one that i recommend you do but i'm also going to show you how you can make a network request to get the user so let's get the user out of this id token here so i'm going to say const google user is equal to and we're going to use jwt.decode be careful with jwt.decode because it's not going to verify the token but we know that this token is signed by google because we've just made this request server side i'm going to import jwt from json web token so usually you would want to use jwt dot verify you can verify that the token was signed with the key that you expect it was scientists say dot decode let me say id token and let's say console.log google user so this is perfectly valid this is a good approach to this implementation but you can also use a network request to get the user so for completeness sake i'm just going to show you how to get that user with the network request so the reason that you would want to do this is because if you just set these tokens on the client and the client makes requests to your application with these tokens you'll want to be able to get the user and so you need to have a network request to do that so i'm going to say export async function get google user and this is going to take an id token and an access token i'm going to add a try catch block here and i'm going to say const res equals await axios dot get and i'm going to get this url here google apis such i want to slash v1 user info json access token and i'm going to append the access token to the endpoint i'm also going to set some headers and the header that we're going to set is authorization and we're going to add bearer and id token so we're going to use the id token to verify that we are who we are and we're going to tell google we want to use this access token to get the user info and because we added user info to the consent screen we're going to have access to this given these tokens here so now let's just return res.data let's catch our error here so i can say log.error add the error error batching google user i'm going to grow the error and let's also type this here so i'm going to use this pre-prepared interface here as you can see we have an id email verified email name given name picture and local so we're going to say that this returns a promise and the promise includes our google user result and we're going to do the same with our axios get let's come back to our controller and i'm just going to comment this out here i'm going to say google user equals await get google user and we're going to pass in our id token and our access token so let's move on to upselling the user so i'm going to come over to user service and i'm going to create a new service for finding and updating a user so i can say export async function find and update user so we're going to have a query we're going to have an update and we're going to have some options i need to move these into the argument so let's type this out so our query is going to be a filter query and this comes from mongoose so you can see here we have filter query and we have user document and if you haven't followed the previous tutorials use a document here is just a interface that extends mongoose.document our update is going to be an update query and this is going to take our user document as the generic input and our options are going to be query options and let's default this to an empty object because you don't have to have your query option set so let's return here use user model dot find one and update so we're gonna pass in our query our update options and our options so let's go use this function here to upset our user you may be wondering why i didn't create a function for upsetting a user and because upset is just an option on our options object here what we're actually doing is updating a user so i'm going to say const user equals weight find and update user so we want to find the user by the email address so the issue with this is that if you use a provider that doesn't verify email addresses apparently something like discord doesn't verify email addresses you could get somebody create a account with an unverified email address and then they could log into your system with that user so you need to make sure that you're using a provider that validates email addresses if you're going to do it this way if you want to you can also save the google id and you can verify them via the google id but that could result in duplicate accounts so i'm going to say email and then i'm going to say google user dot email and to double check this i'm just going to say up the top here if not google user dot verified email address then we're going to throw an error so i'm going to say return res.status 403 dot send google account is not verified so we're just enforcing that the user has verified the email address and we're also trusting that google has done this process correctly for email google user dot email so you may be wondering we're finding the user by the email but then we're also updating the email and this is because we're going to upset so if the user doesn't exist we need to provide this email address here so we can create them with that email you say name it's google user dot name and i'm going to say picture is google user dot picture and we don't have this picture on our model so let's add that but before we do that let's add our options so we're going to say upset is true and new is true so new just means that it's going to return the new document given that it's updated this user okay so let's add picture to our user model picture is going to be a string and it's not required save this okay so let's assume this has worked correctly and we're going to move on to creating our session so we've created a session before create user session handler so let's copy this here in fact let's copy all of this here we're going to create a session create access token and create a refresh token this all looks good i'm going to copy this and paste it down here so we have this create session service that we created earlier and it's going to create a session for that user id we're going to add the user agent to the session so we can see where the user logged in or what browser they logged in with rather we're going to create an access token with the user we're going to assign the session to that access token as well we're going to say that this access token expires in our access token time to live which is currently set to 15 minutes we're going to create a refresh token i'm going to spread the user we're going to add our session id we're going to add expires in and our refresh token time to live which is not 15 minutes it's actually one year so we've done this part down here now we need to set cookie and if i remember correctly we have already set cookies up here and we have let's paste this in here so we're going to say res.cookie in so we're going to say res.cookie i'm going to call this one access token we're going to assign the access token that we created up here to the value of the cookie we're going to set its max age to 15 minutes it should hp only is true domain is set to localhost because that is what we're using you will want to get this from config we're going to set same site to strict and then we're going to set secure to false we're going to do roughly the same thing with the refresh token i'm going to say max age and we're going to set this to one year and then the rest of the options are the same so we copied this from above which means that all of these options are being duplicated in this get use the session handler so let's copy out these properties and make some constants so we can guarantee that when the user logs in with username and password and with google or that they get the exact same session tokens so you can say const access token cookie options is equal to and i'm going to copy this say const refresh token cookie options is equal to an object i'm going to spread on the access token options the only thing that we need to change is this max age put it down here to make sure it's overwritten from this max age and we can copy these options and paste them let's paste that here and we can set this to refresh token put the options let's copy this and we can do the same down here and this looks good okay so the last thing to do is redirect back to the client so i'm going to say res.redirect and i'm going to use this config.get and i'm going to send them back to the origin which is just a client save this and we're ready to try this and we're going to hope that it works end to end click login and you can see here that we have logged in because we have our user here so let's check our cookies and make sure that they're set so you can see we have an access token here and we should have a refresh token as well yep that's down here let's go to jwt.io and check that our access token is looking good so you can see here that access token looks a little bit funny got all this stuff in here and this here looks like we need to call to jason on our user object so you can see up here that we've created a user but we need to convert it to a json object let's go to json i'm going to copy that and i'm going to do the same for the access token let's come back to our application here and open up my cookie editor and i'm just going to clear out all the cookies to make sure that we're logged out i log in again and we're logged in and we get our name here so our cookie must be good if you saw the login flash there that is because if we have a look at the get request back to our localhost my suspicion is that we're not sending cookies along with this request so request headers and there's no cookies here so the reason for that is going to be our cookie policy that we have to find at the top here so i'm going to set the same site policy to lacks and see if that helps and while we're here let's fix these typescript errors we have so i'm going to say these are cookie options and this just comes from express yeah so you can see this is imported here do the same with our refresh token options you can see touchscript is up here let's clear our cookies if you want to create a logout function you can invalidate the session and you can just clear the cookies from the user's browser so you would reset this access token and refresh token and you would just set them for a time to live of -1 for example or some date in the past and that would invalidate these cookies okay so this worked perfectly we didn't have a please log in flash up and i'm going to find the network request where we were redirected back to localhostport3000 so this is a request to the index of our application and you can see the request headers here include the cookies and so the only change we made there was we set the cookie same site policy to lacks from strict so that is how to build a google oauth integration if you like the tutorial please make sure you give it a thumbs up and subscribe make sure that notification bell is turned on so you get new videos in the series thank you for watching and i'll see you in the next video [Music] you
Info
Channel: TomDoesTech
Views: 1,228
Rating: undefined out of 5
Keywords:
Id: Qt3KJZ2kQk0
Channel Id: undefined
Length: 57min 18sec (3438 seconds)
Published: Tue Oct 26 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.