Reputation: 771
I would like to restrict my GraphQL API with User Authentication and Authorization. All Keystone.JS documentation is talking about AdminUI authentication, which I'm not interested in at the moment.
Facts:
Other than that you can suggest any possible way to achieve this.
My thoughts were: I could have Firebase Authentication (which can use Google Sign-in, Apple Sign-in etc.) be done on the client-side (frontend) which would then upon successful authentication somehow connect this to my API and register user (?). Firebase client SDK would also fetch tokens which I could validate on the server-side (?)
What is troubling is that I can't figure out how to do this in a GraphQL environment, and much less in a Keystone-wrapped GraphQL environment.
How does anyone do basic social authentication for their API made in Keystone?
Upvotes: 4
Views: 2828
Reputation: 1138
Keystone authentication is independent of the Admin-UI. If you are not restricting your list with proper access control the authentication is useless. Default access is that it is open to all.
you can set default authentication at keystone level which is merged with the access control at list level.
Admin UI only supports password authentication, meaning you can not go to /admin/signin
page and authenticate there using other authentication mechanism. The Admin Ui is using cookie authentication. cookies are also set when you login using any other login method outside of admin-ui. This means that you can use any means of authentication outside of admin-ui and come back to admin ui and you will find yourself signed in.
Social authentication is done using passportjs and auth-passport
package. there is documentation to make this work. Single Step Account Creation example is when you create user from social auth automatically without needing extra information (default is name and email). Multi Step Account Creation is when you want to capture more information like preferred username, have them accept the EULA or prompt for birthdate or gender etc.
I dont believe Keystone does pure JWT, all they do is set keystone object id in the cookie or the token is a signed version of item id (user item id) which can be decrypted only by the internal session manager using cookie secret.
this is the flow of authentication after you create a custom mutation in keystone graphql.
client -> authenticate with Firebase -> get token -> send token to server -> server verifies the token with firebase using admin sdk -> authenticate existing user by finding the firebase id -> or create (single step) a user or reject auth call (multi step) and let client send more data like age, gender etc. and then create the user -> send token
here is the example of phone auth I did, you can also use passport based firebase package and implement your own solution.
keystone.extendGraphQLSchema({
mutations: [
{
schema: 'authenticateWithFirebase(token: String!): authenticateUserOutput',
resolver: async (obj, { token: fireToken }, context) => {
const now = Date.now();
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors || !data.firebaseUser || !data.firebaseUser.length) {
console.error(errors, `Unable to find user-authenticate`);
throw errors || new Error('unknown_user');
}
const item = data.firebaseUser[0];
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
{
schema: 'signupWithFirebase(token: String!, name: String!, email: String): authenticateUserOutput',
resolver: async (obj, { token: fireToken, name, email }, context) => {
const firebaseToken = await firebase.auth().verifyIdToken(fireToken);
const { uid, phone_number: phone } = firebaseToken;
const { errors, data } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
query findUserFromId($phone: String!, $uid: String!) {
firebaseUser: allUsers(where: { phone: $phone, firebaseId:$uid }) {
id
name
phone
firebaseId
}
}`,
variables: { phone, uid },
});
if (errors) {
throw errors;
}
if (data.firebaseUser && data.firebaseUser.length) {
throw new Error('User already exist');
}
const { errors: signupErrors, data: signupData } = await context.executeGraphQL({
context: context.createContext({ skipAccessControl: true }),
query: `
mutation createUser($data: UserCreateInput){
user: createUser(data: $data) {
id
name
firebaseId
email
phone
}
}`,
variables: { data: { name, phone: phone, firebaseId: uid, email, wallet: { create: { walletId: generateWalletId() } }, cart: { create: { lineItems: { disconnectAll: true } } } } },
});
if (signupErrors || !signupData.user) {
throw signupErrors ? signupErrors.message : 'error creating user';
}
const item = signupData.user;
const token = await context.startAuthedSession({ item, list: { key: 'User' } });
return { item, token };
},
},
],
})
Upvotes: 3