Cookie-based Authentication + Apollo React Client Results in SSL Handshake Failure
February 29, 2020
I recently was given a task to add user authentication to our website. It was decided to implement the authentication logic using httpOnly cookie. Client-side the browser will take care of passing the cookie of course so the only thing left was to pass the cookie header server-side. Headers can be passed to Apollo client in the constructor. The diagram below displays our technology stack:
next-with-apollo
is a handy npm package which makes headers available from Express.js (which serves server-side pages), thus:
import withApollo from "next-with-apollo"
import ApolloClient, { InMemoryCache } from "apollo-boost"
import renderFn from "./render"
export default withApollo(
({ initialState, headers }) => {
return new ApolloClient({
uri: "https://example.com/graphql",
cache: new InMemoryCache().restore(initialState || {}),
headers, // <- headers are passed here
})
},
{
render: renderFn,
}
)
After I implemented the logic I was quite happy and went for a break to practice my Yo-Yo skills (Yo-Yo-ing has become trendy in our office).
I returned to my work station in order to finish the task: polish the server logic which handles authentication. Eventually I was done.
Everything worked perfectly in the development environment.
I had another Yo-Yo practice. The only thing left was to deploy the logic to our staging environment. When I checked the website after deployment there was an error:
ApolloError: Network error: request to https://example.com/graphql/ failed, reason: write EPROTO 140152723232576:error:14094410:SSL routines:ssl3readbytes:sslv3 alert handshake failure:../deps/openssl/openssl/ssl/record/reclayers3.c:1544:SSL alert number 40
Since I added a lot of logic in the task including CORS, cookies and a bunch of graphql resolvers I had to minimize the number of possible causes of bugs.
At first I suspected that CORS is culprit, that somehow it blocks browser requests. This was not the case however. After removing authentication logic piece by piece in order to identify the problem I discovered the issue was passing headers to the Apollo client as can be seen in the code excerpt above, specifically the host
header which was something like bla.bla.bla.elasticbeanstalk.com
.
“But what does that have to do with the SSL handshake error?”, you ask. Well, it turns out that host
header is quite important and it may be used in Server Name Indication (SNI) extension to TLS protocol. Here’s the gist:
Serving multiple domains (virtual hosts) per a given IP address is called name-based virtual hosting. In name-based hosting you tell the server the virtual host you’re interested in via the host header. Such approach wouldn’t work out of the box because headers are not sent to the server before SSL handshake has occurred. But in order for the SSL handshake to occur the server must somehow know for which domain to serve the SSL certificate. In order to solve the chicken-and-egg conundrum SNI was invented: at the beginning of SSL handshake you tell the server which domain you’re interested in, usually via the
servername
option. Thus SNI allows to use SSL with name-based virtual hosting which is significantly cheaper than having a dedicated IP address per domain. Different clients implement SNI differently, specifically Apollo Client uses fetch API-based utilitynode-fetch
which in its turn uses Node.jshttps
module. If the host header is set to some value thenhttps
module assigns the value toservername
, otherwise the hostname is assigned toservername
.
So because our AWS setup was configured to use SNI for the website and I was explicitly passing the host header bla.bla.bla.elasticbeanstalk.com
, the servername
was set to it and as a result SNI failed because our SSL certificate was rather for example.com
.
In order to fix the bug I found out that you can simply pass only the header you need in Apollo client constructor, so I only passed the cookie header:
import withApollo from "next-with-apollo"
import ApolloClient, { InMemoryCache } from "apollo-boost"
import renderFn from "./render"
export default withApollo(
({ initialState, headers }) => {
return new ApolloClient({
uri: "https://example.com/graphql",
cache: new InMemoryCache().restore(initialState || {}),
headers: {
cookie: headers?.cookie,
},
})
},
{
render: renderFn,
}
)
In case you’re wondering how the question mark in this line cookie: headers?.cookie
is valid Javascript check out the optional chaining Babel.js plugin, it’s awesome!