Cognito Post Confirmation Trigger adminAddUserToGroup: "Invalid lambda function output : Invalid JSON" - aws-lambda

I set the following lambda function as the post-confirmation trigger in Cognito. Anyone know why I'm receiving "Invalid lambda function output : Invalid JSON"?
I'm getting log from "console.log("params", params)" in CloudWatch but not from "console.log("inside adminAddUserToGroup", params)".
const AWS = require('aws-sdk');
AWS.config.region = 'us-east-1';
const lambda = new AWS.Lambda();
type TriggerEvent = {
version: string,
region: string,
userPoolId: string,
userName: string,
callerContext: {
awsSdkVersion: string,
clientId: string
},
triggerSource: string,
request: {
userAttributes: {
sub: string,
'cognito:email_alias': string,
'cognito:user_status': string,
birthdate: string,
email_verified: string,
gender: string,
preferred_username: string,
email: string
}
},
response: {}
}
exports.handler = async (event:TriggerEvent, context: any, callback: any) => {
var cognitoIdentityServiceProvider = new AWS.CognitoIdentityServiceProvider({ apiVersion: '2016-04-18' });
console.log("event", event);
var params = {
GroupName: 'customers',
UserPoolId: event.userPoolId,
Username: event.userName
};
console.log("params", params)
try {
cognitoIdentityServiceProvider.adminAddUserToGroup(params, function(err: any, data: any) {
console.log("inside adminAddUserToGroup", params)
if (err) {
console.log(err, err.Stack);
context.done(err);
}
context.done();
});
} catch (err) {
console.log("Add user to group catch err", err);
context.fail("Add user to group catch err");
}
}
Thanks.

There are two different problems in the code:
Fixing the missing output of console.log("inside adminAddUserToGroup", params):
You should await on the adminAddUserToGroup(..).promise(), otherwise the lambda will proceed and complete before the adminAddUserToGroup fully executes. You can check this by adding a console.log("Lambda completed") after the try/catch block and see that it does show.
Fix it with:
await cognitoIdentityServiceProvider.adminAddUserToGroup(params, function(err: any, data: any) {
console.log("inside adminAddUserToGroup", params)
if (err) {
console.log(err, err.Stack);
context.done(err);
}
context.done();
}).promise();
Fixing the "Invalid lambda function output : Invalid JSON" response:
You need to return the event, usage of context.done() is deprecated and, since you are using an async handler, you should use return or throw, for success and failure respectively, as per the AWS documentation on Async handler for Typescript.
Fix it by replacing context.done(); with return event;.

Related

Context within express-graphql returns a function

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);
});

Custom decorator not being invoked on socket message handler (NestJS, SocketIO)

In app.gateway.ts, I have the following event handler:
#SubscribeMessage<TClientToServerEvents>('event')
async handleRefreshCheck(
#SocketContext() ctx: SocketContextDTO,
#ConnectedSocket() socket: TSocket,
) {
this.logger.log({
message: 'Socket received event',
ctx,
meta: {
socketData: {
socketID: socket.id,
handshake: socket.handshake,
user: socket.data,
},
},
});
try {
await this.service.logic(ctx);
} catch (err) {
this.logger.error({
message: err.message,
ctx,
});
throw new InternalServerErrorException();
}
}
socket-context.decorator.ts
export const SocketContext = createParamDecorator(
(_: unknown, ctx: ExecutionContext): SocketContextDTO => {
console.log('INVOKED'); // !! THIS LOG STATEMENT IS NOT PRINTING
const socket = ctx.switchToWs().getClient();
const socketContext = createSocketContextDTO(socket);
return socketContext;
},
);
I can't figure out why the #SocketContext() decorator isn't being invoked. The ConnectedSocket() decorator returns with no issues.
This is a recent development after refactoring other areas of the codebase, but the socket logic hasn't changed.
Thanks in advance for any suggestions!
I solved this by assigning the ctx variable in the body of the function. It's strange that I got away with using it before, but I think it has to do with createParamDecorator being intended as an HTTP route decorator constructor.
New code:
#SubscribeMessage<TClientToServerEvents>('event')
async handleRefreshCheck(
#ConnectedSocket() socket: TSocket,
) {
const ctx = createSocketContextDTO(socket);
// ...
}

Supabase third party oAuth providers are returning null?

