I've setup graphql-shield with a federated graph.
After the permission rule isAuthenticated returns an error, the resolver of the account service is hit but the data returned for the me query is null.
The expected behavior would be that the account service resolver is not hit and the error the permission rule is throwing is returned.
Does graphql-shield support federated graphs? Am I missing something in the implementation?
const { ApolloServer } = require("apollo-server");
const { ApolloGateway } = require("@apollo/gateway");
const { applyMiddleware } = require("graphql-middleware");
const { formatError } = require("apollo-errors");
const { permissions } = require("./middlewares");
const gateway = new ApolloGateway({
serviceList: [
{ name: "account-service", url: "http://localhost:4001/graphql" }
]
});
(async () => {
const gw = await gateway.load();
const { schema } = applyMiddleware(gw.schema, permissions);
const { executor } = gw;
const server = new ApolloServer({
schema,
executor,
formatError
});
server.listen().then(({ url }) => {
console.log(`馃殌 Server ready at ${url}`);
});
})();
const { shield } = require("graphql-shield");
const Rules = require("./rules");
const permissions = shield(
{
Query: {
me: Rules.isAuthenticated
}
},
{
allowExternalErrors: true,
debug: true
}
);
const { rule } = require("graphql-shield");
const Errors = require("../../../misc/errors");
const isAuthenticated = rule()(async (parent, args, context) => {
return new Errors.NotAuthorizedError();
});
Hey @harri121 :wave:,
Thank you for opening an issue. We will get back to you as soon as we can. Also, check out our Open Collective and consider backing us.
https://opencollective.com/graphql-shield
PS.: We offer
prioritysupport for all backers. Don't forget to addprioritylabel when you start backing us :smile:
I've implemented authentication on the subservice level
@harri121 I am happy to hear that! Do you mind sharing your approach? I believe it could be of great benefit to other people trying out the same thing. I am sorry I couldn't get back to you any sooner.
@maticzav Sure
1锔忊儯The gateway service fetches the user from the request auth token.
2锔忊儯The user is then passed to the sub services via the request header.
3锔忊儯Get the user from the request header in the sub service
4锔忊儯Use the user in your permission rules
const { ApolloServer } = require("apollo-server");
const { ApolloGateway, RemoteGraphQLDataSource } = require("@apollo/gateway");
const gateway = new ApolloGateway({
serviceList: [
{ name: "account-service", url: "http://localhost:4001/graphql" }
],
buildService({ name, url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ request, context }) {
if (context.user) {
// 2锔忊儯pass the user to the sub service
request.http.headers.set("user", context.user);
}
}
});
}
});
(async () => {
const { schema, executor } = await gateway.load();
const server = new ApolloServer({
schema,
executor,
context: ({req}) => {
//1锔忊儯 get user from auth token
return {
user
}
}
});
server.listen().then(({ url }) => {
console.log(`馃殌 Server ready at ${url}`);
});
})();
const fs = require("fs");
const express = require("express");
const { ApolloServer, gql } = require("apollo-server-express");
const { buildFederatedSchema } = require("@apollo/federation");
const { applyMiddleware } = require("graphql-middleware");
const { permissions } = require("./middlewares");
const { resolvers } = require("./resolvers");
const typeDefs = gql`
${fs.readFileSync(__dirname.concat("/schema.graphql"), "utf8")}
`;
const schema = buildFederatedSchema([
{
typeDefs,
resolvers
}
]);
const schemaMiddleware = applyMiddleware(schema, permissions);
const server = new ApolloServer({
schema: schemaMiddleware,
context: ({ req }) => {
// 3锔忊儯get the user
if (!req.headers["user"]) {
return {};
}
const user = req.headers["user"];
return {
user
};
}
});
const app = express();
server.applyMiddleware({ app, path: "/graphql" });
app.listen({ port: 4001 }, () => console.log("Account service ready!"));
Thanks! Truly appreciated. 馃檪
@harri121
can you tell us more details about your first step ( get the user from token in gateway service)
did you add the repository process code in gateway service to load user from DB?
what i was thinking is there would be a sub-service like 'account' which will handle user's business.
this's post give me some inspirations
another confusion is if there is another sub-service like 'book', and there is a mutation loading 'myBooks', how to make the auth information be shared in both 'account' and 'book' resolvers, so that no need to load user data twice. use graphql-shield (or permission middleware) in each service and loading user in each service looks silly. i think there must have a central auth service handle those staffs
@minzojian I'm using RabbitMQ to request the user from the account service. I don't know if that's the ideal way but it works for my use case. Since I pass the user object to the sub services there is no need to query the user again in the permission rules of your sub services.
here is what i did so far,used some hack moves...
gateway service
const gateway = new ApolloGateway({
serviceList: [
//there are 2 sub-services, i use the first service as the auth service, handle user login staffs
{ name: "accounts", url: "http://localhost:4001/graphql" },
{ name: "schools", url: "http://localhost:4002/graphql" },
],
buildService({ name, url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ request, context }) {
//sub-service can read the share context with gateway here
if(context.user)
{
//i record the user model into header as a json string
//cause so far i can not find another way to translate the data out expect via http headers
request.http.headers.set('user',JSON.stringify(context.user));
}
},
});
},
});
(async () => {
const { schema, executor } = await gateway.load();
const server = new ApolloServer({ schema, executor,
subscriptions: false,
context: async ({req,res}) => {
// get the user token from the headers
const token = req.headers.authorization || '';
if(token && req.body.operationName==null)
{
// this is for testing, in practically way, there should add some JWT verify code or call some mutation method like loginWithToken(token)
var a_login_request=`mutation {
login(username:\"my_username\",password:\"my_password\")
{
status,
user{username,id,name}
}
}`;
try{
let authReq={query:a_login_request,variables:req.body.variables}
let result:any=await (gateway['serviceMap']['accounts']).process({request:authReq,context:{}})
//direct call the process function over the account service
//to invoke a login request and get the user data back
return {user:result.data.login.user}
}catch(error){
console.error(error);
}
}
return {};
},
});
...
account service resolver codes
...
{
Query: {
me(_,__,{user}) {
return user;
}
},
Mutation:{
login(_,{username,password},context){
//compared and return with specific values, just for testing
if(username=='my_username' && password=='my_password')
return {status:200,user:{username:'my_username',id:1,name:'hello'}}
else
return {status:500}
},
}
}...
school service
import { ApolloServer, gql } from "apollo-server";
import { buildFederatedSchema } from "@apollo/federation";
const typeDefs = gql`
//i can query mySchool to get the logined user's school data, while as an authration header has been set
extend type Query
{
mySchool:School
}
//also i can query "me" with a sub key "school" to get the logined user's school too
extend type User @key(fields: "id") {
id: ID! @external
school:School
}
type School @key(fields: "schoolId") {
schoolId: ID!
schoolname: String
}
`;
const getSchoolByUserId = (userId) => {
var schoolInfo = students_2_schools.find(item => item.studentId === userId);
if (schoolInfo) {
return schools.find(school => school.schoolId === schoolInfo.schoolId);
}
return null;
}
const resolvers = {
Query: {
mySchool(_, __, { user }) {
if(user)
return getSchoolByUserId(user.id);
return null;
}
},
User: {
school(user) {
return getSchoolByUserId(user.id);
}
},
School: {
__resolveReference(object) {
return schools.find(school => school.schoolId === object.id);
}
}
};
const server = new ApolloServer({
schema: buildFederatedSchema([
{
typeDefs,
resolvers
}
]),
//override context function to load the user data(json string) from request header,and decode it
context: async ({ req, res }) => {
const user = req.headers.user && JSON.parse(req.headers.user);
if (user) {
return { user }
}
return {};
}
});
server.listen({ port: 4002 }).then(({ url }) => {
console.log(`馃殌 Server ready at ${url}`);
});
//////static data for testing/////////
const students_2_schools = [
{ studentId: "1", schoolId: "123" }
]
const schools = [
{
schoolId: "123",
schoolname: "Cambridge"
}
];
as you can see, i use the context function in schools service, also the same code appeared in account service,cause both services need read the possibility user data which set by the gateway service.
those are my current progress
this is my first day coding with graphql maybe will found some smart ways later
@harri121
request.http.headers.set("user", context.user);
it looks can't set an object into header items, so i did json encode, later in sub-service context function,decoded it back to user object
@minzojian @harri121 Would sending the user as a Http header would allow a malicious user to send a request to the sublevel service with the user in the headers to pretend to be that user?
@donedgardo I guess it would. My sublevel services can only be accessed from within my k8s cluster, that's why this solution is working for me. You can also set the Authorization header instead of the user directly and check for it in each sublevel service.
I think this issue is not closed, because the original author did a workaround to solve this bug, but it doesn't mean this bug is fixed. I, for example, am having this bug as well. Can we reopen this issue?
@donedgardo I guess it would. My sublevel services can only be accessed from within my k8s cluster, that's why this solution is working for me. You can also set the Authorization header instead of the user directly and check for it in each sublevel service.
@donedgardo Please please please don't do this. This is essentially the equivalent of relying on network security. Yes, the K8s network boundary is a lot easier to lock down, but if you're building a distributed system don't just take authentication as granted. Forward the auth header, or at least some sort of signed message
Most helpful comment
@maticzav Sure
Approach
1锔忊儯The gateway service fetches the user from the request auth token.
2锔忊儯The user is then passed to the sub services via the request header.
3锔忊儯Get the user from the request header in the sub service
4锔忊儯Use the user in your permission rules
Gateway service
Sub service