00:09
to call this social auth ally example. We'll create a brand new project here together so that we know that we're starting at the exact same point as one another. Let's go ahead and enter there.
00:18
For this demonstration, we're going to make use of EdgeJS very slightly just to show our login with Google button. So let's go ahead and start with the web starter kit there, and we're going to use the session
00:27
authentication guard as well as Postgres SQL for our database. Now feel free to use whatever database driver it is that you use on your personal machine. I have Postgres set up online, so that's what I'm using here today.
00:37
While this is installing our dependencies, we can go ahead and open up Visual Studio Code open folder and get this project opened up. Okay, here we go.
00:45
By now, if we jump back into our terminal, everything should be good to go. Let's cd into our social auth ally example project, and we can clear out our terminal.
00:53
We'll do node ace add at AdonisJS slash ally to both install and configure the AdonisJS ally package inside of our project.
01:02
Now during the configuration step, it's going to ask us which providers we plan on using, and this is a multi-select, so you can select as many as you actually plan on using inside of your application.
01:11
For the most part, so long as you don't need any extensive user details, Ally is going to keep all of these different drivers consistent in its implementation. So today we're just going to go through how to approach this with Google, and you should
01:21
be able to fill in the rest on your own. So I'll select Google here, and we can move forward with that. This has now added in an Ally configuration, it's updated our AdonisRC.ts file, as well
01:30
as our environment variable expectations. So if we now jump into our project, take a look at those new environment variables. What it's expecting is a Google client ID and secret.
01:39
These are the environment variables Ally is going to use to authorize a request with Google. But right now we're going to leave these blank and we'll circle back after we've filled out all of our code necessary to implement AdonisJS Ally.
01:49
Now the very first question you may have about this process is what's the code flow that we're going to end up implementing here today in terms with authorizing against Google
01:57
and getting the user's details back as well as authenticating that user? Well as noted, we added in session authentication inside of our project, and we do actually need to make use of it inside of this flow.
02:07
So there's going to be two things that we're interfacing with. First there is a Google side, so we'll put that in a box of its own.
02:13
And then there is our AdonisJS application, so we'll have that in a box of its own there. When users click on their login with Google button, we're going to redirect them over to Google.
02:23
So this is our redirect step. Google will then ask the user to log in with their Google credentials if they are not already logged in.
02:32
If they are logged in, then it will simply ask them to authorize our application to access the user's Google account information.
02:40
So this may have a login step and then there's also an authorization step there as well that happens outside of our application.
02:48
Once the user has logged into their Google account and authorized our application to
02:53
access their Google user information, Google will send the user back to our AdonisJS application
02:59
via a callback URL that we've defined both in our AdonisJS application as well as our Google's configuration.
03:07
And that callback URL must match between the two in order for things to work. Once we get back into our AdonisJS application, that's where we'll need to discern whether
03:17
or not the user is registering or logging in. If they're registering, then we need to create a brand new AdonisJS user record for that
03:25
user using the Google information that's been provided to us via that callback. Otherwise, we can use that same user information to just log the user in if they already have
03:35
a user account inside of our application. And it's for this login and registration step that we're going to be using our session authentication with.
03:43
So even though the two login steps between our AdonisJS and Google application are separate and the only thing being shared between these two is the user's information, this whole
03:51
process as we'll see at the end of this lesson is going to be a seamless experience for our user. So let's first jump back into our terminal real quick and let's get a controller set up.
04:01
So node ace make controller, and we'll call this our social auth controller, and I'm going to do hyphen s there to keep it social auth rather than social auth.
04:09
And let's add in a redirect and a callback method inside of this controller. Clear that out and jump back into our text editor. We'll start by jumping into our routes file.
04:19
And for this flow, we'll need two routes. So we'll have a router dot post to Google slash redirect and use our social auth controller and its redirect method.
04:29
We'll also give this a name of as Google redirect. Then we'll also have a router dot get pointing to Google slash callback that will again use
04:37
our social auth controller, its callback method, and we can give it a name of Google dot callback. Now, if you're using multiple social providers and not just using Google, you can also use
04:47
a route parameter in place of the hard coded Google. So we could call this our provider that would then pass in whether or not we're using Google,
04:55
LinkedIn, discord, or whatever social provider it is that you're using there. Then anywhere that we hard code Google throughout this flow, you would instead make reference to this social provider route parameter instead.
05:04
To keep things straightforward though, I am going to hard code Google here. Okay, let's next command click into our social auth controller for our redirect.
05:12
We'll want to grab Ally out of our HTTP context.
05:16
Using Ally, we want to await Ally and then use our Google provider to redirect our user out to Google.
05:24
This will temporarily take the user outside of our application to go log in with Google as we referenced here in our flow. This is the redirect portion that initiates this whole flow to allow the user to log in
05:34
with Google itself. Then once they've logged in or canceled the process in any way, Google will send the user back to the callback URL that we've defined, which is going to be handled by our callback
05:44
method here. So for this, we're going to want our response, session, auth, and Ally. Let's grab reference to our social provider by setting that equal to Ally, and then just
05:54
using the active provider for the request. And there's three validations that we need to do to ensure that everything worked properly. First, we need to check whether or not access was denied, and we can do that using the method
06:04
access denied off of our social provider. When this happens, we can just send a flash message with a key of an error and a value
06:12
of access was denied, and then return the user back to where they initiated this request. For us, we're just going to do this with our homepage.
06:21
But for you, this is most likely going to be a login or register page. The next is checking for a state mismatch.
06:27
So we'll do searchable.stateMismatch and call that very similarly to how we're calling our access denied.
06:34
Again, we want to session flash and redirect the user if the state is indeed mismatched. When that's the case, we can say request expired, please try again.
06:43
And then there's one more, and that's just a general errors that may occur throughout the process. We can check that by calling hasError.
06:51
And if that returns back truthy in any way, then something went wrong with the process. It wasn't an access issue and it wasn't a state mismatch issue, but it was something else.
06:59
So we can try to get what that error was via social getError. And this is optionally going to be provided.
07:05
So we can provide a fallback for that if it's nullish to an unexpected error occurred. And then again, redirect the user back to wherever it was that they initiated the request.
07:15
So access is going to be denied if the user tells Google, no, I don't want to authorize this request or no, I don't want to provide authorization to this application to access
07:25
my user. That's when we're going to get an access denied error. Our state mismatch is going to happen when communication between our AdonisJS app and Google is expired.
07:34
AdonisJS is going to initiate that redirect with a CSRF token, and that token needs to match up between our AdonisJS app and Google.
07:44
Otherwise we'll get that state mismatch error. And then of course we just have the general hasError down here if anything else throughout the process runs into an issue.
07:52
So that's our validation checks to ensure everything worked correctly. With that set, we now need to determine whether or not we're registering a brand new user or logging in a preexisting user.
08:02
So we can access the Google user via await, we can reach for our social provider and call the user method.
08:08
Then we can make use of AdonisJS's firstOrCreate to get back our applications user, which we'll just call user here. So we can import our user model and call firstOrCreate.
08:18
This takes in two payloads. The first is the search payload. These are keys that we actually want to search for the user with. And then the second is the store payload, which is what's actually used to store a brand
08:28
new user. And the search and store payload are merged if we are indeed storing a brand new record as well. So in order to search, we have a couple of options.
08:38
First, we could try and search via email and then use the Google users email.
08:43
And then in terms of our save payload, we could say the full name is from our googleuser.name.
08:50
And I believe name falls back to nickname automatically inside of AdonisJS Ally. But just for safety sake, because it doesn't take much for us to do this as well, we can
08:59
go ahead and fall back to nickname if a name is not provided there too. So let's give that a save for right now so that it formats.
09:06
The reason I don't like this approach is because emails are prone to change. If the user changes what their Google email is inside of their Google account, the next
09:16
time that they come to log into our application, this email won't match what we've saved inside of our database for the user.
09:23
And then this flow will end up registering them as a brand new user rather than logging them into their pre-existing account. In addition to this, some social providers may not provide an email at all.
09:33
Google creates an email with every one of their accounts via Gmail. So we don't really have to worry about that with Google itself, but you may with other social providers.
09:41
So what I like to do instead of using email for this is to use the actual social provider user ID to perform this search for a user inside of our database.
09:50
So let's start by jumping into our create user migration and let's add in a table string and let's call this our Google ID.
09:58
And I would recommend keeping all of your social provider IDs inside of separate columns so that they don't end up colliding with one another.
10:05
We want this to be nullable with the assumption that you're also going to add an email and password authentication to your application. And then since the Google ID is separate and specific to Google, we can also go ahead and
10:15
make this unique inside of our database to ensure that none of our users collide with one another. Additionally, if a user signs up with Google inside of our application, they're not going to be setting a password. So for that, we have two options.
10:25
We can either make this nullable or we can generate out a fake password for the user whenever they register. For me, I like making this nullable as if we were to add in an email password login
10:35
flow. Password would be required in that. So there's no way for any user to mock up a null password to get access to an email.
10:43
And then from here, you can also store whatever additional information about the user you want from that social provider. So you could say store in their Google email here, and I could set that to nullable as well.
10:53
And you also have access to an access token and a refresh token as well. If you need to make requests out to Google on user's behalf.
11:00
If you don't, I would recommend not storing that information as it could potentially leak access to their account if your database is ever compromised. So we'll leave them out here today.
11:10
And if you do store them, you can take an additional security step by encrypting them inside of your database as well. Okay. So our migration here is done and we currently have it set up in a way where we would allow
11:20
email and password authentication and or Google authentication. Next let's jump into our app models in our user model and add these two new columns in.
11:29
We could do at column, declare Google ID, we've set that as a type of string, but it could also be null. And we've also set our password now to be nullable as well.
11:38
So we'll update that to null or string too. And then we have one more column, which is our Google email, a string or null. Okay. Back to our social auth controller now.
11:48
Let's replace our email search with a Google ID search. Now using the Google user's ID value.
11:56
In addition to that, we can fill out the email as the googleuser.email, and then we can fill in the Google email with that same googleuser.email value too.
12:05
These two emails will be synced whenever the user first creates their account, but we can keep them separate because if the user changes their Google email, that's not indicative
12:13
that they'll want to change their email inside of our application as well, but that's up to you completely on how you want to handle that. So that should be all that we need there is this first or create call.
12:22
It will either find a user and return that user back using the Google ID value, or it will create a user by merging that Google ID with our store payload consisting of our
12:32
full name, email, and Google email. Now let's say very briefly that you were going to store the Google access and refresh token. You can get access to that via the googleuser.token.
12:41
And inside of here is the expires at, expires in, granted scopes, the token's ID, refresh, the token itself, and the type of token.
12:49
So the access token would be googleuser.token.token. And then the Google refresh token would be that googleuser.token.refresh token that we saw there.
12:59
And I would also encourage you to go ahead and just import encryption from at AdonisJS core services encryption and encrypt those values whenever you're storing them.
13:09
You can also set this up as a model hook as well. So just encryption.encrypt. And then conversely, there is an encryption.decrypt for whenever you need to make use of those
13:18
values that would end up looking something like this. We're not storing those here in this example, so I'm just going to go ahead and omit that altogether. Okay.
13:27
Now that we have our user, all that's left is to log them in. So we can await, auth, use, web, and log that new user or existing user that we've now fetched
13:36
into our application and then return, response, redirect to our homepage or wherever you need the user to go from there. That leaves us now with our homepage.
13:46
So let's jump into there and let's just get rid of everything that's inside of the body currently. So let's scroll down to the bottom of our document here and just delete that out.
13:56
Okay. Now, this default welcome page is using the TailwindCSS CDN, so we do have access to Tailwind here.
14:02
So we can do at if, check our flash messages to see whether or not we have an error from our social authentication flow.
14:09
If we do, we can display a div with a class, text read 500, and then just display that flash message error, just like so.
14:18
Then we want to check and see whether or not we have an authenticated user. So we can do at if await auth.check, as our homepage here is not authentication guarded.
14:27
So we need to attempt to load that user in if once found else, and then just end our if down there. If auth check found a user, we can display their details via auth user, and then we can
14:37
just display the full name. Otherwise, if we did not find a user, then we want to have a form method post with an
14:43
action set going to our Google redirect route, and we're doing this as a post so that we can make use of CSRF protection there.
14:53
And we can have a button of type submit login registered with Google. One more thing to take care of here inside of our code, and that's jumping into our config and the ally config.
15:03
This is where we define that callback URL that Google should send the user back to inside of our AdonisJS application.
15:09
And this would look something like HTTP semicolon slash slash localhost 3333 slash Google slash callback as its final value.
15:19
This localhost 3333 is going to change depending on what environment we're currently in. If we're on localhost, then it would obviously be what we're seeing here.
15:27
But out on production, it's going to be whatever your domain is for your application. So a great place to plop that value is inside of our environment variable.
15:35
So we jump into our dot EMV above our app key. Let's add in an app URL, set this to HTTP semicolon slash slash, and then we can use
15:43
the host value from this environment variable file as well as the port value to build out this application URL.
15:51
So this dollar sign port is just going to inject in the port environment variable there and the dollar sign host will do the same with host.
15:58
Then we'll jump into our start EMV.ts and we can add in a validation for this as well as app URL EMV schema string.
16:08
And due to the way that we're defining this on our localhost, we don't want to add in a string format for this as URL or anything. We'll just leave it as string.
16:16
So with that said, we can jump back into our ally config, switch our strings to backticks, and in place of our localhost 3333, we can instead use our EMV, call the get method,
16:26
and reach for our app URL value to build that out. Now the URL portion will be dynamic depending on what environment we're in, so we don't need
16:33
to worry about changing out this config in any way between development and production. Then we need to finish filling out our dot EMV file.
16:41
For this, we first need to start with our database credentials. So the user for me is Postgres, that's where it is password, and then I'm going to make use of a pre-existing database I have called test.
16:51
It already has data in it. I'm just going to truncate that out whenever we run our migrations. You're going to most likely want to use node-ace-migration-run.
16:58
I'll use node-ace-migration-fresh so that it wipes this test database I have clean and then just migrates onward from there. Before we do that though, we need to finish filling out our environment variables with
17:07
our Google client ID and secret values. So we need to jump into our browser for this. Should be able to search for something like console, Google, and I think it filled out
17:16
for me there in the URL, but we'll Google it here just to make sure that we have the right one, console.cloud.google.com.
17:22
This will take you into your Google Cloud console and looks like my test from this is scheduled for shutting down, so I'll go ahead and just cancel out of that.
17:30
You'll either be dropped in whatever last project it was that you were working in inside of Google Cloud or no project at all since mine was scheduled for deletion.
17:38
We'll just jump into here, hit new project, and we'll call this ally-social-auth-example and go ahead and hit create. Once that's done, it should drop you into that newly created project, which you can
17:48
confirm by looking at the dropdown right up here. If it didn't, just click on that dropdown and switch to it by clicking on whatever that project name is.
17:56
Okay, let's click on the hamburger menu over here, go into API services, and then you should see this OAuth consent screen option. We want to jump into there first.
18:05
If you're building out an internal application only people inside of your organization are going to use, you'd use internal there.
18:11
If you're building an application similar to Adocastss where anybody and everybody can log in with Google, then you would want to select external as the user type here.
18:20
So select the applicable one for your application there and then hit create. I'm going to note that this may change in the future because they're warning me right
18:26
here that they will have a new experience soon, but the general flow here should remain the same. So we have our app name, call this Ally Social Auth Example.
18:36
They're going to require me to use an email that I have set up with whatever I'm logged in with. So I'll set that as my user support email.
18:43
You can add in an app logo, but I do believe adding an app logo here requires you to go through an actual manual verification process. So we will skip on that.
18:52
And then you have app domain options that you can set here consisting of your homepage, privacy policy, and terms of service. We're just on local host, so we're not going to have any of that here today.
19:01
And then we also have our authorized domains and then the developer email here that's required. So I'm just going to fill in the exact same one I added above.
19:08
In terms of our authorized domains, we can leave that blank for right now. We'll be able to add in our local host whenever we get to creating our token.
19:16
So we'll hit save and continue here for now, which will take us into scopes. If you have any additional scopeful information that you need to get from Google on behalf
19:23
of your user, this is where you would want to request that information. Again, that may make you go through a manual review process depending on what those scopes are. We're going to skip all of that here today.
19:33
And then we come into our test users. As we're creating our OAuth consent screen right now, it's been created inside of a test mode.
19:40
And this means that only users that we add specifically inside of this test users field are going to be able to register or sign in to our application using Google.
19:49
And this is also capped at 100. So anybody that you want to be able to test with on your local host while your OAuth consent screen is in test mode, you'll want to add user emails for here.
19:58
So again, I'm just going to stick with the same email I've been rolling with so far. Just add that in. And there we go. Hit save and continue.
20:06
And it's telling me the following email addresses are either not associated with a Google account or the account is not eligible for designated as a test user. I believe it's the latter there for me because this is also the owner of this application.
20:16
So it would make sense that that would already be a valid test case. So I think that's all that that meant there.
20:22
And it should drop us now into a summary where we should be able to just hit back to dashboard. It's at this dashboard where you see this publish app option.
20:31
This is where you go from that testing phase into an actual OAuth consent screen you can use in production. So whenever you're done with everything, you want to make sure that you publish your application
20:40
here before deploying out your application with Google login abilities. So we're now done with our OAuth consent screen.
20:46
Let's next jump into our credentials and we need to create an OAuth 2.0 client ID that we add into our AdonisJS environment variable.
20:53
So we'll click on Create Credentials right up here and create an OAuth client ID credential. We need to select our application type.
21:00
We are working with a web application, so we'll select on that one. We can give it a name. I'll stick with the same old Ally Social Auth example.
21:08
And then it's going to ask us to fill in our authorized JavaScript origins. I would recommend doing different credentials between development and production just to make it easier to recycle those tokens if you need to in the future.
21:18
But for our authorized JavaScript origins here, what we want to do is add in our HTTP, localhost, 3333 domain that our application runs on locally.
21:27
Then we also want to add in our authorized redirect URLs. Now don't get this confused with the redirect URL that we have inside of our AdonisJS application.
21:35
This is instead the callback URL, and we can verify this by jumping over to the question mark where it says users will be redirected to this path after they have authenticated with Google.
21:45
So this is our callback URL here. So we'll add in our URI, and this is going to be HTTP semicolon slash localhost 3333
21:52
or whatever port it is that you run your applications on locally slash Google slash callback. Once we have that set, go ahead and hit create.
22:00
It notes this may take up to five minutes to a few hours, hopefully, definitely the former there. And there we go. It immediately popped me into the screen where I can see now my client ID and client secret.
22:09
We want to give that client ID there a copy. Jump back into our text editor. This is our client ID, so we'll plop it in there.
22:15
Then we want to jump back into the browser, copy our secret now, and plop that in as our secret. We're now all done with Google Cloud, so we can hit OK on that, and let's temporarily
22:25
hide our browser away, and let's make sure that everything migrates A-OK inside of our database. So let's open up our terminal here.
22:31
Again, I'm going to run node-ace-migration-fresh, but you most likely just want to run node-ace-migration-run if you're working with a brand new database. And there we go.
22:41
We have now migrated our create users table. So let's boot up our application, npm run dev. Make sure that the server address that your application is running on matches the one
22:50
you've added via your OAuth credentials inside of Google. Otherwise, you're going to run into issues. Once all is set there, we can go ahead and open up our browser.
22:57
I'm going to open up a brand new private window here just to ensure that nobody is logged in via Google whenever I go through this flow.
23:05
And we'll enter into our localhost 3333 URL, and we should be welcomed by our login slash register with Google button. Let's give that a click, and we should get redirected out to Google.
23:15
So that made a POST request up to our Google redirect URL, where it then redirected the user out to Google. I'm going to go ahead and log in with one of my test users here.
23:25
Enter in that user's password, hit next, got the two-factor authentication there. So I'll go through with that. And now that we've actually authenticated and logged into our Google account, now Google's
23:35
going to ask me if I want to authorize the Ally Social Auth Example application that we've set up to access my Google account information.
23:43
So I can either hit cancel here, which will end up with an access denied error, or I can hit continue to authorize this request and move forward with the flow.
23:52
So if I hit cancel here, you'll see that we got redirected back to our application with an access was denied error. If I now log in with register with Google one more time, you'll see that I'm already
24:02
logged in, so I can select my account and we're taken right to the authorization step. This time, if I continue, we'll instead be redirected to our application where we can
24:12
now see that our user was successfully registered inside of our database and authenticated as well with AdonisJS, as we see our user's name right up there.
24:21
From there, everything authentication based works the exact same as it would with any traditional session-based authentication flow inside of AdonisJS 6.
24:28
For example, if we quickly jump back into our text editor, go into our routes, we can
24:32
add in a router.post.logout, async, take in response and auth, await auth use web.logout,
24:41
and then return response redirect back. Jump into our homepage inside of our if auth check.
24:48
So when we have an authenticated user, let's add in a form method post with an action going out. I don't think we gave this an alias, so I'll just do slash log out there at our CSRF field
24:58
and then a button type submit that says log out. If I click back into our browser, there's our log out button. If I now click it, boom, our user has now been logged out of our application.
25:08
Now since we've fully completed the flow on Google's side, whenever I hit log in register with Google again, you'll see that we're immediately redirected right back to our application
25:17
and logged in with Google, making for a very seamless flow inside of our application here. We're already logged in on Google's side and we've already authorized our application to
25:25
access our Google user's information. So everything just works seamlessly.