How can I authenticate MQTT with OAuth?

ofgirichardsonb
ofgirichardsonb Member Posts: 7
edited September 2022 in PubSub+ Event Broker #1

There is very little documentation on how to accomplish this, and no working code sample that I can find. I am using the MQTTNet library with the following code:

protected override async Task OnInitializedAsync()
  {
    var tokenRequest = await TokenProvider.RequestAccessToken();
    if (!tokenRequest.TryGetToken(out var accessToken))
      throw new Exception("Failed to acquire access token");
    var user = (await AuthenticationState).User.FindFirst("email")?.Value ?? throw new Exception("Not authenticated?");
    _mqttClient = new MqttFactory().CreateMqttClient();
    var options = new MqttClientOptionsBuilder()
      .WithWebSocketServer("mr-connection-xxxxxxxxx.messaging.solace.cloud:8443")
      .WithTls()
      .WithCredentials(user, accessToken.Value);
    await _mqttClient.ConnectAsync(options.Build());
  }

I can from the stats that connection attempts are being made, but I can't see what happens to it after that. I get a "bad username or password" response from the broker, but the stats don't reflect: does or doesn't have username, does or doesn't have groups.

Surely this is a simple matter. I have an access token. I have a username to validate against. I also note that I've tried variations on the password field, such as OAUTH~provider~<access_token> to no avail. I'd really like to get this working over MQTT from the SPA instead of indirectly accessing the broker over TCPS via a REST call. Can someone please point me to a code sample of a working MQTT connection using OAuth authentication? Please note that suggestions to simply use the username and password given to me for MQTT are not going to work in this environment. OAuth is the only protocol which will allow me to securely access the broker without exposing credentials.

Best Answer

  • Tamimi
    Tamimi Member, Administrator, Employee Posts: 491 admin
    #2 Answer ✓

    Hey @ofgirichardsonb thanks for your feedback on this thread! I'm sure it will be helpful for anyone wanting to implement this. I have formatted your answer with code formatting for the snippet as well å:)

