Cheekumz
Cheekumz

Reputation: 73

In Keystone js v6, can `withAuth` have multiple listKeys, so users can authenticate against different lists?

Out of the box, Keystone's auth is straight forward and nice and I want to continue using it, however I would like to set up different schema for the users of my front end than the standard Users schema used for the Keystone admin UI. I know I don't need to and could just use the one table however the schemas are so different I'd like to know if I can, for example, keep using the authenticatedItem { ... on User {} } with Keystone and, in my front end queries, use something like authenticatedItem { ... on DifferentUser {} }.

Thanks

Upvotes: 0

Views: 1057

Answers (1)

Molomby
Molomby

Reputation: 6559

Great question. There are basically two ways of achieving this right now with different trade offs.

Here I'm going to assume we have a User list for standard users and a separate Admin list for people who have a higher level of access. We want to set our app up such both can authenticate, get cookied and can be restricted with the standard Keystone access control.

Shared Credentials List

This approach is pretty easy to reason about but, depending on your exact requirements, might not give you the flexibility you need.

Basically, you can reframe the problem so you're not actually authenticating as a user or admin, you're authenticated as a "credential". That is, we can have a single list with username/password pairs that's shared between users and admins. Like this:

const lists = {
  User: list({
    fields: {
      name: text({ validation: { isRequired: true } }),
      credential: relationship({ ref: 'Credential.user', many: true }),
      // ... various other User fields
    },
  }),
  Admin: list({
    fields: {
      name: text({ validation: { isRequired: true } }),
      credential: relationship({ ref: 'Credential.admin', many: true }),
      // ... various other Admin fields
    },
  }),
  Credential: list({
    fields: {
      email: text({ isIndexed: 'unique', validation: { isRequired: true } }),
      password: password({ validation: { isRequired: true } }),
      admin: relationship({ ref: 'Admin.credential', many: false }),
      user: relationship({ ref: 'User.credential', many: false }),
    },
  }),
  // ... various other lists
};

Your createAuth call then refers to this list:

const { withAuth } = createAuth({
  listKey: 'Credential',
  identityField: 'email',
  secretField: 'password',
  sessionData: 'id user { id } admin { id }'
});

The sessionData option passed to createAuth is a GraphQL query fragment, so we can drill into the related user and admin items if we want. As written, we can add access control functions to check for either session.admin or session.user and apply different rules. For example if we wanted to limit create, update and delete operations on the Admin list to authenticated admins only, we could add:

  Admin: list({
    access: {
      operation: {
        create: ({ session }) => !!session.admin,
        update: ({ session }) => !!session.admin,
        delete: ({ session }) => !!session.admin,
      }
    },
    // ... fields, etc.
  }),

To make this a bit more usable, you'd probably also want to add a validateInput hook to the Credential list that forced them to be linked to either a user or an admin item. If it were me, I'd probably also lock those two relationship fields down so you couldn't update them to "move" a credential between users/admins, and maybe a afterOperation hook to delete old credentials when a new pair was created. Adding a virtual labelField would also be nice. I've created a gist with these improvements to show a more complete example.

Another way to implement this pattern is to have a User list that stores the credentials and any common fields, then have an optional, one-to-one relationship with a UserProfile list for any additional fields needed by the frontend-only users. Basically the same solution but treats one type of uses as a superset of the other.

Role Your Own

This is a bit more involved but maybe not as hard as you'd expect.

Fact is, createAuth is actually just a helper function and there's very little magic. It returns a withAuth function that modifies your Keystone schema in standard ways, and does so from "outside" the core Keystone classes – that's why it's published as a separate package.

Specifically it...

  • Sets the getAdditionalFiles and publicPages ui options to configure the sign in page in Next.js (and the initial user feature, if enabled)
  • Creates some pageMiddleware that bounces people to the sign in screen if they have no session
  • Tweaks any isAccessAllowed function provided so the initial user pages work
  • Augments your session config so the item data you specific is populated into session objects, and..
  • Extends the GraphQL schema with types and mutations needed for authentication and (if enabled) dealing with passwordReset, magicAuthLinks and initial item creation

Of all this, I think the pageMiddleware part is the only not yet documented. Regardless, I encourage you to take a look at the @keystone-6/auth source code. You should be able to copy that code into your own project, pull it apart and put it back together however you want. There's nothing stopping you from adding mutations to the GraphQL schema that authenticate against different lists, adding multiple sign in screens to the Admin UI, different type so of session handling for different types of users or doing, more or less, whatever you want.

Upvotes: 4

Related Questions