2

I want to let third-party organizations subscribe to my event grid events in a secure way. I am trying to do this using Azure AD as mentioned in the articles below.

https://learn.microsoft.com/en-us/azure/event-grid/security-authentication

https://learn.microsoft.com/en-us/azure/event-grid/secure-webhook-delivery

In the event subscription creation, I selected the WebHook endpoint type and entered the endpoint of the organization. For a proof of concept, I created an Azure Function (HttpTrigger) as a webhook endpoint out of my Azure AD.

Endpoint

Under the Additional Feature tab, I activated AAD Authentication. In this way, I managed to send a bearer token to the webhook endpoint.

AAD Authentication

My question is that how should the event subscriber validate the token which is coming from the event publisher?

When you want to make a request to an API that is secured with a bearer token authentication, you would first request a token from the API, make your request with that token and the API validates it. In this scenario, the token issuer and validator are the same.

In the event publisher/subscriber scenario, the subscriber is not the token issuer. That's what confuses me.

When the token is decrypted, it is kind of obvious where it comes from.

{
  "aud": "00000000-0000-0000-0000-000000000000",
  "iss": "https://sts.windows.net/00000000-0000-0000-0000-000000000000/",
  "iat": 1612970997,
  "nbf": 1612970997,
  "exp": 1613057697,
  "aio": "Lorem ipsum viverra",
  "appid": "00000000-0000-0000-0000-000000000000",
  "appidacr": "2",
  "idp": "https://sts.windows.net/00000000-0000-0000-0000-000000000000/",
  "oid": "00000000-0000-0000-0000-000000000000",
  "rh": "Lorem ipsum viverra",
  "roles": [
    "role"
  ],
  "sub": "00000000-0000-0000-0000-000000000000",
  "tid": "00000000-0000-0000-0000-000000000000",
  "uti": "Lorem ipsum viverra",
  "ver": "1.0"
}

Should I just validate the token by having some constants about the event publisher or is there a more elegant way?

Update: In case anyone is interested in how I implemented it, I leave a demo project here.

https://github.com/sahinad/Event.Grid.Subscriber.Api

Adem
  • 21
  • 3

1 Answers1

0

This is how I did this. Very similar to the link you posted in your question, but I solved it using built-in Azure classes and needed to make some changes to have the SubscriptionValidationEventData object recognised correctly.

EventsController.cs:


using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.EventGrid.Models;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using System.Net;
using System.ComponentModel;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace SamplesEventGridWebhooksAAD.Controllers
{
    public static class JsonExtension
    {
        public static bool TryParseJson<T>(this string @this, out T result)
        {
            bool success = true;
            var settings = new JsonSerializerSettings
            {
                Error = (sender, args) => { success = false; args.ErrorContext.Handled = true; },
                MissingMemberHandling = MissingMemberHandling.Error
            };
            result = JsonConvert.DeserializeObject<T>(@this, settings);
            return success;
        }
    }
    [Route("api/[controller]/[action]")]
    [ApiController]
    [Authorize]
    public class EventsController : ControllerBase
    {
        [HttpPost]
        public async Task<IActionResult> ProcessEvent(List<EventGridEvent> eventGridEvents)
        {
            foreach (var eventGridEvent in eventGridEvents)
            {
                // If the contents of `eventGridEvent.Data` looks like `SubscriptionValidationEventData`, send the ValidationCode back as part of the response.
                // This is necessary for Event Grid to succesfully register the webhook.
                if (eventGridEvent.Data.ToString().TryParseJson(out SubscriptionValidationEventData eventData))
                {
                    Console.WriteLine($"Got SubscriptionValidation event data, validationCode: {eventData.ValidationCode},  validationUrl: {eventData.ValidationUrl}, topic: {eventGridEvent.Topic}");
                    // Do any additional validation (as required) such as validating that the Azure resource ID of the topic matches
                    // the expected topic and then return back the below response
                    var responseData = new SubscriptionValidationResponse()
                    {
                        ValidationResponse = eventData.ValidationCode
                    };

                    return new OkObjectResult(responseData);
                }
            }

            return new OkObjectResult("The event has been processed.");
        }
    }
}

The pogram can be started up as follows:

Program.cs

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Identity.Web;
using Microsoft.IdentityModel.Logging;

var builder = WebApplication.CreateBuilder(args);

IdentityModelEventSource.ShowPII = true;

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(builder.Configuration.GetSection("AzureAd"));

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services
    .AddApplicationInsightsTelemetry()
    .AddHealthChecks();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();
app.MapHealthChecks("/healthz");

app.Run();

We can use the following configuration:

appsettings.json

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "mydomain.com", // The "Primary domain" of your Azure AD Tenant
    "TenantId": "a9fxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx"
    "ClientId": "0f5xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx", // Output of `azuread_application.webhook.application_id`
    "Audience": "0f5xxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx" // Output of `azuread_application.webhook.application_id`
  }
}
Karl
  • 5,573
  • 8
  • 50
  • 73