clerk / javascript

Official JavaScript repository for Clerk authentication
https://clerk.com
MIT License
1.12k stars 246 forks source link

Clerk's Express helper functions don't support Http.IncomingMessage (used in apollo websocket) #4346

Open luap2703 opened 1 week ago

luap2703 commented 1 week ago

Preliminary Checks

Reproduction

n/a

Publishable key

pk_test_c21pbGluZy1tb2NjYXNpbi01NS5jbGVyay5hY2NvdW50cy5kZXYk

Description

When using clerk with Apollo Graphql's server and more precisely with the Websocket enhancement to support Subscriptions, you would normally pass the request object (handed through ctx.extra.request) to the authentication logic (the middleware or the authenticateRequest function) and then pass the authed user to the context.

This is currently not possible with any of the helper functions since they all require the Request to be an express request. But Apollo injects the request as Http.IncominMessage, without a response (since there's no Http Response in WS connection).

The only working solution we found so far is to go to the lowest available backend abstraction and verify the cookie ourselves which is quite complicated.

Expected behavior:

Allow the ClerkExpressWithAuth or authenticateRequest function to take Http.IncominMessage objects. They do already all the necessary parameters to authenticate it.

Actual behavior:

authenticateRequest and ClerkExpressWithAuth fail when trying to authenticate the IncomingMessage.

What we would like to be accomplish but what doesn't work:

 const serverCleanup = useServer(
        {
            schema,
            context: async (ctx, message, args) => {
               const state = await Clerk.clerkClient.authenticateRequest(ctx.extra.request)
               const auth = state.toAuth()

               if (!auth) {
                     throw new GraphQLError("User is not authenticated", {
                          extensions: {
                            code: "UNAUTHENTICATED",
                            http: { status: 401 },
                          },
                     });
                }

               const user = await validateAuth(auth);

               return {
                     user,
                     prisma,
               }

            },
        },
        wsServer,
    );

What we have to do although all relevant props are written onto IncomingMessage:

 const serverCleanup = useServer(
        {
            schema,
            context: async (ctx, message, args) => {
                /*  return {
                    prisma,
                };*/

                // Get the __session cookie and the Bearer token from the incoming message
                const cookies = ctx.extra.request.headers.cookie;
                const bearer = ctx.extra.request.headers.authorization;

                // Parse the session cookies string using the library cookie
                const sessionCookie =
                    cookies && cookie.parse(cookies).__session;

                const bearerToken = bearer && bearer.replace("Bearer ", "");

                // Log when session or bearer token is missing
                if (!sessionCookie) {
                    console.log("No session cookie found");
                    if (!bearerToken) {
                        console.log("No bearer token found");
                    }
                }

                // If there is no session cookie or bearer token, return null
                const token = sessionCookie || bearerToken;

                const error = new GraphQLError("User is not authenticated", {
                    extensions: {
                        code: "UNAUTHENTICATED",
                        http: { status: 401 },
                    },
                });

                if (!token) {
                    console.error("No token found");
                    throw error;
                }

                const authorizedParties = [
                    process.env.DOMAIN_NAME
                        ? process.env.DOMAIN_NAME
                        : undefined,
                    process.env.APP_DOMAIN ? process.env.APP_DOMAIN : undefined,
                ].filter((x) => x) as string[];

                const verifiedToken = await Clerk.clerkClient.verifyToken(
                    token,
                    {
                        authorizedParties,
                    },
                );

                if (!verifiedToken.sub) {
                    console.error("Token verification failed");
                    throw error;
                }

                // Now get the user
                const user = await validateAuth({
                    userId: verifiedToken.sub,
                    orgId: verifiedToken.org_id,
                    orgPermissions: verifiedToken.org_permissions,
                    orgRole: verifiedToken.org_role,
                    orgSlug: verifiedToken.org_slug,
                    sessionId: verifiedToken.sid,
                }) // Custom logic to get the user from our db

                return {
                    user,
                    prisma,
                };
            },
        },
        wsServer,
    );

Environment

Using:

NodeJS
Express
Apollo Graphql (Server)
graphql-ws
wobsoriano commented 2 days ago

Hi, apologies for the inconvience. It's not documented but we export the custom authenticateRequest we use that converts Express's request to Web Request.

You can use it like this:

import { clerkClient, authenticateRequest } from '@clerk/express'

const serverCleanup = useServer(
    {
        schema,
        context: async (ctx, message, args) => {
            const state = await authenticateRequest({
              clerkClient,
              request: ctx.extra.request,
              // optional
              options: {
                authorizedParties
              }
            })
            const auth = state.toAuth()
            // other code
        },
    },
    wsServer,
)
luap2703 commented 2 days ago

Hi @wobsoriano ,

thanks for the response but as mentioned it's not typesafe (+ throws during runtime, at least when imported from the node sdk):

Screenshot 2024-10-22 at 20 12 16
wobsoriano commented 2 days ago

Hi @luap2703, thanks for replying quick. What's the runtime error you're experiencing?

For the meantime, a quick workaround is to use this internal function as a basis to convert IncomingMessage to Request.

const request = incomingMessageToRequest(ctx.extra.request)
const state = await clerkClient.authenticateRequest(request)

and you're right, the request in the custom authenticateRequest is an Express Request type