Lesson 4: Auth & Sessions
Learn how to add passwords, session data and authentication to your Keystone app.
Where we left off
In the last lesson we setup a publishing workflow for blog posts and ended up with a keystone.js file that looks like:
//keystone.tsimport { list, config } from '@keystone-6/core';import { text, timestamp, select, relationship } from '@keystone-6/core/fields';const lists = {User: list({fields: {name: text({ validation: { isRequired: true } }),email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),posts: relationship({ ref: 'Post.author', many: true }),},}),Post: list({fields: {title: text(),publishedAt: timestamp(),status: select({options: [{ label: 'Published', value: 'published' },{ label: 'Draft', value: 'draft' },],defaultValue: 'draft',ui: { displayMode: 'segmented-control' },}),author: relationship({ ref: 'User.posts' }),},}),};export default config({db: {provider: 'sqlite',url: 'file:./keystone.db',},lists,});
We're now going to add auth to our app so that different types of users have access to different types of things. While Keystone has very granular permissions controls, which you can read about here, this lesson will stay focused on securing our Admin UI behind a password.
Add the Password field
Keystone's password field adheres to typical password security recommendations like hashing the password in the database, and masking the password for Admin UI input fields.
Let's add a password field to our User
list so users can authenticate with Keystone:
import { list, config } from '@keystone-6/core';import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields';const lists = {User: list({fields: {name: text({ validation: { isRequired: true } }),email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),posts: relationship({ ref: 'Post.author', many: true }),password: password({ validation: { isRequired: true } })},}),};
That's all we need to store secure passwords in our database!
Add Authentication
Install the auth
package
Authentication isn't built directly in to Keystone - it's an enhancement you can add on top. To use it in our app we need to add Keystone’s auth package:
yarn add @keystone-6/auth
Now that we have the package, let’s create a new file in the root of our project to write our auth config in:
touch auth.ts
And add the following code:
// auth.tsimport { createAuth } from '@keystone-6/auth';const { withAuth } = createAuth({listKey: 'User',identityField: 'email',sessionData: 'name',secretField: 'password',});export { withAuth };
This code says:
- The
User
list is the list that auth should be applied to email
andpassword
will be the fields used to log a user in
Add sessions
Having added an authentication method, we need to add a 'session', so that authentication can be kept between refreshes. Also in the auth file, we want to add:
// auth.tsimport { createAuth } from '@keystone-6/auth';import { statelessSessions } from '@keystone-6/core/session';let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --';let sessionMaxAge = 60 * 60 * 24 * 30; // 30 daysconst session = statelessSessions({maxAge: sessionMaxAge,secret: sessionSecret,});export { withAuth, session }
Import Auth & Sessions to Keystone config
Back over in our keystone file, we want to import our withAuth
function, and our session
object.
withAuth
will wrap our default export and modify it as a last step in setting up our config. The session is attached to the export.
Finally, we need to add an isAccessAllowed
function to our export so that only users with a valid session can see Admin UI:
//keystone.tsimport { list, config } from '@keystone-6/core';import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields';import { withAuth, session } from './auth';const lists = {};export default config(withAuth({db: {provider: 'sqlite',url: 'file:./keystone.db',},lists,session,ui: {isAccessAllowed: (context) => !!context.session?.data,},}));
Adding init first item
With our new set-up, we'll be locked out of Admin UI! What's more, if we don't have a user in our database yet, or, if a new person clones our project, they won't be able to access Admin UI. Thankfully, Keystone has a feature so that if there are no existing users, you can create one when you first launch Admin UI. This is the initFirstItem
feature in the auth
package:
// auth.tsimport { createAuth } from '@keystone-6/auth';import { statelessSessions } from '@keystone-6/core/session';const { withAuth } = createAuth({listKey: 'User',identityField: 'email',sessionData: 'name',secretField: 'password',initFirstItem: {fields: ['name', 'email', 'password'],},});let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --';let sessionMaxAge = 60 * 60 * 24 * 30; // 30 daysconst session = statelessSessions({maxAge: sessionMaxAge,secret: sessionSecret,});export { withAuth, session }
Now, if you open Admin UI, you can check out the sign in flow. If you have no users, you’ll be presented with fields to create the first user:
What we have now
// auth.tsimport { createAuth } from '@keystone-6/auth';import { statelessSessions } from '@keystone-6/core/session';const { withAuth } = createAuth({listKey: 'User',identityField: 'email',sessionData: 'name',secretField: 'password',initFirstItem: {fields: ['name', 'email', 'password'],},});let sessionSecret = '-- DEV COOKIE SECRET; CHANGE ME --';let sessionMaxAge = 60 * 60 * 24 * 30; // 30 daysconst session = statelessSessions({maxAge: sessionMaxAge,secret: sessionSecret,});export { withAuth, session }
//keystone.tsimport { list, config } from '@keystone-6/core';import { password, text, timestamp, select, relationship } from '@keystone-6/core/fields';import { withAuth, session } from './auth';const lists = {User: list({fields: {name: text({ validation: { isRequired: true } }),email: text({ validation: { isRequired: true }, isIndexed: 'unique' }),posts: relationship({ ref: 'Post.author', many: true }),password: password({ validation: { isRequired: true } })},}),Post: list({fields: {title: text(),publishedAt: timestamp(),status: select({options: [{ label: 'Published', value: 'published' },{ label: 'Draft', value: 'draft' },],defaultValue: 'draft',ui: { displayMode: 'segmented-control' },}),author: relationship({ ref: 'User.posts' }),},}),};export default config(withAuth({db: {provider: 'sqlite',url: 'file:./keystone.db',},lists,session,ui: {isAccessAllowed: (context) => !!context.session?.data,},}));