AWS Lambda handle authorization headers error - aws-lambda

For my project, I'm utilizing AWS Lambda and Graphql. I used apollo-server-lambda for this project. For this project, I created custom headers. And I added a simple condition to throw an error if there is no 'event.headers.authorization'. When the app is launched in a local environment, the error is thrown correctly. But the issue is that I'm not sure how I'm going to put my authorisation in if it's continuously throwing me off. I'm certain my implementation is incorrect. I'm not sure what the best method is for obtaining authorization.
It should be put like this:
.
This is my Lambda
import * as R from 'ramda';
import { AuthenticationError, ForbiddenError } from 'apollo-server-lambda';
export const authToken = (token: string) => {
if (token === 'HELLO') {
return true;
} else {
throw new AuthenticationError('No authorization header supplied');
}
};
const lambda =
(lambdaFunc: AWSLambda.Handler): AWSLambda.Handler =>
(event, context, callback) => {
const { authorization } = event.headers;
if (R.isNil(authorization))
throw new ForbiddenError('You must be authenticated'); // always thorws me error
return authToken(event.headers.authorization);
return lambdaFunc(event, context, callback);
};
export default lambda;
This is my graphql
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core';
import { ApolloServer} from 'apollo-server-lambda';
import schema from '../graphql/schema';
import resolvers from '../resolvers';
import lambda from '../utils/lambda';
const server = new ApolloServer({
typeDefs: schema,
resolvers,
debug: false,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
introspection: true,
});
export default lambda(
server.createHandler({
expressGetMiddlewareOptions: {
cors: {
origin: '*',
credentials: true,
allowedHeaders: ['Content-Type', 'Origin', 'Accept', 'authorization'],
optionsSuccessStatus: 200,
maxAge: 200,
exposedHeaders: ['authorization'],
},
},
})
);
This is YAML file
functions:
graphql:
handler: src/handlers/graphql.default
events:
- http:
path: ${env:api_prefix}/graphql
method: get
cors: true
- http:
path: ${env:api_prefix}/graphql
method: post
cors: true

Related

graphql ExecutionContext not recognized by nest-keycloak-connect

I would like to authorize users in GraphQL in a NestJs project. I use nest-keycloak-connect for this.
Unfortunately, when calling query with "Authorization" set in Headers, I get the error: [Keycloak] Empty JWT, unauthorized.
So it looks like nest-keycloak-connect doesn't recognize that context comes from graphql.
However, when looking at the source code of nest-keycloak-connect, context is checked for http and graphql there.
So what should I do to make nest-keycloak-connect start using graphql correctly?
nest-keycloak-connect context type checking
export const extractRequest = (context: ExecutionContext): [any, any] => {
let request: any, response: any;
// Check if request is coming from graphql or http
if (context.getType() === 'http') {
// http request
const httpContext = context.switchToHttp();
request = httpContext.getRequest();
response = httpContext.getResponse();
} else if (context.getType<GqlContextType>() === 'graphql') {
let gql: any;
// Check if graphql is installed
try {
gql = require('#nestjs/graphql');
} catch (er) {
throw new Error('#nestjs/graphql is not installed, cannot proceed');
}
// graphql request
const gqlContext = gql.GqlExecutionContext.create(context).getContext();
request = gqlContext.req;
response = gqlContext.res;
}
return [request, response];
};
my auth settings
#Module({
imports: [
MongooseModule.forRoot('mongodb://localhost/Products'),
KeycloakConnectModule.register({
authServerUrl: 'http://localhost:8080/auth',
realm: 'users',
clientId: 'users-service',
secret: 'h1xAJnShNwPmxzySR8Y0d3fLh27iwPPh',
policyEnforcement: PolicyEnforcementMode.PERMISSIVE, // optional
tokenValidation: TokenValidation.ONLINE, // optional
}),
GraphQLModule.forRoot<ApolloFederationDriverConfig>({
driver: ApolloFederationDriver,
autoSchemaFile: true,
}),
ProductModule,
ProductImageModule,
ProductAttributeModule,
],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_GUARD,
useClass: ResourceGuard,
},
{
provide: APP_GUARD,
useClass: RoleGuard,
},
],
controllers: [],
})
export class AppModule {}
Resolver
//FindAll
#Query(() => [Product])
#Roles({roles: ['user']})
async products() {
const products = await this.productService.findAll();
return products;
}
Header
{
"Authorization":"eyJhbGciOiJSUzI1N..."
}
Solved by adding Bearer to the header
{
"authorization":"Bearer eyJhbGciOiJSUzI1NiIsInR..."
}

