Skip to Page NavigationSkip to Page NavigationSkip to Content

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.ts
import { 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.ts
import { 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 and password 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.ts
import { 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 days
const 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.ts
import { 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.ts
import { 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 days
const 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:

Create user screen in Admin UI showing name, email, and password fields

What we have now

// auth.ts
import { 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 days
const session = statelessSessions({
maxAge: sessionMaxAge,
secret: sessionSecret,
});
export { withAuth, session }
//keystone.ts
import { 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,
},
})
);

Next lesson