Playing Next Lesson In
seconds

Transcript

  1. - When you can't use cookies, that's when access tokens become a great option for authentication. To get ourselves started, let's go ahead and make a directory.

  2. So make dir, as we're gonna have two separate projects, our backend and then a separate frontend. We'll call this folder access token authentication, and let's go ahead and CD into that folder.

  3. Once we're in here, we can go ahead and npm init, AdonisJS at latest, our new AdonisJS project that we'll call our backend. We'll be using this project as an API,

  4. so we'll select the API starter kit, and we'll be using access tokens for authentication here. Again, if you do have cookies available, session authentication is gonna be much more straightforward,

  5. but we'll move forward with access token here today. I use Postgres for my database, but feel free to select whichever one's applicable to you, and let's have a go ahead and install our dependencies. While it's doing that, I'm gonna create a brand new tab,

  6. and within our same access token authentication folder, let's create a brand new frontend app. For this, I'll be using Vue. So we'll npm create Vue at latest, and I'll call this the frontend. Go ahead and run that.

  7. I'm gonna pass on TypeScript for now, as well as JSX. We'll go ahead and add Vue router and Pina. I'll skip Vitus. We won't be doing any testing here today. Skip ESLint and the dev tools for right now.

  8. Feel free to select any of those applicable to your use case though. Go ahead and cd into our frontend here, and run npm install. We now have our frontend set up, so I'm gonna clear out of that and boot it up. So npm run dev,

  9. and let's jump back over to our AdonisJS tab. Looks like our project created successfully here. So let's go ahead and cd into our backend. We're gonna need to migrate before we start this up. So let's go ahead and get both of these projects open

  10. within our text editor. This blue tinted one here, I'm gonna use as our frontend. So hit File, Open Folder, dime in the code, jump into our access token authentication, and I'll select frontend for this blue one.

  11. Cool, so here we have our Vue app. Now I'll jump over to our normal looking Visual Studio code that we usually use, File, Open Folder, and for this one, we'll open up our backend, which will be our AdonisJS project.

  12. As you can see, we have missing environment variables to take care of, so we'll go ahead and jump into our .env file and take care of that. All we need to worry about for right now is our db_password and db_database.

  13. I have a test database that I'd like to reuse, but do apply your applicable database name there or create a new one if you need to. Then we'll enter in our Postgres user's password. Mine just happens to be password. Okay, with that set up,

  14. the API and access token starter kit that we used for AdonisJS has us set up with a create_users table that will define our users with a full name, email, password, as well as an ID and created_at,

  15. as well as an access_tokens table that has an ID, a tokenable ID, that will relate back to our users, type name, hash, abilities, created_at, updated_at, last_used, and expires. Abilities here are optional

  16. and you can specify them whenever you're creating your token. We're gonna be ignoring them here today as we're just covering the basics of Auth, but do note that they exist and the documentation covers the difference

  17. between these abilities and bouncer abilities. We can jump back into our terminal and within our backend, we can run node as migration run. Now, since I am reusing a database

  18. that already has stuff applied within it, I'm gonna go ahead and have AdonisJS remove everything that I have preexisting inside of this database and then run my migrations by using migration fresh. And as you can see, that dropped any tables

  19. that I had within this database and just migrated forward. Again, if you're working with a brand new database, you just need to run migration run. Cool, so next let's go ahead and create ourselves an Auth controller. So node as make controller,

  20. call this Auth, keep it singular with hyphen S, and we'll add a register, login, logout, and me methods within this controller. And then while we're here, let's go ahead and make a validator as well.

  21. So node as make validator, and we'll just call this Auth. All right, now we're ready to dive into our app and let's go ahead and do our validator first. So we'll dive into here. We'll want one validator for registering and one for logging in.

  22. So we'll export const register validator equals vine compile with a vine object inside of that, which will house our properties.

  23. So we're gonna have an email of type vine string, and we can assert that as an email. And then we can do a little bit of normalization by switching it to lowercase

  24. and then trimming any white spaces from it. In addition to that, we also wanna verify that it is unique within our database. So for this, we can do an async callback that accepts in our database query builder,

  25. as well as the actual value trying to be validated. And we can build out a simple query. So we'll do const match equals await DB,

  26. specify that we wanna use our users table, select just the ID, and hone in where the email equals the value that we're trying to validate.

  27. Then with that, we can try to match the first. If we found the match, then we want to switch it to false because that would mean that the email is not unique. And if we didn't find a match,

  28. then we switch it to true, meaning that the value is unique and passes our validation. Then we also have our password, which will be of type vine string.

  29. And then as a minimal security check, we'll go ahead and verify that that will have a min length of eight before they can move onward. All right, let's save that so it formats.

  30. And let's now do our export const login validator equals vine compile. Again, this will have a vine object within it

  31. with an email property of type vine string. We can verify that it's an email, normalize it to the same as our registration by switching it to lowercase

  32. and trimming any white space from it. Having that be the same within our login as it is within our register will ensure that the lookup matches the values correctly.

  33. And then we also want to add in our password as type vine string. And we can omit the min length eight because the value is already inside of our database. And we just need to verify

  34. that the password actually matches what the user already has inside of the database. And now we're ready to dive into our controllers off controller. So we'll start with our registration and we're gonna need our request and we'll grab our const data

  35. from await request.validate using our register validator. And then we'll need to create a user with that data. So const user equals await user,

  36. hit tab.auto import that and pass in our validated data. Once we have that user, then we just need to create a new access token for them and return that token back as a response for this request.

  37. So we can just return user.accesstokens.create and pass in the user. If you're using abilities here, you can specify those as the second argument.

  38. By default, this will look something like this to allow all abilities. And then we have additional options as the third argument. By default, these access tokens will not expire. So if you do want them to expire,

  39. you can specify an expires in. If you want it to be long live, but still have an expiration, you might set this to something like five years, which I believe is the default duration for the remember me tokens for session authentication,

  40. or you could set it to something like 30 days as well, whatever your heart desires there. For us, we're just gonna roll with the defaults. So we'll take both of those off and leave it at just create. Now, if you're wondering

  41. where this access tokens comes from, if we take a look at our user model, we do have this with Authfinder, the exact same that we have within session authentication that we can use to check whether or not the user has provided a correct email and password

  42. that'll come into play whenever we cover login. But if we scroll down a little bit further here, we're gonna see a static access tokens property that comes from dbAccessTokensProvider for the model, and it provides in our user.

  43. This access tokens provider is what provides the actual access token property onto our user, and it's a static property as well. So if we take a look at what we have available on that,

  44. so user.accessTokens here, we can get all of the user's tokens. We can create a token, delete a token, find the specific token, and verify the validity of a token as well. So via that API,

  45. working with tokens within AdonisJS is rather straightforward. Cool, let's scroll down a little bit and take care of our login next. This is actually gonna look relatively similar. So we'll just have a request,

  46. and then we just need to grab our email and password from our await request validate using our login validator. Now, rather than creating user, we just need to verify

  47. whether or not the email password's correct. So the const user equals, and that's where that AuthFinder mixin comes into play that we just saw on our user model. We can await user dot, and that with AuthFinder mixin

  48. provides a verify credentials method on our user as a static method. We pass in the UID, which is our email, the user's unique identifier, and then their password.

  49. That AuthFinder mixin's also going to hash the user's password during the creation step. It adds a before save hook on there to check for the password change. And if it finds a mutated password,

  50. it will automatically hash it and secure it inside of our database. So that's taken care of automatically as well. And now that we have our user, just like we did with our register, we just need to create an access token to log this user in.

  51. So we'll return user dot access tokens, create. You have the same options available here. We're just gonna stick with passing our user in for now. And there we go, there's our login. We'll scroll down a little bit and let's take care of our logout.

  52. So we have our Auth, await, user, access tokens. And here we'll want to use the delete to delete an access token, essentially logging them out. Pass in the authenticated user,

  53. which we can get via const user equals AuthUser. And we'll do an exclamation point there to assert that a user is actually authenticated. And we'll ensure that by adding a middleware whenever we define this route.

  54. And then we'll provide our user in there. And then we can get the user's current access token via user dot current access token. And then we just need to provide that access token's identifier as such.

  55. And that will delete out that user's access token via its identifier. In turn, essentially logging the user out as that access token will no longer exist inside of our database

  56. and can no longer be used to match against an authenticated user. Then we just need to return something here. We'll just do message success. And then lastly, we have our me. We're gonna want this method to work

  57. whether or not a user's authenticated or not. So we'll just do a silent Auth check. This will check the default guard for an authenticated user. And if it's found, then it will populate that Auth user.

  58. Within our logout, we're gonna use a middleware to make that happen automatically. But within our me, we'll manually make that check because we will not have a middleware for this one. And then we'll return user

  59. and use that newly populated Auth dot user. If the user's logged in, this will be populated with that user's details. And if they're not, our user will not be populated. Cool, so let's go define these routes. Scroll down.

  60. We'll have a router dot post slash register using our Auth controller. Register method is Auth register. We'll have a router dot post slash login.

  61. Again, using our Auth controller, the login method is Auth login. And then we'll have a router dot delete, or you can do a post here as well,

  62. slash logout, Auth controller, logout is Auth logout. Now, the reason I'm using delete here is because we are actually deleting a token out of our database whenever we do logout.

  63. So in turn, it is a literal deletion, but that is completely up to you. And then we'll do router dot get slash me, user Auth controller. The me method is Auth dot me.

  64. Now, in terms of our middleware, all that we want to do is apply a middleware to log out. Use middleware dot and verify that a user is authenticated in order for them to successfully be able to log out.

  65. And this middleware is also what populates that Auth user for us. Remember, we're manually checking for an Auth user in our me using Auth dot check. This middleware will do something similar,

  66. but it will also throw an error if a user is not actually authenticated. Cool, so we should now have our backend, our AdonisJS side all set up. So let's dive back into our terminal and boot this up.

  67. So npm run dev. We can also hide this text editor back away and jump into our frontend text editor now. The first thing that we're gonna wanna do is create a new page for registration.

  68. So we'll create a new file within here. You can put these inside of a new folder called Auth. Now, we're just gonna keep everything flat for this lesson. So we'll do register and we'll stick with the view naming convention that they have going on so far.

  69. We'll have a template with a form at submit prevent, and we'll call this method submit whenever we get to it. We'll have a label, span, our email field,

  70. and then we'll have an input type email, vmodel form email, and then let's give this a copy and a paste, switch this from email to password. Type will be password there as well.

  71. And our vmodel will now be form.password. Lastly, to end out our form, we're gonna have a button type submit with text of register. Cool, now we need to create those.

  72. So we'll do script setup. Let's go ahead and import ref from view, and we'll import use router from view router. Let's go ahead and grab our router equals use router,

  73. and let's const form equals ref, and stop our form with an email and password property. Then we need our async function called submit to do register user.

  74. And then once that user is registered, we'll go ahead and push the user to the homepage. Give that a save. Let's give all of this here a copy and create a new view for our login.

  75. So login view.view, go ahead and paste those contents in, switch our register text to login, scroll on up, and our to do is the only thing here

  76. that changes instead of registering user. We just want to log in that user. And then let's go define the routes for them. So we'll jump into our router, index, scroll down a little bit. We'll leave everything as is that currently exists.

  77. Define a new path for slash register, apply the name of register, and specify our component there too. And then we'll do this exact same thing for login, just like so. Now there's many different approaches that you could take

  78. whenever it comes to actually authenticating your user on the front end. For us here today, I think creating a singular store that houses all of our authentication logic probably gonna be the most straightforward,

  79. easy to reference for us. So let's go and create a new store, new file. We'll call this auth.js. Go ahead and create that. We'll import ref as well as unref from view,

  80. and we'll import define store from Panea. Export const use auth store equals define store auth, and go ahead and create that.

  81. Inside of here, we'll have a const user is ref, and we'll default this to null. And then we'll have a const token, which will be our access token, equals ref.

  82. And we'll attempt to grab the default value for this from our local storage. Get item, and we'll call that token. Depending on what environment it is that you're working with,

  83. you'll need to determine how you wanna store your token. Since we're rolling with the assumption that we don't have cookies available, which if we did, we would just use session authentication, then local storage is an alternative option

  84. for us here on the front end to store our token here long-term. And it also makes our token available across tabs if the user tries to open up a new tab with our application open.

  85. Cool, let's go and create a helper function now. So async function, we'll call this API. We'll just kind of wrap fetch here a little bit. So I have a method URL and a payload

  86. defaulted to an empty object. Const response equals await fetch HTTP slash slash, and point this to our AdonisJS server at 3333,

  87. and then whatever URL we specify. Then for the config for our request, we wanna pass in our method, specify our headers, that the content type will be application JSON,

  88. and then we'll do authorization, provide an error, and then you could unwrap the token there as Codium is suggesting, or we could just reach for token.value

  89. to get the actual value of the token there too. And it's this authorization header with the bearer and then the actual token value that's in charge of authenticating our user for each request.

  90. Whenever we send a request out to our AdonisJS server, AdonisJS will then check for this bearer token, check it against our access tokens for the user, find the user that way, and that is essentially how

  91. this access token authentication works. So if we don't provide this authorization header with our bearer token, you're never gonna be authenticated. But if you do provide it, then it will attempt to find the token for a user

  92. and then use that user to authenticate the request. Then we'll have our body. Since we can't specify bodies on get requests, we'll wanna do a ternary check here. So method does not equal get,

  93. then we can go ahead and JSON stringify our payload. Otherwise we'll just set that to null. And then lastly, we'll return response JSON. You'll wanna take some extra consideration here for error handling.

  94. We're not gonna worry about that in particular here today, because there's a wide array of different front ends that you may be using. And our goal here today is just to cover the basics of authentication. Cool. So let's go ahead and create a function

  95. to actually authenticate a user. We'll take in the result of our API request. And then we wanna set the token.value that we have as our ref at the top of the store

  96. equal to the result.token. This result is what the result will actually be during our register and login responses. And then we'll go ahead and set this to our local storage,

  97. set item token as token.value there as well. Next up, we can go ahead and take care of our login and register method. So async function, login, we'll accept in the payload from the form,

  98. const result equals, await, we'll use our API helper, do a post request to slash login and provide in our payload.

  99. And then we just want to authenticate the user with that result. Next up, we'll do our register. It's gonna look relatively similar. We'll have a const result

  100. from a post request out to slash register with that payload. And then we just want to authenticate the user there as well. Logout and me are gonna be a little bit different. So async function logout,

  101. we don't need to accept anything in for this, but we do need to delete our token. So we'll do await API, send out a delete request or post request if that's how you defined it to slash logout.

  102. And then once that succeeded, we'll set our actual token store value to null, our user value to null, and then we'll go ahead and delete that local storage item. So remove item.

  103. Lastly, we have our me. So we'll do async function me. We'll do a const result equals await API, a get request to slash me. Once we get that result,

  104. we'll want to set that as our user's value. And then we can also return back that value there as well. Lastly, to make all of these accessible, we can return user, login, register, logout, and me.

  105. We'll keep our helpers and our actual token just as internal properties and methods for our store here. Cool, so now we're ready to start breaking these up. So let's jump into our register view. We have our to-do right here.

  106. What we're gonna want to do is grab our auth store. So we can do this right above our router. So const auth equals use auth store. And then in place of our to-do here,

  107. we can now do await auth, call our register method, and provide our form in. Oh, we forgot to make use of unref. Let's go jump into our auth store again and do that.

  108. So we'll scroll up to our API helper, and we're gonna be passing the ref of our payload in. You could also do form.value, but I like how just passing the form in looks.

  109. So we can wrap our payload here in unref to remove the ref reference and just get access to the underlying payload data. Okay, let's jump back into our register view. So that should be good now. The alternative that I was talking about,

  110. if you don't want to use unref, is just to do form.value there. Okay, now we just need to do the exact same thing in our login. So import our auth store. So const auth equals use auth store.

  111. And then for our to-do, await auth login and provide in our form. Since we have everything pushing back to our home after we log in a register, we'll make our home kind of the designator

  112. of whether or not we're logged in. So we'll import use router from uRouter. Const auth equals use auth store. So now we can do auth.user,

  113. just like we are within our AdonisJS server to get access to our auth user. Const router equals use router. We can go ahead and try to populate the authenticated user by doing auth.me.

  114. I'm gonna leave that without an await just so that we don't have to worry about suspense. And we'll just add an if to check whether or not it's populated inside of the template. And then we'll also want to add in a logout function.

  115. So we'll do async function, logout, await, auth, and then call our logout method. And then we can do router.push and push the user to the login page. For our template, we'll replace this welcome component

  116. with a div bif auth.user, div belse, no user found. Inside of our auth user bif, we can go ahead and do, say, a span

  117. that says something like welcome auth user email. And then underneath that span, we're gonna want a form at submit prevent that calls our logout method

  118. that allows this authenticated user to logout. And then we can just do a button that type submit with logout in there. Cool, so if we give that a save, we should already have the server running. Go ahead and jump into your browser. Let's go to localhost.

  119. Oh, forgot to note what port this is running on here. So we'll jump back over to our view, 5173, so colon 5173. There we go. And okay, it looks like we do have something for our auth user,

  120. but we shouldn't because we haven't logged in yet. So let's jump back into our front-end text editor. And I bet this is going wrong within our auth store. We scroll down to our me method.

  121. We're just plopping results directly into our user where there should be result.user. There we go. So with that saved, actually while we're also in here, let's jump into our app.view where we have our router links.

  122. And let's add an additional router link to our login and our registered page there as well. Jump back into our browser, give this a refresh. And there we go. So now we see no user found.

  123. We don't have a user registered yet. So we'll need to register a user first before we can try logging in. We'll do [email protected] and give them some password there. Hit register. And there we go.

  124. So now we're redirected back to our homepage where we get our [email protected] welcome message. If we refresh, that user is still authenticated. And if we give this a copy, since we're storing this inside of local storage

  125. and open this up inside of a different browser tab, we are still authenticated in both of these tabs now. Awesome. We can go ahead and log out, which directs us back to our login page where we can do [email protected],

  126. password and login. And that works too. Furthermore, if we dive into our inspect, take a look at our console for the network requests. I have XHR enabled down there for that. Give this page a refresh.

  127. We can see our me goes out. If we take a look at that, specifically the headers, scroll down a little bit, we'll see our request headers where we have our authorization

  128. and our bearer and our token value being sent up. Again, that's what AdonisJS is using to match against a user and consider them authenticated,

  129. which in turn allows AdonisJS to find that user and return its details back in our response. It's serializing the password as null. So that's being omitted altogether

  130. from the response as well for security purposes. And there you go. There's the gist of access token authentication with AdonisJS. And in this case, we used Vue for our front end.

