How can I add 2 buildService (upload and header) to apollo federation? - apollo-server

Is there any option to add both header and upload to buildService in apollo federation?
I want my headers for authentication and upload for uploading file. buildService does not support return object.
const gateway = new ApolloGateway({
buildService({ name, url }) {
return new RemoteGraphQLDataSource({
url,
willSendRequest({ request, context }: { request: any; context: any }) {
request?.http?.headers?.set(
"authorization",
context.auth ? context.auth : ""
);
request?.http?.session?.set(
"session",
context.session ? context.session : ""
);
request?.http?.sessionStore?.set(
"sessionStore",
context.sessionStore ? context.sessionStore : ""
);
},
});
},
});
how can I also add upload file to my buildService?

Found the solution, if you extend the FileUploadDataSource somehow works:
import FileUploadDataSource from "#profusion/apollo-federation-upload";
class AuthenticationAndUpload extends FileUploadDataSource {
willSendRequest({
request,
context,
}: {
request: GraphQLRequest;
context: any;
}) {
request?.http?.headers?.set(
"authorization",
context.auth ? context.auth : ""
);
}
}
const gateway = new ApolloGateway({
buildService({ url }) {
return new AuthenticationAndUpload({ url });
},
});

Related

Laravel / SvelteKit sending serverside request with Cookie header

I am making authentication with SvelteKit and Laravel. This is the flow i currently have:
User logs in with correct credentials.
User login route has no middleware enabled on the Laravel side.
This login request returns a JWT token, which gets send back to the Sveltekit server.
I set this token as a cookie using this code:
const headers = {
'Set-Cookie': cookie.serialize(variables.authCookieName, body.token, {
path: '/',
httpOnly: true,
sameSite: 'lax'
})
}
return {
headers,
body: {
user
}
}
The cookie is correctly set after that, verified.
So the authentication is handled correctly. But now i want to send that cookie with Axios to the Laravel server and authenticate the user but that doesn't work. The Laravel server never receives the cookie. The Axios withCredentials setting also never sends that cookie to the Laravel server. How can i make it work so that the cookie header is sent with Axios to Laravel? I have 0 CORS errors in my browser so i don't think that is the issue.
My API Class in SvelteKit:
import axios from 'axios'
import { variables } from '$lib/variables'
const headers: Record<string, string | number | boolean> = {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
}
class Api {
constructor() {
axios.defaults.baseURL = variables.apiUrl
axios.defaults.withCredentials = true
axios.interceptors.response.use(
response => response.data,
error => Promise.reject(error.response.data)
)
}
get(url: string) {
return axios.get(url, { headers })
}
post(url: string, data?: unknown) {
return axios.post(url, data, { headers })
}
patch(url: string, data: Record<string, unknown>) {
return axios.patch(url, data, { headers })
}
}
const api = new Api()
export default api
My Userservice:
import api from '$core/api'
const resource = '/users'
const userService = () => {
const getAll = async () => {
return await api.get(resource)
}
return {
getAll
}
}
export default userService
The Index endpoint (routes/dashboard/index.ts)
import services from '$core/services'
export async function get() {
return await services.user.getAll()
.then(({ data }) => {
return {
body: { users: data.users }
}
}).catch((err) => {
return {
body: { error: err.message }
}
})
}
My Hooks.index.ts (maybe for reference)
import * as cookie from 'cookie'
import jwt_decode from 'jwt-decode'
import type { GetSession, Handle } from '#sveltejs/kit'
import type { User } from '$interfaces/User'
// This is server side
/** #type {import('#sveltejs/kit').Handle} */
export const handle: Handle = async ({ event, resolve }) => {
const { jwt } = cookie.parse(event.request.headers.get('cookie') || '')
if (jwt) {
const { user } = jwt_decode<{ user: User }>(jwt)
if (user) {
event.locals.user = user
}
}
return resolve(event)
}
export const getSession: GetSession = async (request) => {
return {
user: request.locals.user
}
}
Can someone help or explain why Axios has no idea if the cookie is set or not, or how i can send the Cookie with the request to the Laravel Server?

Apollo client QUERIES not sending headers to server but mutations are fine

