ChilliCream / graphql-platform

Welcome to the home of the Hot Chocolate GraphQL server for .NET, the Strawberry Shake GraphQL client for .NET and Banana Cake Pop the awesome Monaco based GraphQL IDE.
https://chillicream.com
MIT License
5.16k stars 735 forks source link

Strawberry Shake WebSocket configuration doesn't have Options field to configure authorization header #6052

Open fuji97 opened 1 year ago

fuji97 commented 1 year ago

Is there an existing issue for this?

Product

Strawberry Shake

Describe the bug

In the Authentication section of the documentation si described that, in order to configure the authentication header for WebSockets, I can use the SetRequestHeader() method under Options

services
    .AddConferenceClient()
    .ConfigureWebSocketClient(client =>
    {
        client.Uri = new Uri("ws://localhost:" + port + "/graphql");
        client.Socket.Options.SetRequestHeader("Authorization", "Bearer ...");
    });

But under client.Socket there is no Options field.

Steps to reproduce

  1. Create a sample .NET 7 Console project.
  2. Import the StrawberryShake.Server and the Microsoft.Extensions.Hosting NuGet packages.
  3. Follow the get started guide until ConferenceClient is created
  4. Try to setup the headers in Program.cs
    
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices((hostContext, services) => {
        services.AddConferenceClient()
            .ConfigureWebSocketClient(client => {
                client.Socket.Options.SetRequestHeader("Authorization", "Bearer ...");
            });
    })
    .Build();

host.Run();



### Relevant log output

_No response_

### Additional Context?

_No response_

### Version

13.0.5
Jemy191 commented 9 months ago

@fuji97 Have you found a wait to send the auth token? I tried ISocketConnectionInterceptor, but it is called after the connect call.

fuji97 commented 9 months ago

@TheJemy191 I fixed it by using the following custom ISocketConnectionInterceptor

public class WebApiSocketRequestInterceptor : ISocketConnectionInterceptor {
    private readonly IAccessTokenProvider _accessTokenProvider;
    private readonly IConfiguration _configuration;
    private readonly ILogger<WebApiSocketRequestInterceptor> _logger;
    public WebApiSocketRequestInterceptor(IAccessTokenProvider accessTokenProvider, IConfiguration configuration, ILogger<WebApiSocketRequestInterceptor> logger) {
        _accessTokenProvider = accessTokenProvider;
        _configuration = configuration;
        _logger = logger;
    }

    public async ValueTask<object?> CreateConnectionInitPayload(ISocketProtocol protocol, CancellationToken cancellationToken) {
        try {
            var scopes = _configuration.GetSection("Scopes:Admin").Get<string[]>();
            var accessTokenResult = await _accessTokenProvider.RequestAccessToken(new AccessTokenRequestOptions() {
                Scopes = scopes
            });
            if (!accessTokenResult.TryGetToken(out var token)) {
                throw new InvalidOperationException("Could not get access token.");
            }
            return new Dictionary<string, string> { ["authToken"] = token.Value };
        }
        catch (Exception e) {
            _logger.LogError(e, "Error while creating connection init payload");
            throw;
        }
    }
}

And it the Program.cs:

builder.Services.AddTransient<WebApiSocketRequestInterceptor>();

/// Other code

builder.Services.AddGraphQLClient()
    .ConfigureHttpClient((services, client) => {
        client.BaseAddress = new Uri(url);
    }, b => b.AddHttpMessageHandler<WebApiAuthorizationMessageHandler>())
    .ConfigureWebSocketClient((services, client) => {
        var logger = services.GetRequiredService<ILogger<Program>>();
        try {
            var interceptor = services.CreateScope().ServiceProvider.GetRequiredService<WebApiSocketRequestInterceptor>();
            client.Uri = new Uri(url);
            client.ConnectionInterceptor = interceptor;
        }
        catch (Exception e) {
            logger.LogError(e, "Error while creating connection init payload");
            throw;
        }
    services.CreateScope().ServiceProvider.GetRequiredService<WebApiSocketRequestInterceptor>();
    });

This is directly copied from my Blazor WASM project, so you should change the settings to works with yours (i.e. scopes and the token provider) but for the rest it should works. Let me know if it works

Jemy191 commented 9 months ago

That won't work. Event with your change WebApiSocketRequestInterceptor is called after the first connection and the server never receive any token.

Jemy191 commented 9 months ago

Found the problem. I needed to add .AddSocketSessionInterceptor<SocketAuthorizationInterceptor>() when configuring HotChocolate. This is what receive the initial payload from ISocketConnectionInterceptor

michaelstaib commented 8 months ago

You need to put the token on the init request ....

The authorization header is not supported in browser when using websocket.

I will post how to do it later.

michaelstaib commented 8 months ago

@TheJemy191 exactly ... on the server you need SocketAuthorizationInterceptor we used to have a full demo ... but we need to have another look at that.

cmeeren commented 7 months ago

The workaround above does not seem to be able to add headers. How can that be done?

(Our API, including the WS endpoint, requires a session ID in a cookie, with an Authorization header available for simpler smoke testing, and it's the Authorization header I want to add now.)