There is no matching message handler error in NestJs TCP E2E test

I'm playing around with Microservice architecture using NestJs. I've made a simplified repository with a few services that communicate over TCP with a mix of message and event patterns.
I have moved on to writing E2E tests for the using Supertest, and while I'm able to run the needed microservice, the requests respond with {"error": "There is no matching message handler defined in the remote service.", "statusCode": 500}
GatewayService: HTTP Rest Api where the E2E tests are run. Calls the service
AuthService: NestJs microservice running on 0.0.0.0:3001 by default
configService: a simple service that returns information needed to set up the services, like host and port. I have tried eliminating it from the test and hardcoding the values.
The E2E test file
import { INestApplication, ValidationPipe } from '#nestjs/common';
import { ClientProxy, ClientsModule, Transport } from '#nestjs/microservices';
import { Test, TestingModule } from '#nestjs/testing';
import * as request from 'supertest';
import { configService } from '../src/config.service';
import { RpcExceptionFilter } from '../src/filters/rpc-exception.filter';
import { AppModule } from './../src/app.module';
describe('AuthenticationController (e2e)', () => {
let app: INestApplication;
let authClient: ClientProxy;
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [
AppModule,
ClientsModule.register([
{
...configService.getServiceConfigs().authService,
transport: Transport.TCP,
},
]),
],
}).compile();
// Setup the app instance
app = moduleFixture.createNestApplication();
// Setup the relevant micorservice(s)
app.connectMicroservice({
transport: Transport.TCP,
name: configService.getServiceConfigs().authService.name,
options: configService.getServiceConfigs().authService.options,
});
app.startAllMicroservices();
// Add request validation
app.useGlobalPipes(
new ValidationPipe({
transform: true,
whitelist: true,
forbidNonWhitelisted: true,
forbidUnknownValues: true,
}),
);
// Add needed filters
app.useGlobalFilters(new RpcExceptionFilter());
await app.init();
authClient = app.get(configService.getServiceConfigs().authService.name);
await authClient.connect();
console.log('authClient', authClient);
});
describe('POST /auth/login', () => {
it('Should return status 200 and a user object with access token', () => {
return (
request(app.getHttpServer())
.post('/auth/login')
.send({ username: 'exmple#user.com', password: 'password' })
// .expect(200)
.expect((response) => {
console.log('response', response.body);
expect(response.body).toHaveProperty('id');
expect(response.body).toHaveProperty('username');
expect(response.body).toHaveProperty('accessToken');
})
);
});
});
afterAll(async () => {
await app.close();
await authClient.close();
});
});
I have attempted adding a provider which I've used before when working with Grpc as the transport layer (this is TCP). Didn't change anything.
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
...
providers: [
{
provide: 'AUTH_SERVICE',
useFactory: () => {
return ClientProxyFactory.create({
transport: Transport.TCP,
options: { host: 'localhost', port: 3001 },
});
},
},
],
I know that the microservice starts up and the gateway service is able to connect to it since when printing the authClient: Client proxy it returns a correct object with URL 0.0.0.0:3001. If I change the URL, or the name of the service in any part of the setup then errors about missing providers show, further confirming that it is supposedly correctly set up.
One of the best guides I've found on this matter. Sadly it doesn't work for my code.

apollo-server-lambda: Unable to determine event source based on event