I hooked up a front end to a graphql server. Most if not all the mutations are protected while all the queries are not protected. I have an auth system in place where if you log in, you get an access/refresh token which all mutations are required to use. And they do which is great, backend receives the headers and everything!
HOWEVER. There is one query that needs at least the access token to distinguish the current user! BUT the backend does not receive the two headers! I thought that the middlewareLink I created would be for all queries/mutations but I'm wrong and couldn't find any additional resources to help me out.
So here's my setup
apollo-client.js
import { InMemoryCache } from "apollo-cache-inmemory"
import { persistCache } from "apollo-cache-persist"
import { ApolloLink } from "apollo-link"
import { HttpLink } from "apollo-link-http"
import { onError } from "apollo-link-error"
import { setContext } from "apollo-link-context"
if (process.browser) {
try {
persistCache({
cache,
storage: window.localStorage
})
} catch (error) {
console.error("Error restoring Apollo cache", error)
}
}
const httpLink = new HttpLink({
uri: process.env.GRAPHQL_URL || "http://localhost:4000/graphql"
})
const authMiddlewareLink = setContext(() => ({
headers: {
authorization: localStorage.getItem("apollo-token") || null,
"x-refresh-token": localStorage.getItem("refresh-token") || null
}
}))
const afterwareLink = new ApolloLink((operation, forward) =>
forward(operation).map(response => {
const context = operation.getContext()
const {
response: { headers }
} = context
if (headers) {
const token = headers.get("authorization")
const refreshToken = headers.get("x-refresh-token")
if (token) {
localStorage.setItem("apollo-token", token)
}
if (refreshToken) {
localStorage.setItem("refresh-token", refreshToken)
}
}
return response
})
)
const errorLink = onError(({ graphQLErrors, networkError }) => {
...
// really long error link code
...
})
let links = [errorLink, afterwareLink, httpLink]
if (process.browser) {
links = [errorLink, afterwareLink, authMiddlewareLink, httpLink]
}
const link = ApolloLink.from(links)
export default function() {
return {
cache,
defaultHttpLink: false,
link
}
}
Is there a way to target ALL mutations/queries with custom headers not just mutations? Or apply some headers to an individual query since I could probably put that as an app middleware?
edit: Haven't solved the SSR portion of this yet.. will re-edit with the answer once I have.

Accessing Mutation Result in Angular Apollo Graphql

I am new to Graphql and I am using the Apollo client with Angular 7.
I have a mutation in the server that I am using for authentication.This mutation generates returns an access token and a refresh token:
#Injectable({
providedIn: "root"
})
export class LoginTokenAuthGQL extends Apollo.Mutation<
LoginTokenAuth.Mutation,
LoginTokenAuth.Variables
> {
document: any = gql`
mutation loginTokenAuth($input: LoginAuthInput!) {
loginTokenAuth(input: $input) {
accessToken
refreshToken
}
}
`;
}
I am running this mutation in my sign-in component like this:
onLoginSubmit() {
const email = this.loginForm.controls['userEmail'].value;
const password = this.loginForm.controls['userPassword'].value;
console.log('Sending mutation with', email, password);
this.loginGQL.mutate({
input: {
email,
password,
userType: AuthUserType.Crm
}
}).pipe(
map((response) => response.data )
).subscribe(
(output: LoginTokenAuth.Mutation) => {
console.log('Access token', output.loginTokenAuth.accessToken);
console.log('Refresh token', output.loginTokenAuth.refreshToken);
console.log(this.apollo.getClient().cache);
},
((error: any) => {
console.error(error);
})
);
}
Once I get the access token I will need to add it as header on my requests.
From what I read from the Apollo Client all results from queries and mutations are cached locally in the client. But it is not clear to me how can I access them and add it to the apollo-link.
To be more clear I would like to do this in my Graphql module:
const http = httpLink.create({uri: '/graphql'});
const auth = setContext((_, { headers }) => {
// get the authentication token from the cache
const token = ???
if (!token) {
return {};
} else {
return {
headers: headers.append('Authorization', `Bearer ${token}`)
};
}
});
Even official docs of Apollo Client suggest you store this token as usually - to localStorage.
import { ApolloClient } from 'apollo-client';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import { InMemoryCache } from 'apollo-cache-inmemory';
const httpLink = createHttpLink({
uri: '/graphql',
});
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = localStorage.getItem('token');
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache()
});