Answers

  • amackenzie
    amackenzie Member, Employee Posts: 260 Solace Employee

    Hello @ofgirichardsonb ,

    I have not actually run the code in this tutorial, so forgive me if it's not what you are looking for but have you seen this:

    https://solace.com/blog/how-to-set-up-solace-pubsub-event-broker-with-oauth-for-mqtt-against-keycloak/

  • ofgirichardsonb
    ofgirichardsonb Member Posts: 7

    Unfortunately that guide only covers the back-end setup, and doesn't provide any concrete examples of connection code. I have, however, completed the setup as per the guide.

  • mstobo
    mstobo Member, Employee Posts: 22 Solace Employee
    edited September 2022 #5

    There is a newer version of that tutorial using openid-connect (OIDC):


    Are you using OpenID Connect and JWTs, or are you doing token introspection with an opaque token?

  • ofgirichardsonb
    ofgirichardsonb Member Posts: 7

    Thanks for the updated article, but I'm still not having any joy... I am trying to run the perfsdk_c utility the blog article refers to, but a) it's not clear which of id_token or access_token are required, and b) neither of them work anyway. I still see nothing in the stats, except that there are tokens received that apparently drop in a black hole; they do not report any error, nor do they report success. How can I see some kind of indication what the broker is doing with those tokens? Does no one have a working .NET code sample? Preferably MQTT/OAuth2, but even SMF OAuth2 would be fine.

  • ofgirichardsonb
    ofgirichardsonb Member Posts: 7
    edited September 2022 #7

    I attempted the SMF route, and ended up with the following:

      protected IActionResult PublishEvent(object @event, string topic)
      {
        try
        {
          if (_currentUser is ClaimsPrincipal { Identity.IsAuthenticated: true } principal)
          {
            _sessionProperties.AuthenticationScheme = AuthenticationSchemes.OAUTH2;
            _sessionProperties.OAuth2AccessToken = _token.Value ??= principal.FindFirstValue("access_token");
            _sessionProperties.UserName = null;
            _sessionProperties.Password = null;
          }
          using var session = _context.CreateSession(_sessionProperties, null, null);
          var returnCode = session.Connect();
          if (returnCode != ReturnCode.SOLCLIENT_OK)
            throw new Exception($"Failed to connect to event broker with return code {returnCode}");
          var destination = ContextFactory.Instance.CreateTopic(topic);
          var message = ContextFactory.Instance.CreateMessage();
          message.Destination = destination;
          message.ApplicationMessageType = TypeMapper.GetTypeName(@event.GetType());
          message.BinaryAttachment = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(@event));
          returnCode = session.Send(message);
          if (returnCode != ReturnCode.SOLCLIENT_OK)
            throw new Exception($"Publish failed with return code {returnCode}");
          return Ok(new ApiResponse { Success = true });
        }
        catch (Exception ex)
        {
          return BadRequest(new ApiResponse
            { Success = false, Error = ex.Message, StackTrace = ex.StackTrace });
        }
      }
    

    I have filled out all of the session properties correctly as seen in the debugger. I have created a pubsub+ authorization group, and the access token contains a permissions array with pubsub+. I have set the Authorization Group property to be "permissions", and the username to be "email". The email is not sent in the access_token, but it is available on the userinfo endpoint (and the discovery document is correctly configured). I can probably get the email in the access token if necessary. I've opened a support ticket now, but I'll update this thread if I find any resolution that works in .NET.

  • ofgirichardsonb
    ofgirichardsonb Member Posts: 7
    edited September 2022 #8

    After working with the Solace support team, I was able to come up with the following working code:

    {
      private ClaimsPrincipal User;
      private IMqttClient _mqttClient;
    
      [CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
    
      protected override async Task OnInitializedAsync()
      {
        const string cacheKey = "Microsoft.AspNetCore.Components.WebAssembly.Authentication.CachedAuthSettings";
        var authSettingsRAW = await JSRuntime.InvokeAsync<string>("sessionStorage.getItem", new[] { cacheKey });
        if (authSettingsRAW == null) return;
        var authSettings = JsonConvert.DeserializeObject<CachedAuthSettings>(authSettingsRAW);
        if (authSettings == null) return;
        var userRAW = await JSRuntime.InvokeAsync<string>("sessionStorage.getItem", new[] { authSettings.OIDCUserKey });
        if (userRAW == null) return;
        var user = JsonConvert.DeserializeObject<UserData>(userRAW);
        if (user == null) return;
        var request = await TokenProvider.RequestAccessToken();
        var response = request.TryGetToken(out var accessToken);
        if (!response) return;
        var factory = new MqttFactory();
        _mqttClient = factory.CreateMqttClient();
        var settings = new MqttClientOptionsBuilder()
          .WithClientId(Guid.NewGuid().ToString())
          .WithConnectionUri("wss://mr-connection-xxxxxxxxxxx.messaging.solace.cloud:8443")
          .WithCredentials("ignoreme_unless_validating", $"OAUTH~MyOAuthProfile~{user.access_token}")
          .Build();
        Console.WriteLine(user.id_token);
        Console.WriteLine(user.access_token);
        await _mqttClient.ConnectAsync(settings);
        var message = factory.CreateApplicationMessageBuilder()
          .WithTopic("my-mqtt-topic")
          .WithPayload(Encoding.UTF8.GetBytes(@"{""Id"": 1234, ""Text"": ""Hello from Blazor WASM!""}"))
          .Build();
        await _mqttClient.PublishAsync(message);
      }
    
      class UserData
      {
        public string id_token;
        public string access_token;
        public string expires_at;
      }
    
      class CachedAuthSettings
      {
        public string? authority { get; set; }
        public string? metadataUrl { get; set; }
        public string? client_id { get; set; }
        public string[]? defaultScopes { get; set; }
        public string? redirect_uri { get; set; }
        public string? post_logout_redirect_uri { get; set; }
        public string? response_type { get; set; }
        public string? response_mode { get; set; }
        public string? scope { get; set; }
    
        public string OIDCUserKey => $"oidc.user:{authority}:{client_id}";
      }
    }
    

    Hope this helps somebody! This is working from Blazor WASM. Thanks to the Solace team for working through this with me. Note that the code above is written with the broker in Resource Server role. If it were in Client mode, you would send id_token instead.

  • Tamimi
    Tamimi Member, Administrator, Employee Posts: 491 admin
    #9 Answer ✓

    Hey @ofgirichardsonb thanks for your feedback on this thread! I'm sure it will be helpful for anyone wanting to implement this. I have formatted your answer with code formatting for the snippet as well å:)

  • Aaron
    Aaron Member, Administrator, Moderator, Employee Posts: 508 admin

    Hi @ofgirichardsonb, resurrecting an old thread!

    I'm about to start down the path of trying to setup a broker with OAuth2 for a customer demo, gotta config an authorization server and all that. Customer was specifically asking about troubleshooting connection issues. In your dealing with Support team, did they point you to any logs or commands to run to help determine why you were having such a difficult time connecting? Any tips or tricks appreciated..! 🙏🏼