Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sample with JWT #68

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions scenarios/jwt_authentication/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# :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 a Json Web Token, which is used to authenticate to Event Grid.
- 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
- 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/+|


## :lock: Configure the Json Web Token and AAD Role Assignments

1. Modify the JSON file `auth.json`, found in `./dotnet/jwt_authentication/` with a subscription Id:

```json
{
"properties": {
"roleName": "Event Grid Pub-Sub",
"description": "communicate with Event Grid.",
"assignableScopes": [
"/subscriptions/<YOUR SUBSCRIPTION ID HERE>"
],
"permissions": [
{
"actions": [],
"notActions": [],
"dataActions": [
"Microsoft.EventGrid/*"
],
"notDataActions": []
}
]
}
}
```
2. In the Azure portal, go to your Resource Group and open the Access control (IAM) page.
3. Click Add and then click Add custom role. This opens the custom roles editor.
4.

## :triangular_ruler: Configure Event Grid Namespaces

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

We will use the SubjectMatchesAuthenticationName validation scheme for `sample_client` to create the client from the portal or with the script:

```bash
# from folder scenarios/getting_started
source ../../az.env

az resource create --id "$res_id/clients/sample_client" --properties '{
"authenticationName": "sample_client",
"state": "Enabled",
"clientCertificateAuthentication": {
"validationScheme": "SubjectMatchesAuthenticationName"
},
"attributes": {
"type": "sample-client"
},
"description": "This is a test publisher client"
}'
```

### 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.

```bash
# from folder scenarios/getting_started
source ../../az.env

az resource create --id "$res_id/topicSpaces/samples" --properties '{
"topicTemplates": ["sample/#"]
}'

az resource create --id "$res_id/permissionBindings/samplesPub" --properties '{
"clientGroupName":"$all",
"topicSpaceName":"samples",
"permission":"Publisher"
}'

az resource create --id "$res_id/permissionBindings/samplesSub" --properties '{
"clientGroupName":"$all",
"topicSpaceName":"samples",
"permission":"Subscriber"
}'
```

### Create the .env file with connection details

The required `.env` files can be configured manually, we provide the script below as a reference to create those files, as they are ignored from git.

```bash
# from folder scenarios/getting_started
source ../../az.env
host_name=$(az resource show --ids $res_id --query "properties.topicSpacesConfiguration.hostname" -o tsv)

echo "MQTT_HOST_NAME=$host_name" > .env
echo "MQTT_USERNAME=sample_client" >> .env
echo "MQTT_CLIENT_ID=sample_client" >> .env
echo "MQTT_CERT_FILE=sample_client.pem" >> .env
echo "MQTT_KEY_FILE=sample_client.key" >> .env
```

## :fly: Configure Mosquitto

To establish the TLS connection, the CA needs to be trusted, most MQTT clients allow to specify the ca trust chain as part of the connection, to create a chain file with the root and the intermediate use:

```bash
# from folder _mosquitto
cat ~/.step/certs/root_ca.crt ~/.step/certs/intermediate_ca.crt > chain.pem
cp chain.pem ../scenarios/getting_started
```
The `chain.pem` is used by mosquitto via the `cafile` settings to authenticate X509 client connections.

```bash
# from folder scenarios/getting_started
echo "MQTT_HOST_NAME=localhost" > .env
echo "MQTT_CLIENT_ID=sample_client" >> .env
echo "MQTT_CERT_FILE=sample_client.pem" >> .env
echo "MQTT_KEY_FILE=sample_client.key" >> .env
echo "MQTT_CA_FILE=chain.pem" >> .env
```

To use mosquitto without certificates

```bash
# from folder scenarios/getting_started
echo "MQTT_HOST_NAME=localhost" > .env
echo "MQTT_TCP_PORT=1883" >> .env
echo "MQTT_USE_TLS=false" >> .env
echo "MQTT_CLIENT_ID=sample_client" >> .env
```

## :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/getting_started
dotnet build dotnet/getting_started.sln
```

To run the dotnet sample:

```bash
dotnet/getting_started/bin/Debug/net7.0/getting_started
```
(this will use the `.env` file created before)
25 changes: 25 additions & 0 deletions scenarios/jwt_authentication/dotnet/jwt_authentication.sln
Original file line number Diff line number Diff line change
@@ -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
47 changes: 47 additions & 0 deletions scenarios/jwt_authentication/dotnet/jwt_authentication/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Client.Extensions;
using System.Text;
using Azure.Identity;
using Azure.Core;

//System.Diagnostics.Trace.Listeners.Add(new System.Diagnostics.ConsoleTraceListener());

//MqttConnectionSettings cs = MqttConnectionSettings.CreateFromEnvVars();
//Console.WriteLine($"Connecting to {cs}");

// Create client
IMqttClient mqttClient = new MqttFactory().CreateMqttClient(MqttNetTraceLogger.CreateTraceLogger());
string hostname = "<Event Grid Mqtt Hostname Here>";

// Create JWT
var defaultCredential = new DefaultAzureCredential();
var tokenRequestContext = new TokenRequestContext(new string[] { "https://eventgrid.azure.net/" });
AccessToken jwt = defaultCredential.GetToken(tokenRequestContext);
patilsnr marked this conversation as resolved.
Show resolved Hide resolved

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();

// TODO -- app authentication
// TODO -- mqtt client extensions?
// TODO -- test which params are needed (e.g., tls, username)
// TODO -- catch mqtt autnetication failed exceptions and gracefully exit
// MSAL -- borwser flow authentication
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.10.1" />
<PackageReference Include="MQTTnet" Version="4.3.1.873" />
</ItemGroup>

<ItemGroup>
<Reference Include="MQTTnet.Client.Extensions">
<HintPath>..\..\..\..\mqttclients\dotnet\MQTTnet.Client.Extensions\bin\Debug\net7.0\MQTTnet.Client.Extensions.dll</HintPath>
</Reference>
</ItemGroup>

</Project>