sst / sst

Build modern full-stack applications on AWS

Home Page:https://sst.dev

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Cognito user identity is undefined in context when making GraphQL requests

haruki-m opened this issue · comments

I'm trying to protect my GraphQL endpoints with a Cognito user pool, but when I make requests from the frontend using AWS Amplify, the identity field in the context is undefined (and in turn cognitoIdentityId is undefined), making it impossible/difficult to identify the user who made the request. This setup used to work in the past and now it doesn't. Any help would be appreciated!

Relevant dependency versions:

  • node: 20.2.0
  • sst: 2.40.3
  • constructs: 10.3.0
  • aws-cdk-lib: 2.124.0
  • @pothos/core: ^3.38.0
  • aws-amplify / @aws-amplify: ^6.0.12

sst.config.ts:

import type { SSTConfig } from 'sst';
import { Api } from './stacks/ApiStack';
import { Auth } from './stacks/AuthStack';
import { Site } from './stacks/SiteStack';

export default {
  config(_input) {
    return { name: 'sample', region: 'us-east-1' };
  },
  stacks(app) {
    app.stack(Auth).stack(Api).stack(Site);
  },
} satisfies SSTConfig;

stacks/AuthStack.ts:

import { Cognito, type StackContext } from 'sst/constructs';

export function Auth({ stack, app }: StackContext) {
  const auth = new Cognito(stack, 'auth', { login: ['email'] });
  return { auth };
}

stacks/ApiStack.ts:

import { Api as ApiGateway, use, type StackContext } from 'sst/constructs';
import { Auth } from './AuthStack';

export function Api({ stack }: StackContext) {
  const { auth } = use(Auth);

  const api = new ApiGateway(stack, 'api', {
    authorizers: {
      cognito: {
        type: 'user_pool',
        userPool: { id: auth.userPoolId, clientIds: [auth.userPoolClientId] },
      },
    },
    routes: {
      'POST /graphql': {
        type: 'graphql',
        authorizer: 'cognito',
        function: { handler: 'packages/functions/src/graphql/graphql.handler' },
        pothos: {
          schema: 'packages/functions/src/graphql/schema.ts',
          output: 'packages/graphql/schema.graphql',
          commands: [
            'cd packages/graphql && npx @genql/cli --output ./genql --schema ./schema.graphql --esm',
          ],
        },
      },
    },
  });

  auth.attachPermissionsForAuthUsers(stack, [api]);
  return { api };
}

stacks/SiteStack.ts

import { StackContext, use, type StaticSite } from 'sst/constructs';
import { Api } from './ApiStack';
import { Auth } from './AuthStack';

export function Site({ stack, app }: StackContext) {
  const { api } = use(Api);
  const { auth } = use(Auth);

  const site = new StaticSite(stack, 'site', {
    path: 'packages/site',
    buildCommand: 'pnpm run build',
    buildOutput: 'dist',
    environment: {
      VITE_API_URL: api.url,
      VITE_USER_POOL_ID: auth.userPoolId,
      VITE_USER_POOL_CLIENT_ID: auth.userPoolClientId,
    },
  });
  return { site };
}

packages/functions/src/graphql/graphql.ts:

import { GraphQLHandler } from 'sst/node/graphql';
import { schema } from './schema';

export const handler = GraphQLHandler({
  schema,
  context: async (serverContext) => {
    const cognitoId = serverContext.context.identity?.cognitoIdentityId;

    console.log(serverContext); // serverContext.context.identity is undefined here

    return { cognitoId };
  },
});

packages/functions/src/graphql/schema.ts:

import SchemaBuilder from '@pothos/core';

const builder = new SchemaBuilder<{ Context: { cognitoId?: string } }>({});
builder.queryType({ description: 'Queries' })
builder.queryFields((t) => ({
  sampleQuery: t.field({
    type: 'String',
    resolve: async (obj, args, context) => 'sampleQuery',
  }),
}));
export const schema = builder.toSchema({})

packages/site/src/main.tsx:

import ReactDOM from 'react-dom/client';

import { Amplify } from 'aws-amplify';
import { generateClient } from "aws-amplify/api";
import { signIn } from 'aws-amplify/auth';

Amplify.configure({
  Auth: {
    Cognito: {
      userPoolId: import.meta.env.VITE_USER_POOL_ID,
      userPoolClientId: import.meta.env.VITE_USER_POOL_CLIENT_ID,
    },
  },
  API: {
    GraphQL: {
      endpoint: `${import.meta.env.VITE_API_URL}/graphql`,
      defaultAuthMode: 'userPool',
    },
  },
});

const client = generateClient();

const App = () => {
  const onClick = async () => {
    await signIn({ username: 'sample@email.com', password: 'password' });
    const response = await client.graphql({ query: 'query { sampleQuery }' });
    console.log(response);
  }
  return <button onClick={onClick}>call</button>;
};

ReactDOM.createRoot(document.getElementById('root')!).render(<App />);

Clicking on the button will sign in and make a request. Here's the serverContext output on the sst dev logs:

{
  event: { ... },
  context: {
    awsRequestId: 'some request id',
    invokedFunctionArn: 'some arn',
    getRemainingTimeInMillis: [Function: getRemainingTimeInMillis],
    identity: undefined, // this field is supposed to have { cognitoIdentityId: string }
    clientContext: undefined,
    functionName: 'some function name',
    functionVersion: '$LATEST',
    memoryLimitInMB: undefined,
    logGroupName: 'some log group name',
    logStreamName: 'some log stream name',
    callbackWaitsForEmptyEventLoop: true,
    done: [Function: done],
    fail: [Function: fail],
    succeed: [Function: succeed]
  },
  waitUntil: [Function: waitUntil],
  request: { ... },
}

I found the article that I based my code off of: here

Ok, never mind. After re-reading the article, the thing that I was looking for was actually stored in serverContext.event.requestContext.authorizer.jwt.claims.sub