AdonisJS 6 Access Token Authentication in 20 Minutes

@tomgobich
Published by
@tomgobich
In This Lesson

We'll cover how to implement access token authentication, using opaque tokens, in AdonisJS 6. We'll also take a look at what this would look like on the frontend via a Vue 3 app using Pinia

⏰ Chapters
00:00 - Creating Our AdonisJS 6 Project
00:50 - Creating Our Vue 3 Project
01:20 - Opening Our Projects
01:50 - Setting Up Our AdonisJS 6 Project
03:00 - Creating Our Auth Controller & Validator
03:17 - Defining Our Register & Login Validators
04:53 - Registering A User
05:55 - User Access Tokens Provider
06:40 - Logging In A User
07:40 - Logging Out A User
08:28 - Getting An Authenticated Users Details
08:56 - Defining Our Auth Routes
10:13 - Creating A Register Page & Form
11:17 - Creating A Login Page & Form
11:34 - Defining Register & Login Page Routes
11:50 - Creating An Auth Pinia Store
12:55 - Creating An API Helper Function
14:25 - Authenticating A User In Our Pinia Store
14:49 - Auth Store Login & Register Methods
15:18 - Auth Store Logout Method
15:40 - Auth Store Get User Details
16:06 - Register & Login A User On Form Submit
17:02 - Auth User Details & Logout Form
18:10 - Testing Our Authentication Flow

