Reputation: 73
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
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.
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.
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...
getAdditionalFiles
and publicPages
ui
options to configure the sign in page in Next.js (and the initial user feature, if enabled)pageMiddleware
that bounces people to the sign in screen if they have no sessionisAccessAllowed
function provided so the initial user pages worksession
config so the item data you specific is populated into session
objects, and..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