I'm trying to create an authentication strategy using context within express-graphql, however, when I access context within isAuthenticated it returns [Function: context]. What am I not understanding?
app.use(
"/graphql",
graphqlHTTP(async (req: any) => ({
schema: schema,
graphiql: true,
context: (req: any) => {
const user = users.find((user) => user.username === "test user");
if (!user) {
return {
message: "Incorrect username or password.",
};
}
return {
user: "test user",
active: "Yes",
};
},
}))
);
const isAuthenticated =
() =>
(next: any) =>
async (root: any, args: any, context: any, info: any) => {
console.log("context", context);
if (!context.currentUser) {
throw new Error("You are not authorized");
}
return next(root, args, context, info);
};
Here's an advice I got from Samer Buna in his book titled "GraphQL in Action":
Don't do authentication within GraphQL resolving logic. It's better to
have a different layer handling it either before or after the GraphQL
service layer.
Can you try something like this
server.use("/", (req, res) => {
//currentUser logic here
graphqlHTTP({
schema: schema,
context: { currentUser },
})(req, res);
});
Related
I'm trying to test NestJS's built in HttpService (which is based on Axios). I'm having trouble testing error/exception states though. In my test suite I have:
let client: SomeClearingFirmClient;
const mockConfigService = {
get: jest.fn((type) => {
switch(type) {
case 'someApiBaseUrl': {
return 'http://example.com'
}
case 'someAddAccountEndpoint': {
return '/ClientAccounts/Add';
}
case 'someApiKey': {
return 'some-api-key';
}
default:
return 'test';
}
}),
};
const successfulAdd: AxiosResponse = {
data: {
batchNo: '39cba402-bfa9-424c-b265-1c98204df7ea',
warning: '',
},
status: 200,
statusText: 'OK',
headers: {},
config: {},
};
const failAddAuth: AxiosError = {
code: '401',
config: {},
name: '',
message: 'Not Authorized',
}
const mockHttpService = {
post: jest.fn(),
get: jest.fn(),
}
it('Handles a failure', async () => {
expect.assertions(1);
mockHttpService.post = jest.fn(() => of(failAddAuth));
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: ConfigService,
useValue: mockConfigService,
},
{
provide: HttpService,
useValue: mockHttpService,
},
SomeClearingFirmClient,
],
}).compile();
client = module.get<SomeClearingFirmClient>(SomeClearingFirmClient);
const payload = new SomeClearingPayload();
try {
await client.addAccount(payload);
} catch(e) {
console.log('e', e);
}
});
And my implementation is:
async addAccount(payload: any): Promise<SomeAddResponse> {
const addAccountEndpoint = this.configService.get('api.someAddAccountEndpoint');
const url = `${this.baseUrl}${addAccountEndpoint}?apiKey=${this.apiKey}`;
const config = {
headers: {
'Content-Type': 'application/json',
}
};
const response = this.httpService.post(url, payload, config)
.pipe(
map(res => {
return res.data;
}),
catchError(e => {
throw new HttpException(e.response.data, e.response.status);
}),
).toPromise().catch(e => {
throw new HttpException(e.message, e.code);
});
return response;
}
Regardless of whether I use Observables or Promises, I can't get anything to catch. 4xx level errors sail on through as a success. I feel like I remember Axios adding some sort of config option to reject/send an Observable error to subscribers on failures... but I could be imagining that. Am I doing something wrong in my test harness? The other StackOverflow posts I've seen seem to say that piping through catchError should do the trick, but my errors are going through the map operator.
Your mockHttpService seems to return no error, but a value:
mockHttpService.post = jest.fn(() => of(failAddAuth));
What of(failAddAuth) does is to emit a value(failAddAuth) and then complete.
That's why the catchError from this.httpService.post(url, payload, config) will never be reached, because no errors occur.
In order to make sure that catchError is hit, the observable returned by post() must emit an error notification.
You could try this:
// Something to comply with `HttpException`'s arguments
const err = { response: 'resp', status: '4xx' };
mockHttpService.post = jest.fn(() => throwError(err));
throwError(err) is the same as new Observable(s => s.error(err))(Source code).
Hello I'm new to GraphQl and to Apollo Server.
I would like to implement authentication on my project.
But
For some reason, I can't seem to set context on my resolvers in apollo server.
Here's my index
const server = new ApolloServer({
typeDefs,
resolvers,
context: ({ req }) => {
const userId = jwtDecode(req.headers.authorization)
return userId.sub
}
})
And my query
Query: {
users: async (parent, args, context) => {
try {
console.log(context)
return await getUsers(context)
} catch (err) {
console.log(err)
throw new Error(err.message)
}
}
When I try to output the context the result is always like this...
{ injector:
Injector {
options:
{ name: 'index.ts_8346047369535445_SESSION',
injectorScope: 'SESSION',
hooks: [Array],
children: [] },
_classMap: Map {},
_factoryMap: Map {},
_applicationScopeInstanceMap:
Map {
Symbol(ModuleConfig.index.ts_8346047369535445) => undefined,
[Function] => undefined },
_sessionScopeInstanceMap: Map { [Function: ModuleSessionInfo] => [ModuleSessionInfo] },
_applicationScopeServiceIdentifiers:
[ Symbol(ModuleConfig.index.ts_8346047369535445), [Function] ],
_requestScopeServiceIdentifiers: [],
_sessionScopeServiceIdentifiers: [ [Function: ModuleSessionInfo] ],
_hookServiceIdentifiersMap: Map {},
_name: 'index.ts_8346047369535445_SESSION',
_injectorScope: 'SESSION',
_defaultProviderScope: 'SESSION',
........
What's returned inside the context function should always be an object. So you would do something like
context: ({ req }) => {
const { sub } = jwtDecode(req.headers.authorization)
return {
sub,
}
}
and then access the value inside the resolver by calling context.sub.
However, if you're using GraphQL Modules to create your schema, you should follow the library's documentation for configuring your context on a per-module basis.
I just want to send the request to all my resolvers through the context field, but when I access it from one of my resolvers, it returns null.
app.use('/graphql', graphqlHTTP(async (request, response, graphQLParams) => ({
schema: schema,
context:{token_1:null,test:request},
graphiql:true
})));
These are part of my schema. Firstly I Login to set the token ,but when I want to access the context.token_1 from the other resolver (BuyItems), it returns null.
BuyItems :{
type: UserType,
args: {
name: {type: GraphQLString},
points: {type: GraphQLInt}
},
resolve(parent,args,context){
console.log(context.token_1)
return UserModel.findOneAndUpdate({name:args.name},{points:args.points})
}
},
Login: {
type: AuthType,
args: {
email: {type:GraphQLString},
password: {type:GraphQLString}
},
async resolve(parent,args,context){
const user = await UserModel.findOne({ email: args.email });
if (!user) {
throw new Error('User does not exist on login!');
}
const isEqual = await bcrypt.compare(args.password, user.password);
if (!isEqual) {
throw new Error('Password is incorrect!');
}
const token = jwt.sign(
{ userId: user.id, email: user.email },
'somesupersecretkey',
{ expiresIn: '1h' }
);
context.token_1 = token;
return {tokenExpiration: 1, userId: user.id, token:token}
}
}
I am OK with javascript but I am very new to GraphQL. I currently have this GraphQL structure and it is working. I found examples online for how to get different types organized into SRP files. I am however unable to find how to do this with the resolve: as it requires a function.
GraphQL:
const RootQueryType = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
ownerData: {
type: OwnerType,
description: 'Get all owners',
args:{key: {type: GraphQLString} },
resolve: (obj, args) => {
const url = 'http://localhost:5001/api/.../' + args.key
return fetch(url)
.then(response => {
return response.json()
})
.then(json => {
return transform(json)
})
.catch(err => {
console.trace(err)
})
}
},
carData: {
type: carType,
description: 'Get owned vehicles',
args:{key: {type: GraphQLString} },
resolve: (obj, args) => {
const url = 'http://localhost:6001/api/.../' + args.key
return fetch(url)
.then(response => {
return response.json()
})
.then(json => {
return transform(json)
})
.catch(err => {
console.trace(err)
})
}
},
}
})
I can move the service calls into separate files but not sure how to structure the resolve as it needs a function.
Would it be something like this:
const VehicleService = require('./ExternalServices/Vehicles');
.....snip...
resolve: (obj, args) => { VehicleService.GetVehicles() }
Generally speaking, I've found the best way to keep my code organized is to put all of the business logic elsewhere, initialized into the context object. If you're using graphql-js directly (it'll be set up differently if you're using something like apollo-server, but the context is still the right place for this):
graphql.js excerpt
(dataloaders is the SRP logic here)
const { graphql } = require('graphql');
const dataloaders = require('./dataloaders');
const typeDefs = require('./type-definitions')
const schema = require('./schema')
exports.query = (whatever, args) => {
const context = {};
context.requestId = 'uuid-something';
context.loaders = dataloaders.initialize({ context });
return graphql(schema, query, null, context)
}
schema.js excerpt
const RootQueryType = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
ownerData: {
type: OwnerType,
description: 'Get all owners',
args:{key: {type: GraphQLString} },
resolve: (obj, args, context) => {
const key = args.key;
return context.loaders.owner({ key });
}
}
}
});
dataloaders.js excerpt
exports.initialize = ({ context }) => {
return {
owner({ key }) {
const url = 'http://localhost:6001/api/.../' + key
return fetch(url, { headers: { requestId: context.requestId }})
.then(response => {
return response.json()
})
.then(json => {
return transform(json)
})
.catch(err => {
console.trace(err)
});
}
}
};
In addition to better code organization, doing it this way allows for easier testing, since your resolvers don't need any external dependencies. You can inject your dependencies this way by preloading the context with whatever you want for testing, and you can handle business logic where business logic belongs.
Initializing your business logic with the context of the request also allows you to adjust functionality based off the request: requestId (as I've shown), access control, etc.
The syntax () => {} is simply a function definition. The resolve field expects a function definition so it can run it when the field must be resolved.
You can move the resolving function to a different file like so:
car-data-resolve.js:
const resolveCarData = (obj, args) => {
const url = 'http://localhost:6001/api/.../' + args.key
return fetch(url)
.then(response => {
return response.json()
})
.then(json => {
return transform(json)
})
.catch(err => {
console.trace(err)
})
}
export default resolveCarData;
And then use it in your schema definition:
import resolveCarData from './car-data-resolve';
const RootQueryType = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
/* Other fields */
carData: {
type: carType,
description: 'Get owned vehicles',
args: { key: { type: GraphQLString } },
resolve: resolveCarData
},
},
});
I understand how to set the context object when creating a GraphQL server e.g.
const app = express();
app.use(GRAPHQL_URL, graphqlExpress({
schema,
context: {
foo: 'bar'
},
}));
so that the context object is passed to my resolvers when handling an incoming request.
However I'm not seeing this context object when the resolvers are triggered by a subscription (i.e. a client subscribes to a GraphQL subscription, and defines the shape of the data to be sent to them when the subscription fires); in that case the context appears to be an empty Object.
Is there way to ensure that my context object is set correctly when resolvers are called following a PubSub.publish() call?
I guess you are using the package subscription-transport-ws. In that case it is possible to add a context value in different execution steps.
See API. Two possible scenarios
If you have some kind of authentication. You could add a viewer in the context at the onConnect execution step. This is done at the first connection to the websocket and wont change until the connection is closed and opened again. See example.
If you want to add a context more dynamically you can add a kind of middleware before the execute step.It could look like this:
const middleware = (args) => new Promise((resolve, reject) => {
const [schema, document, root, context, variables, operation] = args;
context.foo = "bar"; // add something to context
resolve(args);
})
subscriptionServer = SubscriptionServer.create({
schema: executable.schema,
subscribe,
execute: (...args) => middleware(args).then(args => {
return execute(...args);
})
}, {
server: websocketServer,
path: "/graphql",
}, );
Here is my solution:
You can pass the context and do the authentication for graphql subscription(WebSocket )like this:
const server = new ApolloServer({
typeDefs,
resolvers,
context: contextFunction,
introspection: true,
subscriptions: {
onConnect: (
connectionParams: IWebSocketConnectionParams,
webSocket: WebSocket,
connectionContext: ConnectionContext,
) => {
console.log('websocket connect');
console.log('connectionParams: ', connectionParams);
if (connectionParams.token) {
const token: string = validateToken(connectionParams.token);
const userConnector = new UserConnector<IMemoryDB>(memoryDB);
let user: IUser | undefined;
try {
const userType: UserType = UserType[token];
user = userConnector.findUserByUserType(userType);
} catch (error) {
throw error;
}
const context: ISubscriptionContext = {
// pubsub: postgresPubSub,
pubsub,
subscribeUser: user,
userConnector,
locationConnector: new LocationConnector<IMemoryDB>(memoryDB),
};
return context;
}
throw new Error('Missing auth token!');
},
onDisconnect: (webSocket: WebSocket, connectionContext: ConnectionContext) => {
console.log('websocket disconnect');
},
},
});
You can pass the context argument of resolver using pubsub.publish method in your resolver like this:
addTemplate: (
__,
{ templateInput },
{ templateConnector, userConnector, requestingUser }: IAppContext,
): Omit<ICommonResponse, 'payload'> | undefined => {
if (userConnector.isAuthrized(requestingUser)) {
const commonResponse: ICommonResponse = templateConnector.add(templateInput);
if (commonResponse.payload) {
const payload = {
data: commonResponse.payload,
context: {
requestingUser,
},
};
templateConnector.publish(payload);
}
return _.omit(commonResponse, 'payload');
}
},
Now, we can get the http request context and subscription(websocket) context in
your Subscription resolver subscribe method like this:
Subscription: {
templateAdded: {
resolve: (
payload: ISubscriptionPayload<ITemplate, Pick<IAppContext, 'requestingUser'>>,
args: any,
subscriptionContext: ISubscriptionContext,
info: any,
): ITemplate => {
return payload.data;
},
subscribe: withFilter(templateIterator, templateFilter),
},
},
async function templateFilter(
payload?: ISubscriptionPayload<ITemplate, Pick<IAppContext, 'requestingUser'>>,
args?: any,
subscriptionContext?: ISubscriptionContext,
info?: any,
): Promise<boolean> {
console.count('templateFilter');
const NOTIFY: boolean = true;
const DONT_NOTIFY: boolean = false;
if (!payload || !subscriptionContext) {
return DONT_NOTIFY;
}
const { userConnector, locationConnector } = subscriptionContext;
const { data: template, context } = payload;
if (!subscriptionContext.subscribeUser || !context.requestingUser) {
return DONT_NOTIFY;
}
let results: IUser[];
try {
results = await Promise.all([
userConnector.findByEmail(subscriptionContext.subscribeUser.email),
userConnector.findByEmail(context.requestingUser.email),
]);
} catch (error) {
console.error(error);
return DONT_NOTIFY;
}
//...
return true;
}
As you can see, now we get the subscribe users(who establish the WebSocket connection with graphql webserver) and HTTP request user(who send the mutation to graphql webserver) from subscriptionContext and HTTP request context.
Then you can do the rest works if the return value of templateFilter function is truthy, then WebSocket will push message to subscribe user with payload.data, otherwise, it won't.
This templateFilter function will be executed multiple times depending on the count of subscribing users which means it's iterable. Now you get each subscribe user in this function and does your business logic to decide if push WebSocket message to the subscribe users(client-side) or not.
See github example repo
Articles:
GraphQL Subscription part 1
GraphQL Subscription part 2
If you're using Apollo v3, and graphql-ws, here's a docs-inspired way to achieve context resolution:
const wsContext = async (ctx, msg, args) => {
const token = ctx.connectionParams.authorization;
const currentUser = await findUser(token);
if(!currentUser) throw Error("wrong user token");
return { currentUser, foo: 'bar' };
};
useServer(
{
schema,
context: wsContext,
}
wsServer,
);
You could use it like so in your Apollo React client:
import { GraphQLWsLink } from '#apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
const wsLink = new GraphQLWsLink(createClient({
url: 'ws://localhost:4000/subscriptions',
connectionParams: {
authorization: user.authToken,
},
}));