Apollo Gateway: Forward Subgraph `set-cookie` Header to Gateway to Client - cookies

I have a subgraph microservice that handles sessions. We store our sessions via cookies that the subgraph creates, and should set it via the set-cookie header. Only issue is my gateway does not seem to be forwarding the set-cookie header from the subgraph to the client.
Here is the code for my gateway
const { ApolloServer } = require('apollo-server');
const { ApolloGateway, RemoteGraphQLDataSource } = require('#apollo/gateway');
const { readFileSync } = require('fs');
const supergraphSdl = readFileSync('./gateway/supergraph.graphql').toString();
class CookieDataSource extends RemoteGraphQLDataSource {
didReceiveResponse({ response, request, context }) {
const cookie = response.http.headers.get('set-cookie');
console.log("Cookie:", cookie)
return response;
}
}
const gateway = new ApolloGateway({
supergraphSdl,
buildService({url}) {
return new CookieDataSource({url});
}
});
const server = new ApolloServer({
gateway,
cors: {
origin: ["http://localhost:3000", "https://studio.apollographql.com"],
credentials: true
},
csrfPrevention: true,
});
server.listen().then(({ url }) => {
console.log(`🚀 Gateway ready at ${url}`);
}).catch(err => {console.error(err)});
version info
“#apollo/gateway”: “^2.1.2”,
“apollo-server”: “^3.10.2”,
I can confirm that the subgraph is sending back a set-cookie header, however, it is not being passed through to the client.
Thank you!

I ended up resolving the issue by creating both a gateway datasource that added context value. Then, pass the header from the subgraph context value to the response header.
import { GatewayGraphQLResponse, GatewayGraphQLRequestContext } from '#apollo/server-gateway-interface';
import { RemoteGraphQLDataSource } from '#apollo/gateway';
import { ApolloServerPlugin, GraphQLRequestContext, GraphQLRequestListener } from '#apollo/server';
interface ServerContext {
passthrough_cookies?: string
}
export class CookieProcessorDataSource extends RemoteGraphQLDataSource {
didReceiveResponse({response, context}: Required<Pick<GatewayGraphQLRequestContext<Record<string, any>>, 'request' | 'response' | 'context'>>): GatewayGraphQLResponse | Promise<GatewayGraphQLResponse> {
context.passthrough_cookies = response.http?.headers.get('set-cookie');
return response;
}
}
export class CookieServerListener implements GraphQLRequestListener<ServerContext> {
public willSendResponse({contextValue, response}: GraphQLRequestContext<ServerContext>): Promise<void> {
if (contextValue.passthrough_cookies !== undefined) {
response.http.headers.set('set-cookie', contextValue.passthrough_cookies);
}
return Promise.resolve()
}
}
export class CookieServerPlugin implements ApolloServerPlugin<ServerContext> {
async requestDidStart() {
return new CookieServerListener();
}
}

Related

How to access authorizer.claims from API gateway Lambda Auth with simple response mode in Flask API

Hi guys I struck with this for a days and I hope some of you can figure it out
I have lambda authorizer with simple response mode like this.
I want to forward value inside JWT token (user_id) to my Flask API.
My authorizer work fine but I don't know to access authorizer.claims in the Flask API.
exports.handler = async(event) => {
const method = 'main';
log.info(`[${method}] BEGIN`);
log.info(`[${method}] incoming event= ${JSON.stringify(event)}`);
const secret = await secretManager(secretName);
const response = {
isAuthorized: false,
context: {
authorizer: {
claims: {}
},
error: {}
}
};
const authorization = event.headers.authorization;
if(!authorization){
log.error(`[${method}] No authorization found`);
response.context.error.message = 'No authorization found';
response.context.error.responseType = 401;
return response;
}
const auth = authorization.split(' ');
if (auth[0] !== 'Bearer' || auth.length !== 2) {
log.error(`[${method}] Invalid authorization`);
response.context.error.message = 'Invalid authorization';
response.context.error.responseType = 401;
return response;
}
try {
const decoded = verify(auth[1], secret.public_key, {
algorithms: ['RS512'],
});
response.isAuthorized = true;
response.context.authorizer.claims = decoded;
} catch (error) {
log.error(`[${method}] Error occured:${error.message}`);
response.isAuthorized = false;
response.context.error.message = error.message;
response.context.error.responseType = 401;
}
log.info(`[${method}] END`);
return response;
};
And my Flask application be like this.
I used blueprint and app.before_request_funcs as a middlewares.
from api.middlewares.fetchUser import fetchUser
from api.routes.blueprint import myBlueprint
app.before_request_funcs = {
myBlueprint.name: [fetchUser]
}
app.register_blueprint(myBlueprint)
Here is where I want to access authorizer.claims to fetch user before my API start.
from flask import request, g
from api.utils.logger import logger
import json
def fetchUser():
logger.warn('FETCH USER')
logger.warn(json.dumps(request.headers.to_wsgi_list()))
logger.warn(json.dumps(request.authorization))
How to access authorizer.claims
Thanks you.

