Skip to content

Instantly share code, notes, and snippets.

@PascalSenn
Last active March 31, 2023 12:20
Show Gist options
  • Save PascalSenn/43fedfbc1bc96692d99263a9da2d9ac4 to your computer and use it in GitHub Desktop.
Save PascalSenn/43fedfbc1bc96692d99263a9da2d9ac4 to your computer and use it in GitHub Desktop.
Authentication Apollo Client - How To

The problem with authentication of web sockets is that you can only authenticate a http request once against a authentication scheme. After that the authentication is cached and will always yield the same result.

A web socket connection starts over HTTP. A HTTP request with the Upgrade header is send to the back end. This request is then "upgraded" into a web socket that runs over the web socket pipeline. The main issue with web sockets is that you cannot set additional headers with this initial HTTP header. Therefore the authentication will fail and the request will be unauthenticated. The HTTP context of the initial request will last as long as the web socket connection is running.

The trick we do is that we add a stub authentication scheme:

AddJwtBearer("Websockets", ctx => { })

We need to authenticate the request against this scheme. This way we can then later authenticate against our default scheme. We can do this when we use the ForwardDefaultSelector on our default scheme to forward to the stub ("Websockets") scheme.

We can use this code snipped:

 if (!context.Items.ContainsKey(AuthenticationSocketInterceptor.HTTP_CONTEXT_WEBSOCKET_AUTH_KEY) && // <- do we need later
        context.Request.Headers.TryGetValue("Upgrade", out var value) && // <-- checking if it is a Upgrade request
        value.Count > 0 && 
        value[0] is string stringValue && 
        stringValue == "websocket")
 {
   return "Websockets";
 }
 return "YourDefaultScheme";

Now the request is Unauthenticated and the web socket is going to be established. Sweet. We need to intercept the connection request. We can do this by specifying a SocketConnectionInterceptor. services.AddSingleton<ISocketConnectionInterceptor<HttpContext>, AuthenticationSocketInterceptor>();

The socket interceptor basically does the same thing as the authentication middle ware of .net core itself. Parts of the code can actually be the same. I made a gist for it because I do believe that this would make the message a bit too long 😄 The idea is as follows. We try to get the key of the connection_init request of apollo. WEBOCKET_PAYLOAD_AUTH_KEY This is the key you use here

const wsClient = new SubscriptionClient(`ws://localhost:5000/`, {
    reconnect: true,
    connectionParams: {
        authToken: user.authToken, //WEBOCKET_PAYLOAD_AUTH_KEY = "authToken
    },"
});

We take this key out of the properties in the back end and store it in on the HTTP Context: HTTP_CONTEXT_WEBSOCKET_AUTH_KEY We then authenticate again, but this time against the default schema. You can see all the details in the gist below https://gist.github.com/PascalSenn/6e2a1838e44e836017ede3ce55b4fc3b

And now comes the third an last part. We first forwarded the Upgrade request from the default scheme on to a stub scheme, then intercepted the connection_init message and authenticated again but this time against the default scheme. Now the default scheme has only have to understand that it should take the token out of the HTTPContext rather than out of the Headers.

So lets have a look again at the forwarder first. The first time we skipped the default scheme because it hat the Upgrade header The HTTP Context is still the same, so it is still a Upgrade request. But now the context contains the token. First line:

 if (!context.Items.ContainsKey(AuthenticationSocketInterceptor.HTTP_CONTEXT_WEBSOCKET_AUTH_KEY) && // <- now we need it :)
        context.Request.Headers.TryGetValue("Upgrade", out var value) && 
        value.Count > 0 && 
        value[0] is string stringValue && 
        stringValue == "websocket")
 {
   return "Websockets";
 }
 return "YourDefaultScheme";

Sweet. The default scheme does not know where it finds the token though. There for we need to tell the scheme how to get the token with the TokenRetriever

options.TokenRetriever = new Func<HttpRequest, string>(req =>
   {
      if (req.HttpContext.Items.TryGetValue(
              AuthenticationSocketInterceptor.HTTP_CONTEXT_WEBSOCKET_AUTH_KEY,
              out object token) &&
          token is string stringToken)
         {
           return stringToken;
         }
         var fromHeader = TokenRetrieval.FromAuthorizationHeader();
         var fromQuery = TokenRetrieval.FromQueryString();  
          return fromHeader(req) ?? fromQuery(req);
     });

Now you are all set 😄 eaaasy 😄 Keep in mind though: The token does not expire or is refreshed, the web socket could be open for days. You have to close the web socket from the back end for security reasons i guess

As an alternative you could use a query string to authenticate 🙂

You can have a look at the whole Authentication configuration here:

https://gist.github.com/PascalSenn/a3204e7bb880705a178048a43ee2147c

Interceptor

https://gist.github.com/PascalSenn/6e2a1838e44e836017ede3ce55b4fc3b

And lastly all of the stuff I've written archived under:

https://gist.github.com/PascalSenn/43fedfbc1bc96692d99263a9da2d9ac4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment