Clerk's Express helper functions don't support Http.IncomingMessage (used in apollo websocket)
Closed this issue · 5 comments
Preliminary Checks
-
I have reviewed the documentation: https://clerk.com/docs
-
I have searched for existing issues: https://github.com/clerk/javascript/issues
-
I have not already reached out to Clerk support via email or Discord (if you have, no need to open an issue here)
-
This issue is not a question, general help request, or anything other than a bug report directly related to Clerk. Please ask questions in our Discord community: https://clerk.com/discord.
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
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,
)
Hi @wobsoriano ,
thanks for the response but as mentioned it's not typesafe (+ throws during runtime, at least when imported from the node sdk):
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
@luap2703 Hey! Just checking if the solution above worked for you 👍
Hi @wobsoriano ,
thank you for doublechecking!
Works!
We used the authenticateRequest on the higher level sdk node package before where it was failing, but seems to be smooth with this one!
Closing the thread!