Passing a pdf from Django backend to Angular frontend

I haven't been able to make any of the solutions to similar problems work for my case.
I would like to load a pdf from filesystem with django and return it via an API call to Angular so that it can be displayed. My Django code is pretty much:
class LoadPdfViewSet(views.APIView):
def get(self, request):
# some code here here
response = FileResponse(open(path_to_pdf, 'rb').read())
response.headers = {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment;filename="report.pdf"',
}
response.as_attachment = True
return response
while on the Angular side I have a service that does this:
export class LoadPdfService {
constructor(
private http: HttpClient
) {}
getPdf(): Observable<Blob> {
const params = new HttpParams({
fromObject: {
responsetype: 'arraybuffer'
// other stuff here
}
})
return self.http.get<Blob>(loadpdf_api_url, {params}).pipe(catchError(self.myErrorHandler))
}
}
and a component that tries to open the pdf like this:
export class MyComponent {
constructor(
public loadPdfService: LoadPdfService
) {}
download_pdf() {
let call = self.loadPdfService.getPdf();
call.subscribe( (response:Blob) => {
if (window.navigator && window.navigator.msSaveOrOpenBlob) { // for IE
window.navigator.msSaveOrOpenBlob(blob, "report.pdf");
} else {
let pdfUrl = URL.createObjectURL(blob)
window.open(pdfUrl, '_blank')
URL.revokeObjectURL(pdfUrl);
}
}
}
}
but nothing happens. I have also tried using different responses and passthrough renderers on the django side, and Observable<Response> and .then() callbacks like
response.arrayBuffer().then(buffer => new Blob([buffer], {type: 'application/pdf'}))
on the Angular side. Sometimes I have managed to get the new window/tab to open but no pdf could be displayed.
I finally figured it out. In the python part, the read() can be removed with no problem. The issue was with the service response type and mapping of the response:
getPdf(): Observable<Blob> {
const options = {
params: new HttpParams({
fromObject: {
// my own parameters here
}
}),
responseType: 'blob' as 'json'
};
return this.http.get(this.url, options).pipe(
map(response => response as Blob),
catchError(this.myErrorHandler))
}

How to make AWS API gateway to "understand" trailing slash?

I have an AWS API Gateway configured such that /auth method calls a Lambda.
However, an existing product tries to call /auth/ with trailing slash and it ends up as error 404.
What can I do so that /auth/ URL goes to the same route as /auth in the API Gateway?
Turns out that the way to solve it is to configure the path like so (Terraform code from API Gateway config)
WAS
"GET /auth" = {
integration_type = "AWS_PROXY"
integration_http_method = "POST"
payload_format_version = "2.0"
lambda_arn = module.my-lambda.this_lambda_function_invoke_arn
}
(this makes /auth work)
NOW
"GET /auth/{proxy+}" = {
integration_type = "AWS_PROXY"
integration_http_method = "POST"
payload_format_version = "2.0"
lambda_arn = module.my-lambda.this_lambda_function_invoke_arn
}
(this makes /auth/ work and breaks /auth).
You could configure the route as ANY /{proxy+} so that any HTTP method (GET, POST, PATCH, DELETE) for any routes are directed to the same handler. Alternatively, you could also specify the HTTP method to narrow it down, like POST /{proxy+}.
So...
What can I do so that /auth/ URL goes to the same route as /auth in the API Gateway?
Technically speaking, this solves your problem, but now it is up to you to differentiate routes and know what to do.
As far as I know, this is the only way to achieve it with API Gateway, since according to some RFC out there routes "/auth" and "/auth/" are actually different routes and API Gateway complies with that RFC.
This is what I ended up doing (using the ANY /{proxy+}) and, if it is any help, this is the code I have to handle my routes and know what to do:
// Queue.ts
class Queue<T = any> {
private items: T[];
constructor(items?: T[]) {
this.items = items || [];
}
get length(): number {
return this.items.length;
}
enqueue(element: T): void {
this.items.push(element);
}
dequeue(): T | undefined {
return this.items.shift()!;
}
peek(): T | undefined {
if (this.isEmpty()) return undefined;
return this.items[0];
}
isEmpty() {
return this.items.length == 0;
}
}
export default Queue;
// PathMatcher.ts
import Queue from "./Queue";
type PathMatcherResult = {
isMatch: boolean;
namedParameters: Record<string, string>;
};
const NAMED_PARAMETER_REGEX = /(?!\w+:)\{(\w+)\}/;
class PathMatcher {
static match(pattern: string, path: string): PathMatcherResult {
const patternParts = new Queue<string>(this.trim(pattern).split("/"));
const pathParts = new Queue<string>(this.trim(path).split("/"));
const namedParameters: Record<string, string> = {};
const noMatch = { isMatch: false, namedParameters: {} };
if (patternParts.length !== pathParts.length) return noMatch;
while (patternParts.length > 0) {
const patternPart = patternParts.dequeue()!;
const pathPart = pathParts.dequeue()!;
if (patternPart === "*") continue;
if (patternPart.toLowerCase() === pathPart.toLowerCase()) continue;
if (NAMED_PARAMETER_REGEX.test(patternPart)) {
const [name, value] = this.extractNamedParameter(patternPart, pathPart);
namedParameters[name] = value;
continue;
}
return noMatch;
}
return { isMatch: true, namedParameters };
}
private static trim(path: string) {
return path.replace(/^[\s\/]+/, "").replace(/[\s\/]+$/, "");
}
private static extractNamedParameter(
patternPart: string,
pathPart: string
): [string, string] {
const name = patternPart.replace(NAMED_PARAMETER_REGEX, "$1");
let value = pathPart;
if (value.includes(":")) value = value.substring(value.indexOf(":") + 1);
return [name, value];
}
}
export default PathMatcher;
export { PathMatcherResult };
Then, in my lambda handler, I do:
const httpMethod = event.requestContext.http.method.toUpperCase();
const currentRoute = `${httpMethod} ${event.rawPath}`;
// This will match both:
// GET /products/asdasdasdas
// GET /products/asdasdasdas/
const match = PathMatcher.match("GET /products/{id}", currentRoute);
if (match.isMatch) {
// Here, the id parameter has been extracted for you
const productId = match.namedParameters.id;
}
Of course you can build a registry of routes and their respective handler functions and automate that matching process and passing of parameters, but that is the easy part.

How to dynamically change Apollo Web Socket Link URI?

Currently I've set up Apollo's web socket link like so:
const wsLink = new WebSocketLink({
uri: `ws://example.com/graphql?token=${getToken()}`,
options: {
reconnect: true,
connectionParams(): ConnectionParams {
return {
authToken: getToken(),
};
},
},
});
This works fine while the connection lasts, but fails when the connection needs to be re-established if the token in the query string has expired.
The way the infra I'm dealing with is set up requires this token to be set as a query param in the URI. How can I dynamically change the URI so that I may provide a new token when the connection needs to be re-established?
You can set property wsLink.subscriptionClient.url manually (or create a new subscriptionClient instance?) in function setContext https://www.apollographql.com/docs/link/links/context/.
For example:
import { setContext } from 'apollo-link-context'
...
const wsLink = your code...
const authLink = setContext(() => {
wsLink.subscriptionClient.url = `ws://example.com/graphql?token=${getToken()}`
})
...
const config = {
link: ApolloLink.from([
authLink,
wsLink
]),
...
}

Is it possible to get project metadata in cloud function?

I want to get project-wide metadata set in compute engine within my GCP cloud function. Is this possible?
Here is my try:
metadata.js:
const request = require('request-promise');
async function getMetaData(attr) {
const url = `http://metadata.google.internal/computeMetadata/v1/project/attributes/${attr}`;
const options = {
headers: {
'Metadata-Flavor': 'Google'
}
};
return request(url, options)
.then(response => {
console.info(`Retrieve meta data successfully. meta data: ${response.body}`);
return response.body;
})
.catch(err => {
console.error('Retrieve meta data failed.', err);
});
}
async function retrieveMetaData() {
return {
IT_EBOOKS_API: await getMetaData('IT_EBOOKS_API')
};
}
module.exports = { getMetaData, retrieveMetaData };
cloud function index.js:
const { retrieveMetaData } = require('./metadata');
async function retrieveComputeMetadata(req, res) {
const envVars = await retrieveMetaData();
console.log('envVars: ', envVars);
res.status(200).json(envVars);
}
exports.retrieveComputeMetadata = retrieveComputeMetadata;
When I test the cloud function, the logs show me an error:
Retrieve meta data failed. { StatusCodeError: 404 - "404 page not found\n" at new StatusCodeError (/srv/node_modules/request-promise-core/lib/errors.js:32:15) at ....
It seems the url is not found.
The API you're looking to hit '..v1/project/attributes/' isn't available. As Cloud Functions run on GAE Standard the there are details around which endpoints are available here.