I am using apollo-server-lambda for my app. I have create custom authoization http headers and it is required . if authoization: LETMEIN then it will return true and also return all data, if there is no any authoization or wrong authoization then it wll throw an error. For local development I used serverless-offline.In Local environment, it works as expected and here is the image but when I deploy my code to AWS, the api end does not work. It always throws me the error: here is the link.
I test my function AWS console. I am getting this error:
I did not get what I am doing wrong.
Here is my code
/* eslint-disable #typescript-eslint/no-var-requires */
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core';
import { ApolloServer, AuthenticationError } from 'apollo-server-lambda';
import schema from '../graphql/schema';
import resolvers from '../resolvers';
import runWarm from '../utils/run-warm';
export const authToken = (token: string) => {
if (token === 'LETMEIN') {
return;
} else {
throw new AuthenticationError('No authorization header supplied');
}
};
const server = new ApolloServer({
typeDefs: schema,
resolvers,
debug: false,
plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
context: ({ event }) => {
//console.log(context);
if (event.headers) {
authToken(event.headers.authorization);
}
},
});
export default runWarm(
server.createHandler({
expressGetMiddlewareOptions: {
cors: {
origin: '*',
credentials: true,
allowedHeaders: ['Content-Type', 'Origin', 'Accept'],
optionsSuccessStatus: 200,
maxAge: 200,
},
},
})
);
This is my Lambda function
/**
* Running warm functions help prevent cold starts
*/
const runWarm =
(lambdaFunc: AWSLambda.Handler): AWSLambda.Handler =>
(event, context, callback) => {
// Detect the keep-alive ping from CloudWatch and exit early. This keeps our
// lambda function running hot.
if (event.source === 'serverless-plugin-warmup') {
return callback(null, 'pinged');
}
return lambdaFunc(event, context, callback);
};
export default runWarm;
This is not a direct answer, but might help, and could be useful if anyone else (like me) found this thread because of the error "Unable to determine event source based on event" when using apollo-server-lambda.
That error is coming from #vendia/serverless-express which is being used by apollo-server-lambda.
Within serverless-express, in src/event-sources/utils.js, there is a function called getEventSourceNameBasedOnEvent(), which is throwing the error. It needs to find something in the event object, and after a bit of experimentation I found that writing the lambda function like this solved the issue for me:
const getHandler = (event, context) => {
const server = new ApolloServer({
typeDefs,
resolvers,
debug: true,
});
const graphqlHandler = server.createHandler();
if (!event.requestContext) {
event.requestContext = context;
}
return graphqlHandler(event, context);
}
exports.handler = getHandler;
Note that the context object is added to the event object with the key "requestContext"....that's the fix.
(Also note that I have defined typeDefs and resolvers elsewhere in the code)
I can't guarantee this is the ideal thing to do, but it did work for me.

Pubsub publish multiple events Apollo Server

