I have created a site where an user can login and do things with a GraphQL table. That works fine. But now there is a need to have a public read access to that table.
schema.grapql:
type Garden #model #auth(rules: [{allow: public, operations: [read]}] {
id: ID!
name: String
}
I am aware that allow: public means only Authorized users can access this. So public is not true public.
With the following call (Javascript):
await API.graphql({
query: listGardens,
authMode: 'API_KEY',
authToken: 'token_from_amazon_aplify_backend'
})
But this call only returns _deleted items. What am I missing here?
GraphQL is installed and working.
Auth is installed and working with login with users.
Related
We are currently working on an application using AWS Amplify. We have 10/15 Lambda functions defined through our Amplify project along with using several other plugins. The project itself is an Angular Application.
Right now, we are working on interfacing some 3rd party services into our Amplify project that utilizes publishing mutations for real time updates using #aws_subscribe. We can trigger these just fine based on the solution using a HTTP request; however, we are wondering if it is possible to reuse the mutation operations already generated within the API.service.ts file to provide consistent typing for both "clients". This will help maintain type control across both "clients" and catch errors earlier. It also prevents us from re-writing mutations/queries down the road.
Our current solution is working but seems inefficient.
Query Statement
const PublishReportingLocationWorkflowProgressUpdateMutation = `mutation PublishReportingLocationWorkflowProgressUpdate( $id: ID!, $status: WorkflowProgressUpdateStatus!, $reportingLocation_id: String! ) { publishReportingLocationWorkflowProgressUpdate(result: {id: $id, status: $status, reportingLocation_id: $reportingLocation_id}) { id status reportingLocation_id } }`;
HTTP Request Body
{
query: PublishReportingLocationWorkflowProgressUpdateMutation,
operationName: 'PublishReportingLocationWorkflowProgressUpdate',
variables: {
id: event.Payload.executionID,
status: 'COMPLETE',
reportingLocation_id: event.Payload.reportingLocation.id }
HTTP Call
let response = await axios({
method: 'POST',
url: process.env.API_URL,
data: mutation,
headers: {
'Content-Type': 'application/json',
'x-api-key': process.env.API_KEY,
}
});
I have an Appsync Schema which defines a User type which itself contains a List of Notes:
type User {
id: ID!
name: String!
notes: [Note]
}
type Note {
id: ID!
text: String!
}
In the request mapping for the User, I am able to access the headers from the request, to access the Bearer token:
{
"version": "2018-05-29",
"method": "GET",
"resourcePath": $util.toJson("/prod/user/$ctx.args.userId"),
"params":{
"headers":{
"Content-Type": "application/json",
"Authorization": "Bearer $ctx.request.headers.Authorization",
}
}
}
This works well and the header is passed to the data source.
The resolver for the User.notes field is a Lambda resolver which itself iteratively calls a similar API GET /prod/user/{userId}/notes/{noteId}. I am able to access the Parent resolvers response data using $ctx.source.... However, I also need to be able to pass through the Authorization header (e.g. $ctx.request.headers.Authorization).
I have tried this in the User response mapping, but it does not work:
#set($results = $ctx.result.body)
#set($headers = {
"Authorization": "Bearer $ctx.request.headers.Authorization"
})
#set($results.headers = $headers)
$results
My question is, how can I pass the request headers from a Parent resolver to a Child resolver?
Perhaps another way to ask the question would be, given I have a Bearer token on the Graphql request header, how can I ensure it is always passed to any resolvers which are called?
As #Myz mention in their comment, the solution was to use a Pipeline Resolver.
In the end, I pushed the header into the $ctx.stash object (which is available to all functions within a Pipeline Resolver) - I did this using:
$util.qr($ctx.stash.put("bearerToken", "Bearer $ctx.request.headers.Authorization"))
I was then able to access this value in each of the Functions (i.e. the child resolvers) using: "$ctx.stash.bearerToken"
Add in Note user_id
type Note {
id: ID!
user_id: ID!
text: String!
}
and resolve to notes: [Note] by $ctx.source.id
In postgres this appier
{
"version": "2018-05-29",
"statements": [
"select * from notes where user_id = '$ctx.source.id'"
]
}
where $ctx.source.id link to User -> id
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:
I want to have some social logins (no basic email/password)
I want to use JWT Bearer Tokens
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?
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 Authentication
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:
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.
JWT
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.
Using Firebase to authenticate user
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 };
},
},
],
})
I'm trying to implement GraphQL in my project and I would like to use passport.authenticate('local') in my login Mutation
Code adaptation of what I want:
const typeDefs = gql`
type Mutation {
login(userInfo: UserInfo!): User
}
`
const resolvers = {
Mutation: {
login: (parent, args) => {
passport.authenticate('local')
return req.user
}
}
Questions:
Was passport designed mostly for REST/Express?
Can I manipulate passport.authenticate method (pass username and password to it)?
Is this even a common practice or I should stick to some JWT library?
Passport.js is a "Express-compatible authentication middleware". authenticate returns an Express middleware function -- it's meant to prevent unauthorized access to particular Express routes. It's not really suitable for use inside a resolver. If you pass your req object to your resolver through the context, you can call req.login to manually login a user, but you have to verify the credentials and create the user object yourself before passing it to the function. Similarly, you can call req.logout to manually log out a user. See here for the docs.
If you want to use Passport.js, the best thing to do is to create an Express app with an authorization route and a callback route for each identify provider you're using (see this for an example). Then integrate the Express app with your GraphQL service using apollo-server-express. Your client app will use the authorization route to initialize the authentication flow and the callback endpoint will redirect back to your client app. You can then add req.user to your context and check for it inside resolvers, directives, GraphQL middleware, etc.
However, if you are only using local strategy, you might consider dropping Passport altogether and just handling things yourself.
It took me a while to wrap my head around the combination of GraphQL and Passport. Especially when you want to use the local strategy together with a login mutation makes life complicated. That's why I created a small npm package called graphql-passport.
This is how the setup of the server looks like.
import express from 'express';
import session from 'express-session';
import { ApolloServer } from 'apollo-server-express';
import passport from 'passport';
import { GraphQLLocalStrategy, buildContext } from 'graphql-passport';
passport.use(
new GraphQLLocalStrategy((email, password, done) => {
// Adjust this callback to your needs
const users = User.getUsers();
const matchingUser = users.find(user => email === user.email && password === user.password);
const error = matchingUser ? null : new Error('no matching user');
done(error, matchingUser);
}),
);
const app = express();
app.use(session(options)); // optional
app.use(passport.initialize());
app.use(passport.session()); // if session is used
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req, res }) => buildContext({ req, res, User }),
});
server.applyMiddleware({ app, cors: false });
app.listen({ port: PORT }, () => {
console.log(`🚀 Server ready at http://localhost:${PORT}${server.graphqlPath}`);
});
Now you will have access to passport specific functions and user via the GraphQL context. This is how you can write your resolvers:
const resolvers = {
Query: {
currentUser: (parent, args, context) => context.getUser(),
},
Mutation: {
login: async (parent, { email, password }, context) => {
// instead of email you can pass username as well
const { user } = await context.authenticate('graphql-local', { email, password });
// only required if express-session is used
context.login(user);
return { user }
},
},
};
The combination of GraphQL and Passport.js makes sense. Especially if you want to add more authentication providers like Facebook, Google and so on. You can find more detailed information in this blog post if needed.
You should definitely use passport unless your goal is to learn about authentication in depth.
I found the most straightforward way to integrate passport with GraphQL is to:
use a JWT strategy
keep REST endpoints to authenticate and retrieve tokens
send the token to the GraphQL endpoint and validate it on the backend
Why?
If you're using a client-side app, token-based auth is the best practice anyways.
Implementing REST JWT with passport is straightforward. You could try to build this in GraphQL as described by #jkettmann but it's way more complicated and less supported. I don't see the overwhelming benefit to do so.
Implementing JWT in GraphQL is straightforward. See e.g. for express or NestJS
To your questions:
Was passport designed mostly for REST/Express?
Not in principle, but you will find most resources about REST and express.
Is this even a common practice or I should stick to some JWT library?
Common practice is to stick to JWT.
More details here: OAuth2 in NestJS for Social Login (Google, Facebook, Twitter, etc)
Example project bhere: https://github.com/thisismydesign/nestjs-starter
So, if I am testing pages in a vacuum without much interaction with the backend, it works great. I am having issues with actually interacting with my UI if it hits any type of service. Basically, nothing is Auth'd. I try programmatically setCookie, no dice. I try to read the cookie, nope. Btw, my whole site requires a login.
cy.setCookie('sess', ';askjdfa;skdjfa;skdjfa;skdjfa;skfjd');<-- does not work
cy.getCookie('sess').should('exist') <-- does not work
I am having an issue on really the best way to "test" this. For example, I have an account section that a user can "update" their personals. I try, fill out the form (via UI testing), but the submission is rejected, no Auth. EVEN THOUGH I just logged in (via UI testing). - I know I need to remove that since it is bad practice to UI-Login for every section of my site.
So, I don't know how to stub graphql calls with cy.request(). Here is my mutation.
mutation Login($email: Email!, $password: String!) {
login(email: $email, password: $password) {
userID
firstName
lastName
}
}
Right now, I am importing the login spec for each section of the site i am testing. I know this is an anti-pattern. Like to solve this problem.
My AUTH (cookie) is not being set. Even when I try to set it, programmatically, doesn't work.
Maybe I should just stub out my graphql mutations? How?
Lastly, IF I am stubbing out my graphql mututations, how do I update the session ( via my main session query ). If I can get these mutations to work, then refreshing the page will get my my updated data, so I'm not completely needing this part.
Any ideas?
I didn't do the stub and all those, as you were asking how the mutation would work with cy.request in my other post. I did it this way and it just basically works. Hopefully this would help
I created a const first though
export const join_graphQL = (query, extra={}) => {
return `mutation {
${query}(join: { email: "${extra.email}", id: "${extra.id}" }) {
id, name, email
}
}`
};
request config const
export const graphqlReqConfig = (body={}, api=graphQlapi, method='POST') => {
return {
method,
body,
url: api,
failOnStatusCode: false
}
};
mutation query with cy.request
const mutationQuery = join_graphQL('mutationName', {
email: "email",
id: 38293
});
cy.request(graphqlReqConfig({
query: mutationQuery
})).then((res) => {
const data = res.body.data['mutationName']; // your result
});
hopefully it's not too messy to see.
basically the fields need to be string such as "${extra.email}" else it will give you error. Not sure how the graphql works deeply but if I just do ${extra.email} I would get an error which I forgot what error it was.
Here's a simpler way of handling a mutation with cy.request
const mutation = `
mutation {
updateUser(id: 1, firstName: "test") {
firstName
lastName
id
role
}
}`
cy.request({
url: url,
method: 'POST',
body: { query: mutation },
headers: {
Authorization: `Bearer ${token}`,
},
})