diff --git a/scenarios/jwt_authentication/README.md b/scenarios/jwt_authentication/README.md new file mode 100644 index 0000000..065f71f --- /dev/null +++ b/scenarios/jwt_authentication/README.md @@ -0,0 +1,103 @@ +# :point_right: JWT Authentication to Event Grid + +| [Create the Client Certificate](#lock-create-the-client-certificate) | [Configure Event Grid Namespaces](#triangular_ruler-configure-event-grid-namespaces) | [Configure Mosquitto](#fly-configure-mosquitto) | [Run the Sample](#game_die-run-the-sample) | + +This scenario showcases how to authenticate to Azure Event Grid via JWT authentication using MQTT 5. This scenario is identical to `getting_started` in functionality. + +The sample provides step by step instructions on how to perform following tasks: + +- Create the resources including client, topic spaces, permission bindings +- Use $all client group, which is the default client group with all the clients in a namespace, to authorize publish and subscribe access in permission bindings +- Create a custom role assignment on the Azure Portal to access Event Grid via Json Web Token (JWT) authentication. +- Create a JWT, which is used to authenticate to Event Grid. +- Connect with MQTT 5.0.0 + - Configure connection settings such as KeepAlive and CleanSession +- Publish messages to a topic +- Subscribe to a topic to receive messages + +To keep the scenario simple, a single client called "sample_client" publishes and subscribes to MQTT messages on topics shown in the table. + +|Client|Role|Operation|Topic/Topic Filter| +|------|----|---------|------------------| +|sample_client|publisher|publish|sample/topic1| +|sample_client|subscriber|subscribe|sample/+| + +## Prerequisites +This sample involves configuring Event Grid per the specifications in [getting_started](../getting_started). If that sample has not already been set up and run, it should be done before moving onto this one. + +## :lock: Configure the Json Web Token and AAD Role Assignments + +1. Modify the following JSON snippet by adding an Azure subscription Id: + +```json +{ + "properties": { + "roleName": "Event Grid Pub-Sub", + "description": "communicate with Event Grid.", + "assignableScopes": [ + "/subscriptions/" + ], + "permissions": [ + { + "actions": [], + "notActions": [], + "dataActions": [ + "Microsoft.EventGrid/*" + ], + "notDataActions": [] + } + ] + } +} +``` +2. Copy the modified snippet and save it locally. +3. In the Azure portal, go to your Resource Group that contains Event Grid and open the Access control (IAM) page. +4. Click Add and then click Add custom role. This opens the custom roles editor. +5. On the `Basics` tab, select `Start from JSON`, and upload the modified JSON file you saved locally. +6. Select the `Review and Create` tab and then `Create`. +7. **NOTE:** It is possible that your Azure account may not have room for more custom role assignments. In this instance the current workaround is to create a free Azure account and complete this process while logged in from there. + +## :triangular_ruler: Configure Event Grid Namespaces (Skip if [getting_started](../getting_started) has already been properly configured) + +Ensure to create an Event Grid namespace by following the steps in [setup](../setup). Event Grid namespace requires registering the client, and the topic spaces to authorize the publish/subscribe permissions. + +### Create the Client (Skip if [getting_started](../getting_started) has already been properly configured) + +We will use the SubjectMatchesAuthenticationName validation scheme for `sample_client`. Instructions for how to do this can be found in [getting_started](../getting_started). If this has already been done once, it does not have to be done again (unless using a different Azure account). + +### Create topic spaces and permission bindings +Run the commands to create the "samples" topic space, and the two permission bindings that provide publish and subscribe access to $all client group on the samples topic space. As for above, the instructions to do this are part of [getting_started](../getting_started) and do not have to be repeated if they have already been done in the Azure account being used to run this sample. + +## :game_die: Run the Sample + +All samples are designed to be executed from the root scenario folder. + +### dotnet + +To build the dotnet sample run: + +```bash +# from folder scenarios/jwt_authenticaton +dotnet build dotnet/jwt_authentication.sln +``` + +To run the dotnet sample: + +```bash + dotnet/jwt_authentication/bin/Debug/net7.0/jwt_authentication +``` + +## Connecting over WebSocket +To connect using WebSockets, modify client's `ConnectAsync()` call as follows: +```csharp +MqttClientConnectResult connAck = await mqttClient!.ConnectAsync(new MqttClientOptionsBuilder() + .WithClientId("sample_client") + //.WithTcpServer(hostname, 8883) + .WithWebSocketServer(b => b.WithUri($"{hostname}:443/mqtt")) + .WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V500) + .WithAuthentication("OAUTH2-JWT", Encoding.UTF8.GetBytes(jwt.Token)) + .WithTlsOptions(new MqttClientTlsOptions() { UseTls = true }) + .Build()); +``` + +Note that it is required to use port 443 for websocket connections. To learn more about this flow visit the [documentation](https://learn.microsoft.com/azure/event-grid/mqtt-support#connection-flow). diff --git a/scenarios/jwt_authentication/dotnet/jwt_authentication.sln b/scenarios/jwt_authentication/dotnet/jwt_authentication.sln new file mode 100644 index 0000000..3af2a80 --- /dev/null +++ b/scenarios/jwt_authentication/dotnet/jwt_authentication.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "jwt_authentication", "jwt_authentication\jwt_authentication.csproj", "{64CD6647-A322-4F5C-AFD1-3B657CE65FA5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {64CD6647-A322-4F5C-AFD1-3B657CE65FA5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64CD6647-A322-4F5C-AFD1-3B657CE65FA5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64CD6647-A322-4F5C-AFD1-3B657CE65FA5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64CD6647-A322-4F5C-AFD1-3B657CE65FA5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7A2246F6-BDDC-4173-AD14-FD2DC0F879D0} + EndGlobalSection +EndGlobal diff --git a/scenarios/jwt_authentication/dotnet/jwt_authentication/Program.cs b/scenarios/jwt_authentication/dotnet/jwt_authentication/Program.cs new file mode 100644 index 0000000..ab5d830 --- /dev/null +++ b/scenarios/jwt_authentication/dotnet/jwt_authentication/Program.cs @@ -0,0 +1,39 @@ +using MQTTnet; +using MQTTnet.Client; +using MQTTnet.Client.Extensions; +using System.Text; +using Azure.Identity; +using Azure.Core; + +// Create client +IMqttClient mqttClient = new MqttFactory().CreateMqttClient(MqttNetTraceLogger.CreateTraceLogger()); +string hostname = ""; + +// Create JWT +var defaultCredential = new DefaultAzureCredential(); + +// Sets the audience field of the JWT to Event Grid +var tokenRequestContext = new TokenRequestContext(new string[] { "https://eventgrid.azure.net/" }); +AccessToken jwt = defaultCredential.GetToken(tokenRequestContext); + +// Required to use port 8883: https://learn.microsoft.com/azure/event-grid/mqtt-support +MqttClientConnectResult connAck = await mqttClient!.ConnectAsync(new MqttClientOptionsBuilder() + .WithClientId("sample_client") + .WithTcpServer(hostname, 8883) + .WithProtocolVersion(MQTTnet.Formatter.MqttProtocolVersion.V500) + .WithAuthentication("OAUTH2-JWT", Encoding.UTF8.GetBytes(jwt.Token)) + .WithTlsOptions(new MqttClientTlsOptions() { UseTls = true }) + .Build()); + +Console.WriteLine($"Client Connected: {mqttClient.IsConnected} with CONNACK: {connAck.ResultCode}"); + +mqttClient.ApplicationMessageReceivedAsync += async m => await Console.Out.WriteAsync( + $"Received message on topic: '{m.ApplicationMessage.Topic}' with content: '{m.ApplicationMessage.ConvertPayloadToString()}'\n\n"); + +MqttClientSubscribeResult suback = await mqttClient.SubscribeAsync("sample/+", MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce); +suback.Items.ToList().ForEach(s => Console.WriteLine($"subscribed to '{s.TopicFilter.Topic}' with '{s.ResultCode}'")); + +MqttClientPublishResult puback = await mqttClient.PublishStringAsync("sample/topic1", "hello world!", MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce); +Console.WriteLine(puback.ReasonString); + +Console.ReadLine(); diff --git a/scenarios/jwt_authentication/dotnet/jwt_authentication/jwt_authentication.csproj b/scenarios/jwt_authentication/dotnet/jwt_authentication/jwt_authentication.csproj new file mode 100644 index 0000000..96ed836 --- /dev/null +++ b/scenarios/jwt_authentication/dotnet/jwt_authentication/jwt_authentication.csproj @@ -0,0 +1,21 @@ + + + + Exe + net7.0 + enable + enable + + + + + + + + + + ..\..\..\..\mqttclients\dotnet\MQTTnet.Client.Extensions\bin\Debug\net7.0\MQTTnet.Client.Extensions.dll + + + +