Skip to content

Instantly share code, notes, and snippets.

@PascalSenn
Last active May 15, 2025 11:30
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

@apazureck
Copy link

Hi,

Thank you for sharing the manual how to do websocket authentication. We are using HC 13 and this is what I changed to get the authentication to work.

I put the static keys in options, so it is possible to change them, as well as modifying getting the auth header from the message and setting it to the http context.

I am not sure, if some changes happened in HC 14+, so there might have to be some adjustments.

Socket interceptor

using HotChocolate.AspNetCore;
using HotChocolate.AspNetCore.Subscriptions;
using HotChocolate.AspNetCore.Subscriptions.Protocols;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Websocketauthentication;

/// <summary>
/// Code from https://gist.github.com/PascalSenn/6e2a1838e44e836017ede3ce55b4fc3b
/// </summary>
public class ApolloAuthenticationSocketInterceptor : DefaultSocketSessionInterceptor
{
    private readonly IAuthenticationSchemeProvider _schemes;
    private readonly IOptions<SocketAuthenticationOptions> _options;
    private readonly ILogger<ApolloAuthenticationSocketInterceptor> _logger;

    public ApolloAuthenticationSocketInterceptor(IAuthenticationSchemeProvider schemes,
        IOptions<SocketAuthenticationOptions> options,
        ILogger<ApolloAuthenticationSocketInterceptor> logger)
    {
        _schemes = schemes;
        _options = options;
        _logger = logger;
    }

    public override async ValueTask<ConnectionStatus> OnConnectAsync(ISocketSession session,
        IOperationMessagePayload connectionInitMessage,
        CancellationToken cancellationToken = default)
    {
        _logger.LogInformation("New Websocket Connection");
        var context = session.Connection.HttpContext;
        string? authHeader = _options.Value.GetAuthHeaderFromPayload(connectionInitMessage);

        if (authHeader is null)
            return ConnectionStatus.Reject("No Authentication Header Found");

        context.Items[_options.Value.HttpContextWebsocketAuthKey] = authHeader;
        _options.Value.SetAuthHeaderOnHttpContext(context, authHeader);

        context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
        {
            OriginalPath = context.Request.Path,
            OriginalPathBase = context.Request.PathBase
        });

        var result = await context.AuthenticateAsync();
        if (!result.Succeeded)
            return ConnectionStatus.Reject("User is not authenticated");

        context.User = result.Principal;
        return ConnectionStatus.Accept();
    }
}

SocketInterceptorOptions

public delegate string? GetAuthHeaderFromMessagePayloadDelegate(IOperationMessagePayload initMessage);

public delegate void SetAuthHeaderOnHttpContextDelegate(HttpContext context, string authHeader);

public class SocketAuthenticationOptions
{
    public string HttpContextWebsocketAuthKey { get; set; } = "websocket-auth-token";

    public GetAuthHeaderFromMessagePayloadDelegate OnGetAuthHeaderFromPayload { get; set; }
    
    public SetAuthHeaderOnHttpContextDelegate OnSetAuthHeaderOnHttpContext { get; set; }
    
    public SocketAuthenticationOptions()
    {
        OnSetAuthHeaderOnHttpContext = DefaultOnSetAuthHeaderOnHttpContext;
        OnGetAuthHeaderFromPayload = DefaultOnGetAuthHeaderFromPayload;
    }

    internal string? GetAuthHeaderFromPayload(IOperationMessagePayload initMessage)
    {
        return OnGetAuthHeaderFromPayload(initMessage);
    }

    internal void SetAuthHeaderOnHttpContext(HttpContext context, string authHeader)
    {
        OnSetAuthHeaderOnHttpContext(context, authHeader);
    }
    
    private void DefaultOnSetAuthHeaderOnHttpContext (HttpContext context, string authHeader)
    {
        context.Request.Headers.Authorization = authHeader;
    }

    private string? DefaultOnGetAuthHeaderFromPayload(IOperationMessagePayload initMessage)
    {
        var payload = initMessage.As<ApolloAuthPayload>();
        if (payload is null)
            return null;
        return payload.Authorization;
    }

    private record ApolloAuthPayload(string? Authorization);
}

Authenitcation extensions

// ...
public static class AuthorizationServiceCollectionExtensions
{
    public const string WebsocketSchemeKey = "Websocket";
    
    private static void AddAuthentication(WebApplicationBuilder builder)
    {
        builder.Services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme,
            builder.Configuration.GetSection("auth"));

        builder.AddMaksAppUserInfoAccessor();

        builder.Services.AddScoped<ISocketSessionInterceptor, ApolloAuthenticationSocketInterceptor>();
        builder.Services.AddAuthentication(o =>
            {
                o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
                o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(WebsocketSchemeKey, options =>
            {
                
            })
            .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme,
                options =>
                {
                    // other Options for OAuth2 token validation etc.
                    options.ForwardDefaultSelector = SelectAuthSchema;
                });
    }
    
    private static string? SelectAuthSchema(HttpContext context)
    {
        if (CheckIfItIsWebsocketRequest(context))
        {
            return WebsocketSchemeKey;
        }

        return JwtBearerDefaults.AuthenticationScheme;
    }

    private static bool CheckIfItIsWebsocketRequest(HttpContext context)
    {
        var socketInterceptorOptions =
            context.RequestServices.GetRequiredService<IOptions<SocketAuthenticationOptions>>();
        return !context.Items.ContainsKey(socketInterceptorOptions.Value.HttpContextWebsocketAuthKey) &&
               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";
    }
    //...
}

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