I'm trying to implement Facebook, Google and Twitter authentication. So far, I've set up the apps within the respective developer platforms, added those keys/secrets to my Supabase console, and created this graphql resolver:
/* eslint-disable #typescript-eslint/explicit-module-boundary-types */
import camelcaseKeys from 'camelcase-keys';
import { supabase } from 'lib/supabaseClient';
import { LoginInput, Provider } from 'generated/types';
import { Provider as SupabaseProvider } from '#supabase/supabase-js';
import Context from '../../context';
import { User } from '#supabase/supabase-js';
export default async function login(
_: any,
{ input }: { input: LoginInput },
{ res, req }: Context
): Promise<any> {
const { provider } = input;
// base level error object
const errorObject = {
__typename: 'AuthError',
};
// return error object if no provider is given
if (!provider) {
return {
...errorObject,
message: 'Must include provider',
};
}
try {
const { user, session, error } = await supabase.auth.signIn({
// provider can be 'github', 'google', 'gitlab', or 'bitbucket'
provider: 'facebook',
});
console.log({ user });
console.log({ session });
console.log({ error });
if (error) {
return {
...errorObject,
message: error.message,
};
}
const response = camelcaseKeys(user as User, { deep: true });
return {
__typename: 'LoginSuccess',
accessToken: session?.access_token,
refreshToken: session?.refresh_token,
...response,
};
} catch (error) {
return {
...errorObject,
message: error.message,
};
}
}
I have three console logs set up directly underneath the signIn() function, all of which are returning null.
I can also go directly to https://<your-ref>.supabase.co/auth/v1/authorize?provider=<provider> and auth works correctly, so it appears to have been narrowed down specifically to the signIn() function. What would cause the response to return null values?
This is happening because these values are not populated until after the redirect from the OAuth server takes place. If you look at the internal code of supabase/gotrue-js you'll see null being returned explicitly.
private _handleProviderSignIn(
provider: Provider,
options: {
redirectTo?: string
scopes?: string
} = {}
) {
const url: string = this.api.getUrlForProvider(provider, {
redirectTo: options.redirectTo,
scopes: options.scopes,
})
try {
// try to open on the browser
if (isBrowser()) {
window.location.href = url
}
return { provider, url, data: null, session: null, user: null, error: null }
} catch (error) {
// fallback to returning the URL
if (!!url) return { provider, url, data: null, session: null, user: null, error: null }
return { data: null, user: null, session: null, error }
}
}
The flow is something like this:
Call `supabase.auth.signIn({ provider: 'github' })
User is sent to Github.com where they will be prompted to allow/deny your app access to their data
If they allow your app access, Github.com redirects back to your app
Now, through some Supabase magic, you will have access to the session, user, etc. data

throw a descriptive error with graphql and apollo

Consider the following class:
// entity/Account.ts
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity, Index, CreateDateColumn, UpdateDateColumn } from 'typeorm'
import { Field, Int, ObjectType } from 'type-graphql'
#ObjectType()
#Entity()
export class Account extends BaseEntity {
#Field(() => Int)
#PrimaryGeneratedColumn()
id: number
#Field()
#Column({ length: 50, unique: true })
#Index({ unique: true })
accountIdentifier: string
#Field({ nullable: true })
#Column({ length: 100 })
name?: string
}
With it's corresponding resolver:
// AccountResolver.ts
#Resolver()
export class AccountResolver {
#Mutation(() => Account)
async addAccount(#Arg('options', () => AccountInput) options: AccountInput) {
try {
// if (!options.accountIdentifier) {
// throw new Error(`Failed adding account: the accountIdentifier is missing`)
// }
return await Account.create(options).save()
} catch (error) {
if (error.message.includes('Cannot insert duplicate key')) {
throw new Error(
`Failed adding account: the account already exists. ${error}`
)
} else {
throw new Error(`Failed adding account: ${error}`)
}
}
}
}
Jest test file
// AccountResolver.test.ts
describe('the addAccount Mutation', () => {
it('should throw an error when the accountIdentifier is missing', async () => {
await expect(
client.mutate({
mutation: gql`
mutation {
addAccount(
options: {
name: "James Bond"
userName: "James.Bond#contoso.com"
}
) {
accountIdentifier
}
}
`,
})
).rejects.toThrowError('the accountIdentifier is missing')
})
The field accountIdentifier is mandatory and should throw a descriptive error message when it's missing in the request. However, the error thrown is:
"Network error: Response not successful: Received status code 400"
What is the correct way to modify the error message? I looked at type-graphql with the class-validators and made sure that validate: true is set but it doesn't give a descriptive error.
EDIT
After checking the graphql playground, it does show the correct error message by default. The only question remaining is how write the jest test so it can read this message:
{
"error": {
"errors": [
{
"message": "Field AccountInput.accountIdentifier of required type String! was not provided.",
Thank you for any help you could give me.
The ApolloError returned by your client wraps both the errors returned in the response and any network errors encountered while executing the request. The former is accessible under the graphQLErrors property, the latter under the networkError property. Instea dof using toThrowError, you should use toMatchObject instead:
const expectedError = {
graphQLErrors: [{ message: 'the accountIdentifier is missing' }]
}
await expect(client.mutate(...)).rejects.toMatchObject(expectedError)
However, I would suggest avoiding using Apollo Client for testing. Instead, you can execute operations directly against your schema.
import { buildSchema } from 'type-graphql'
import { graphql } from 'graphql'
const schema = await buildSchema({
resolvers: [...],
})
const query = '{ someField }'
const context = {}
const variables = {}
const { data, errors } = await graphql(schema, query, {}, context, variables)

Apollo GraphQL server; setting context to handle requests triggered by a fired subscription

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,
},
}));

Resources