Join the Discussion 11 comments

Create a free account to join in on the discussion
  1. @guy-aloni

    Thank you very much for the clear and detailed explanation!
    I wonder whether should be a good idea to add token expiration protection in front-end level, or rely on 409 server response in order to delete the token.

    1
    1. Responding to guy-aloni
      @tomgobich

      The expiry time should be included with the initial token, so you could store it as well on your client and check it every once in a while to see if time is running up. I would rely on the server though for actually discerning whether it's expired or not.

      1
  2. @shahriar

    Awesome Tutorial! Thank you.
    Just one thing: I get this error:
    ```
    Property 'currentAccessToken' does not exist on type 'never'
    ```

    1
    1. Responding to shahriar
      @tomgobich

      Thanks Shahriar!

      I would say to try restarting your text editor, sometimes it fails to pick up type changes. Apart from that, make sure your auth is configured correctly and double-check to make sure accessTokens is on your user model:

      static accessTokens = DbAccessTokensProvider.forModel(User)
      Copied!

      If you're still having issues, please share a link to the repo or a repoduction and I can see if anything stands out!

      1
  3. @guy-aloni
    1
    1. Responding to guy-aloni
      @tomgobich

      Social Auth with AdonisJS forms the connection between AdonisJS and the 3rd party service and gives you the tools to discern who a user is. You can then use that info to create a user inside your application. From there, auth is done normally inside AdonisJS.

      0
  4. Hi. Thank you for the detailed explanation.

    I have been having trouble returning a value for the token after login. I keep getting the error message "Cannot save access token. The result '[0]' of insert query is unexpected" even though the token gets saved to the database. I'm not sure what I could be doing wrong.

    You can check out the login method here:

    https://github.com/ansman58/properties-backend/blob/main/app/controllers/auth.ts

    1
    1. Responding to nnakwe-anslem
      @tomgobich

      Hi Nnakwe!

      Thank you for providing a repo! My best guess, after an initial look, is that you're probably running into an error at line 176 here:
      https://github.com/adonisjs/auth/blob/develop/modules/access_tokens_guard/token_providers/db.ts#L176

      I don't have MySQL installed currently to be able to test it out though. When I get some free time I can give it a go. You may want to try debugging result at line 175 to see exactly what you're getting back. Line 175 is where it's inserting your token and returning back the inserted id. Since you said it's inserting okay, my best guess is the returning('id') isn't returning as expected for MySQL or your specific MySQL version.

      Could potentially be

      • Something specific to MySQL/MySQL2

      • The specific MySQL version you have installed on your machine

      • A bug in KnexJS

      • A bug in AdonisJS at line 176.

      1
      1. Responding to tomgobich

        Thanks for pointing me to the source code. I have taken a look and the problem seems to be with the `returning('id')`, as you correctly pointed out. Is there any configuration I need to apply to MySQL in my codebase to enable the returning clause? I don't think it is fully supported in MySQL.

        EDIT: Switched to postgres and it now works fine. It's A bug in AdonisJS at line 176 when using MySQL.

        1
        1. Responding to nnakwe-anslem
          @tomgobich

          Looks like there's a previously reported issue with the same, I didn't look though the closed issues last time I checked so I missed it:
          https://github.com/adonisjs/auth/issues/241

          Sounds like it could potentially be related to how the UUID is defined as the issue creator reported switching that resolved his issue.

          If you'd prefer using MySQL, it may be worth looking into and, if it is related, potentially providing your reproduction repository within that issue.

          0
  5. How to implement access token authentication with multiple models in Adonis 6? I've searched everywhere, and I can't make it work.

    0