Alexa-SkillRequest verifizieren

Bisher habe ich ja bereits den einen oder anderen Alexa-Skill veröffentlicht und bisher sind die Funktionen alle auf AWS, also in der Amazon Cloud, gehostet. Aber es gibt natürlich auch andere Cloud-Anbieter, wie zum Beispiel Microsoft Azure und da bei einem Alexa-Request auch nur JSON-Dateien ausgetauscht werden, ist es auch relativ einfach einen Skill bei einem anderen Cloud-Anbieter zu hosten. Amazon möchte jedoch, dass jeder Request dahin gehend verifiziert wird, dass dieser wirklich von Alexa stammt und ich möchte in diesem Beitrag zeigen, wie man dies innerhalb einer Azure Function mit der Hilfe des NuGet-Packages Alexa.NET und ein wenig Code erledigt.

Es gibt drei Kriterien, welche erfüllt sein müssen, damit ein Request erfolgreich verifiziert werden kann. Zunächst wird geschaut, ob im Header der Key SignatureCertChainUrl mit einer entsprechenden Url hinterlegt ist. Anschließend wird geprüft, ob dem Request ein Body angefügt ist, welcher nicht leer sein darf und zu guter letzt folgt ein weiterer Header-Test über einen Signatur-Parameter. Alle drei Sicherheitsmerkmale wollen wir nun validieren.

Als Basis dient ein Azure Functions Projekt, welches ihr in Visual Studio anlegen könnt.

Als Einstiegspunkt wählen wir aus der Gruppe Azure Functions v2 (.NET Core) den Eintrag Http trigger, denn unsere Funktion soll später über eine Url erreichbar sein. Die Einstellung für den Storage Account können wir zunächst auf Storage Emulator belassen und bei Access rights wählen wir Anonymous aus.

Nun müssen wir noch das NuGet-Package Alexa.NET unserem Projekt hinzufügen. Dabei handelt es sich um einen Wrapper für .NET, welcher das Erstellen von Alexa Skills vereinfacht.

Wir legen nun eine Klasse SkillRequestExtension an, welche später die Validierung eines SkillRequests von Alexa durchführen soll. Zunächst erstellen wir uns ein paar Hilfsmethoden zur Ermittlung der verschiedenen Einträge, welche zur Validierung notwendig sind. Wir beginnen mit der SignatureCertChainUrl, welche wir als Uri aus den Headern extrahieren.

private static Uri GetSignatureCertChainUrlFromRequest(HttpRequest httpRequest)
{
    httpRequest.Headers.TryGetValue("SignatureCertChainUrl",
      out var signatureCertChainUrlAsString);

    if (string.IsNullOrWhiteSpace(signatureCertChainUrlAsString))
        return null;

    Uri signatureCertChainUrl;
    try
    {
        signatureCertChainUrl = new Uri(signatureCertChainUrlAsString);
    }
    catch
    {
        return null;
    }

    return signatureCertChainUrl;
}

Im nächsten Schritt extrahieren wir den Body als String.

private static async Task<string> GetBodyFromRequestAsync(HttpRequest httpRequest)
{
    httpRequest.Body.Position = 0;
    var body = await httpRequest.ReadAsStringAsync();
    httpRequest.Body.Position = 0;

    return body;
}

Nun müssen wir auch noch aus den Headern den Signature-Eintrag extrahieren. Dafür verwenden wir die folgende Methode:

private static string GetSignatureFromRequest(HttpRequest httpRequest)
{
    httpRequest.Headers.TryGetValue("Signature", out var signature);
    return signature;
}

Zu diesen drei Methoden, welche uns die zu validierenden Daten aus den übermittelten Daten beschaffen, benötigen wir noch zwei Methoden, welche die eigentliche Validierung vornehmen. Hierbei kommen zwei Methoden aus dem Alexa.NET Package zum Einsatz.

private static bool IsTimestampValid(SkillRequest skillRequest)
{
    return RequestVerification.RequestTimestampWithinTolerance(skillRequest);
}

private static async Task<bool> IsRequestValid(string signature, 
  Uri signatureCertChainUrl, string body)
{
    return await RequestVerification.Verify(signature, 
      signatureCertChainUrl, body);
}

Nun haben wir alle Hilfsmethoden vorbereitet und können uns um die eigentliche Methode kümmern, welche wir später auch als Extension-Methode aufrufen werden. Der Ablauf der Methode ist selbsterklärend, denn zunächst werden die notwendigen Daten beschafft und im weiteren Verlauf dann noch validiert.

public static async Task<bool> ValidateRequestAsync(this SkillRequest skillRequest,
  HttpRequest request, ILogger log)
{
    // get signature certification chain url
    var signatureCertChainUrl = GetSignatureCertChainUrlFromRequest(request);
    if (signatureCertChainUrl == null)
    {
      log.LogError("Validation failed, because of incorrect SignatureCertChainUrl");
      return false;
    }

    // get signature header
    var signature = GetSignatureFromRequest(request);
    if (string.IsNullOrWhiteSpace(signature))
    {
        log.LogError("Validation failed, because of empty signature");
        return false;
    }

    // get body
    var body = await GetBodyFromRequestAsync(request);
    if (string.IsNullOrWhiteSpace(body))
    {
        log.LogError("Validation failed, because of empty body");
        return false;
    }

    // validate timestamp
    if (!IsTimestampValid(skillRequest))
    {
        log.LogError("Validation failed, because timestamp is not valid");
        return false;
    }

    // validate signature, signaturecertchainurl and body
    if (!await IsRequestValid(signature, signatureCertChainUrl, body))
    {
        log.LogError("Validation failed, because verification of request failed");
        return false;
    }

    return true;
}

Nun können wir eingehende Requests an unsere Azure Function validieren. Hierzu reicht es den Body des Requests als SkillRequest zu parsen und dann können wir den Request validieren. Wichtig ist, dass wir einen BadRequest senden, sofern die Validierung fehlschlägt. Ansonsten wird der Skill nämlich von Amazon nicht zertifiziert.

// Get body and deserialize json
var payload = await req.ReadAsStringAsync();
var skillRequest = JsonConvert.DeserializeObject<SkillRequest>(payload);

// Verifies that the request is a valid request from Amazon Alexa
var isValid = await skillRequest.ValidateRequestAsync(req, log);
if (!isValid)
    return new BadRequestResult();

Somit sind wir jetzt in der Lage einen eingehenden Request zu validieren und können so auch bequem unsere Skills nicht nur in AWS, sondern zum Beispiel auch bei Microsoft Azure, hosten. Dafür könnt ihr entweder diese Datei direkt in euer Projekt integrieren oder aber ihr nutzt das NuGet-Package Alexa.NET.Security.Functions, welches euch ebenfalls bequem die Extension-Methode zur Verfügung stellt.

Alexa Skills mit Azure Functions App Center mit .NET MAUI Apps verwenden Xamarin.Forms: Bilder aufnehmen bzw. auswählen