I am using Apollo Server and I want to publish 2 events in the row from same resolver. Both subscriptions are working fine but only if I dispatch only one event. If I try to dispatch both, second subscription resolver never gets called. If I comment out the first event dispatch second works normally.
const publishMessageNotification = async (message, me, action) => {
const notification = await models.Notification.create({
ownerId: message.userId,
messageId: message.id,
userId: me.id,
action,
});
// if I comment out this one, second pubsub.publish starts firing
pubsub.publish(EVENTS.NOTIFICATION.CREATED, {
notificationCreated: { notification },
});
const unseenNotificationsCount = await models.Notification.find({
ownerId: notification.ownerId,
isSeen: false,
}).countDocuments();
console.log('unseenNotificationsCount', unseenNotificationsCount);// logs correct value
// this one is not working if first one is present
pubsub.publish(EVENTS.NOTIFICATION.NOT_SEEN_UPDATED, {
notSeenUpdated: unseenNotificationsCount,
});
};
I am using default pubsub implementation. There are no errors in the console.
import { PubSub } from 'apollo-server';
import * as MESSAGE_EVENTS from './message';
import * as NOTIFICATION_EVENTS from './notification';
export const EVENTS = {
MESSAGE: MESSAGE_EVENTS,
NOTIFICATION: NOTIFICATION_EVENTS,
};
export default new PubSub();
Make sure, that you use pubsub from context of apollo server, for example:
Server:
const server = new ApolloServer({
schema: schemaWithMiddleware,
subscriptions: {
path: PATH,
...subscriptionOptions,
},
context: http => ({
http,
pubsub,
redisCache,
}),
engine: {
apiKey: ENGINE_API_KEY,
schemaTag: process.env.NODE_ENV,
},
playground: process.env.NODE_ENV === 'DEV',
tracing: process.env.NODE_ENV === 'DEV',
debug: process.env.NODE_ENV === 'DEV',
});
and example use in resolver, by context:
...
const Mutation = {
async createOrder(parent, { input }, context) {
...
try {
...
context.pubsub.publish(CHANNEL_NAME, {
newMessage: {
messageCount: 0,
},
participants,
});
dialog.lastMessage = `{ "orderID": ${parentID}, "text": "created" }`;
context.pubsub.publish(NOTIFICATION_CHANNEL_NAME, {
notification: { messageCount: 0, dialogID: dialog.id },
participants,
});
...
}
return result;
} catch (err) {
log.error(err);
return sendError(err);
}
},
};
...
It has been a while since this moment.
I have also been a struggle with pubsub not working problem.
and I would like to see your ApolloClient setup code.
I changed my configurations with regard to graphql version and client-side setup.
graphql version : 14.xx.xx -> 15.3.0
const client = new ApolloClient({
uri: 'http://localhost:8001/graphql',
cache: cache,
credentials: 'include',
link: ApolloLink.from([wsLink, httpLink])
});
I want you to clarify link order, especially about httpLink, if you use in your case, "HttpLink is a terminating Link.", according to Apollo official site.
At first, I used link order [httpLink, wsLink].
Therefore, pubsub.publish didn't work.
I hope this answer will help some of graphql users.

admin-on-rest / restClient : call a resource with no auth

I made a register page that use restClient to send a POST to /users api.
But my problem is that the only way to send a POST is to be logged first as I receive this error log from the restClient :
'Could not find stored JWT and no authentication strategy was given'
Is there a way to desactivate the authentication middleware for a specific api call ?
// registerActions.js
import { CREATE } from 'admin-on-rest'
export const USER_REGISTER = 'AOR/USER_REGISTER'
export const USER_REGISTER_LOADING = 'AOR/USER_REGISTER_LOADING'
export const USER_REGISTER_FAILURE = 'AOR/USER_REGISTER_FAILURE'
export const USER_REGISTER_SUCCESS = 'AOR/USER_REGISTER_SUCCESS'
export const userRegister = (data, basePath) => ({
type: USER_REGISTER,
payload: { data: { email: data.username, ...data } },
meta: { resource: 'users', fetch: CREATE, auth: true },
})
//registerSaga.js
import { put, takeEvery, all } from 'redux-saga/effects'
import { push } from 'react-router-redux'
import { showNotification } from 'admin-on-rest'
import {
USER_REGISTER,
USER_REGISTER_LOADING,
USER_REGISTER_SUCCESS,
USER_REGISTER_FAILURE
} from './registerActions'
function* registerSuccess() {
yield put(showNotification('Register approved'))
yield put(push('/'))
}
function* registerFailure({ error }) {
yield put(showNotification('Error: register not approved', 'warning'))
console.error(error)
}
export default function* commentSaga() {
yield all([
takeEvery(USER_REGISTER_SUCCESS, registerSuccess),
takeEvery(USER_REGISTER_FAILURE, registerFailure),
])
}
You'll probably have to make your own feathers client and explicitly bypass the call to authenticate for this specific request
You can also write a rest wrappper this will intercept the call for this particular case and bypass auth
https://marmelab.com/admin-on-rest/RestClients.html#decorating-your-rest-client-example-of-file-upload
So something like below
const restWrapper = requestHandler => (type, resource, params) => {
import { fetchUtils } from 'admin-on-rest';
if (type === 'CREATE' && resource === 'users') {
return fetchUtils.fetchJson(url, params)
.then((response) => {
const {json} = response;
return { data: json };
})
}
Eliminates the need of rewriting an entire Rest Client when you only want to override the default behaviour for a single case

Resources