Stitching secure subscriptions using makeRemoteExecutableSchema

We have implemented schema stitching where GraphQL server fetches schema from two remote servers and stitches them together. Everything was working fine when we were only working with Query and Mutations, but now we have a use-case where we even need to stitch Subscriptions and remote schema has auth implemented over it.
We are having a hard time figuring out on how to pass authorization token received in connectionParams from client to remote server via the gateway.
This is how we are introspecting schema:
API Gateway code:
const getLink = async(): Promise<ApolloLink> => {
const http = new HttpLink({uri: process.env.GRAPHQL_ENDPOINT, fetch:fetch})
const link = setContext((request, previousContext) => {
if (previousContext
&& previousContext.graphqlContext
&& previousContext.graphqlContext.request
&& previousContext.graphqlContext.request.headers
&& previousContext.graphqlContext.request.headers.authorization) {
const authorization = previousContext.graphqlContext.request.headers.authorization;
return {
headers: {
authorization
}
}
}
else {
return {};
}
}).concat(http);
const wsLink: any = new WebSocketLink(new SubscriptionClient(process.env.REMOTE_GRAPHQL_WS_ENDPOINT, {
reconnect: true,
// There is no way to update connectionParams dynamically without resetting connection
// connectionParams: () => {
// return { Authorization: wsAuthorization }
// }
}, ws));
// Following does not work
const wsLinkContext = setContext((request, previousContext) => {
let authToken = previousContext.graphqlContext.connection && previousContext.graphqlContext.connection.context ? previousContext.graphqlContext.connection.context.Authorization : null
return {
context: {
Authorization: authToken
}
}
}).concat(<any>wsLink);
const url = split(({query}) => {
const {kind, operation} = <any>getMainDefinition(<any>query);
return kind === 'OperationDefinition' && operation === 'subscription'
},
wsLinkContext,
link)
return url;
}
const getSchema = async (): Promise < GraphQLSchema > => {
const link = await getLink();
return makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link,
});
}
const linkSchema = `
extend type UserPayload {
user: User
}
`;
const schema: any = mergeSchemas({
schemas: [linkSchema, getSchema],
});
const server = new GraphQLServer({
schema: schema,
context: req => ({
...req,
})
});
Is there any way for achieving this using graphql-tools? Any help appreciated.
I have one working solution: the idea is to not create one instance of SubscriptionClient for the whole application. Instead, I'm creating the clients for each connection to the proxy server:
server.start({
port: 4000,
subscriptions: {
onConnect: (connectionParams, websocket, context) => {
return {
subscriptionClients: {
messageService: new SubscriptionClient(process.env.MESSAGE_SERVICE_SUBSCRIPTION_URL, {
connectionParams,
reconnect: true,
}, ws)
}
};
},
onDisconnect: async (websocket, context) => {
const params = await context.initPromise;
const { subscriptionClients } = params;
for (const key in subscriptionClients) {
subscriptionClients[key].close();
}
}
}
}, (options) => console.log('Server is running on http://localhost:4000'))
if you would have more remote schemas you would just create more instances of SubscriptionClient in the subscriptionClients map.
To use those clients in the remote schema you need to do two things:
expose them in the context:
const server = new GraphQLServer({
schema,
context: ({ connection }) => {
if (connection && connection.context) {
return connection.context;
}
}
});
use custom link implementation instead of WsLink
(operation, forward) => {
const context = operation.getContext();
const { graphqlContext: { subscriptionClients } } = context;
return subscriptionClients && subscriptionClients[clientName] && subscriptionClients[clientName].request(operation);
};
In this way, the whole connection params will be passed to the remote server.
The whole example can be found here: https://gist.github.com/josephktcheung/cd1b65b321736a520ae9d822ae5a951b
Disclaimer:
The code is not mine, as #josephktcheung outrun me with providing an example. I just helped with it a little. Here is the original discussion: https://github.com/apollographql/graphql-tools/issues/864
This is a working example of remote schema with subscription by webscoket and query and mutation by http. It can be secured by custom headers(params) and shown in this example.
Flow
Client request
-> context is created by reading req or connection(jwt is decoded and create user object in the context)
-> remote schema is executed
-> link is called
-> link is splitted by operation(wsLink for subscription, httpLink for queries and mutations)
-> wsLink or httpLink access to context created above (=graphqlContext)
-> wsLink or httpLink use context to created headers(authorization header with signed jwt in this example) for remote schema.
-> "subscription" or "query or mutation" are forwarded to remote server.
Note
Currently, ContextLink does not have any effect on WebsocketLink. So, instead of concat, we should create raw ApolloLink.
When creating context, checkout connection, not only req. The former will be available if the request is websocket, and it contains meta information user sends, like an auth token.
HttpLink expects global fetch with standard spec. Thus, do not use node-fetch, whose spec is incompatible (especially with typescript). Instead, use cross-fetch.
const wsLink = new ApolloLink(operation => {
// This is your context!
const context = operation.getContext().graphqlContext
// Create a new websocket link per request
return new WebSocketLink({
uri: "<YOUR_URI>",
options: {
reconnect: true,
connectionParams: { // give custom params to your websocket backend (e.g. to handle auth)
headers: {
authorization: jwt.sign(context.user, process.env.SUPER_SECRET),
foo: 'bar'
}
},
},
webSocketImpl: ws,
}).request(operation)
// Instead of using `forward()` of Apollo link, we directly use websocketLink's request method
})
const httpLink = setContext((_graphqlRequest, { graphqlContext }) => {
return {
headers: {
authorization: jwt.sign(graphqlContext.user, process.env.SUPER_SECRET),
},
}
}).concat(new HttpLink({
uri,
fetch,
}))
const link = split(
operation => {
const definition = getMainDefinition(operation.query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink, // <-- Executed if above function returns true
httpLink, // <-- Executed if above function returns false
)
const schema = await introspectSchema(link)
const executableSchema = makeRemoteExecutableSchema({
schema,
link,
})
const server = new ApolloServer({
schema: mergeSchemas([ executableSchema, /* ...anotherschemas */]),
context: ({ req, connection }) => {
let authorization;
if (req) { // when query or mutation is requested by http
authorization = req.headers.authorization
} else if (connection) { // when subscription is requested by websocket
authorization = connection.context.authorization
}
const token = authorization.replace('Bearer ', '')
return {
user: getUserFromToken(token),
}
},
})

"react-admin" proxy issue with dataProvider

I have my build running on my localhost. I have the dataProvider working properly with all standard views using https (i.e. "Show", Create"). I want to create data from within a function like this:
import { CREATE, GET_ONE, UPDATE } from 'react-admin';
import dataProviderFactory from '../dataProvider';
dataProviderFactory(
process.env.REACT_APP_SERVER_HTTPS_URL
).then(dataProvider => {
dataProvider(CREATE, 'batches', {
data: {
name: "test"
}
})
.then(response => response.data)
.then(data => {
console.log('data', data);
});
});
The exact same POST in the entity 'batches' works from the standard react-admin "create" template, but with this function, it fails. It appears to be trying to proxy to 'localhost' instead of maintaining the root API URL. Is there some other way I need to either pull the dataProvider from the Admin controller, or can I spec the proxy root url for the dataProvider?
dataProvier.js:
export default type => {
return import('./rest').then(provider => provider.default);
};
rest.js:
import simpleRestProvider from 'ra-data-simple-rest';
import Constants from "../constants/Constants";
import { fetchUtils } from 'react-admin';
import SimpleRestClient from '../utils/SimpleRestClient';
import AppConfig from '../AppConfig';
const httpClient = (url, options = {}) => {
if (!options.headers) {
options.headers = new Headers();
}
if(!url.endsWith("/authenticate") && localStorage.getItem('token') !== null) {
options.user = {
authenticated: true,
token: 'Bearer ' + localStorage.getItem('token')
}
}
return fetchUtils.fetchJson(url, options);
}
// Use custom rest client. For fakeserver use react admin rest client
const restDataProvider = !AppConfig.useFakeServer ? SimpleRestClient(Constants.urls.apiBaseUrl, httpClient) :
simpleRestProvider(Constants.urls.apiBaseUrl, httpClient);
export default (type, resource, params) =>
new Promise(resolve => {
if (!AppConfig.useFakeServer) {
setTimeout(() => resolve(restDataProvider(type, resource, params)), 500);
} else {
resolve(restDataProvider(type, resource, params));
}
}
);

Resources