AWS IoT Dashboard Title Screen

AWS IoT mit Cognito absichern – Ein Schritt-für-Schritt-Guide

Keine Kommentare

Mit AWS IoT bekommt man sehr schnell und leicht seine Daten in die Cloud. Doch was macht man mit diesen dann? In diesem Artikel gehe ich darauf ein, wie man eine einfache JavaScript SPA mit AWS IoT verbinden kann, um die Daten in Web-Applikationen zu verwenden. Und zwar sicher, indem wir den Zugriff auf AWS IoT mit Cognito absichern.

AWS IoT Edge meets Frontend

Das Szenario sieht wie folgt aus: Die Edge Devices wurden an AWS IoT angebunden und kommunizieren damit schon fleißig. Ein Kollege aus dem Frontend hat ein Dashboard mit einem der üblichen Verdächtigen im Bereich SPA Frameworks gebaut. Und hier stehe ich nun und soll beide miteinander vereinigen und sie dazu bringen, dass sie miteinander kommunizieren können. Klingt ja erstmal nicht so schlimm. AWS IoT kommuniziert über MQTT, und da ich mich in einem JavaScript-/TypeScript-Umfeld befinde, kann ich die MQTT.js-Library verwenden. Damit verbinde ich mich einfach mit dem AWS IoT Broker, dann subsribe ich auf das Topic und – zack! – bekomme ich die Updates der Edge Devices, kann die UI mit diesen updaten und dann sollte doch auch schon alles funktionieren. Gesagt, getan. Nichts geht.

Der Standardweg zur Kommunikation mit AWS IoT erwartet Zertifikate, so wie es mit den Edge Devices gemacht wird. Da wir uns in einem Web-Frontend befinden, müssten wir die Zertifikate samt Private Key als Content an alle Clients ausliefern. Nein! Nope! Niemals! Der Private Key sollte niemals in einem Netzwerk zur Verfügung stehen, selbst wenn die App nur aus dem Intranet erreichbar sein sollte!
Alternativ zu den Zertifikaten aktzeptiert AWS aber auch Requests, die mit Signature-Version 4 (kurz: sigv4) signiert wurden. Hierzu ist eine vorherige Authentifizierung notwendig, zum Beispiel über IAM oder Cognito. Da die User nur auf das Dashboard zugreifen, verwenden wir Cognito für das User Management.

al

Legen wir einen User in Cognito an! Hierzu benötigen wir zwei Dinge: einen User Pool und einen Identity Pool. Der User Pool dient zur Verwaltung von Usern selbst, der Identity Pool vergibt den Usern Identitäten inklusive Berechtigungen.

Achtung: Dieser Artikel beschränkt sich nur auf das Verbinden von Cognito mit IoT, weshalb ich an allen anderen Stellen die Standardeinstellungen verwende. In einem produktiven Umfeld wird überlicherweise ein Unternehmens-LDAP für die Benutzerverwaltung verwendet. In einem solchen Umfeld muss kein User Pool angelegt werden. Beim Identity Pool wird dann, statt des Cognito User Pool, eine SAML Integration verwendet, um mit LDAP zu arbeiten.

Der Cognito User Pool

In der AWS-Konsole navigieren wir als erstes zu Cognito. Dieses findet man unter „Security, Identity, & Compliance“, oder indem man einfach „Cognito“ in der Service-Suche eintippt. Im Cognito Home Screen wählen wir nun „Manage user pools“ aus. Dann gehen wir auf „Create a user pool“.
Im folgenden Screen können wir uns als „Pool name“ etwas Passendes ausdenken. Später arbeiten wir nur noch mit der ID, er muss also nicht programmatisch benannt werden. Die Standardeinstellungen müssen nur minimal verändert werden, daher reicht uns hier der Klick auf „Review Defaults“.

aws iot cognito user pool

Nun zeigt uns Cognito die aktuelle Konfiguration des User Pools an. Wir müssen einen App Client erstellen. Ein Klick auf das Stift Symbol beim Eintrag „App Client“ öffnet die Optionen für diesen.

cognito_user_pool_creation_overview_add_app_client

Im folgenden minimalistischen Screen wählen wir die einzige Option aus, die es gibt: „Add an app client“.

cognito_user_pool_add_app_client

Nun müssen wir uns einen Namen für den App Client überlegen. Wie auch beim User Pool wird später nur mit der ID gearbeitet und es ist kein programmatischer Name notwendig. Die Gültigkeit für das Refresh Token ist nicht wichtig und kann auf dem Standardwert bleiben. Die Checkboxen müssen alle deaktiviert sein. Über den Button „Create app client“ legen wir den App Client an.

cognito_app_client_creation

Der nun angezeigte Screen ist etwas verwirrend. Man kann den Namen des App Clients bearbeiten, aber als Buttons sind nur wieder „Add an app client“ und „Return to pool details“ vorhanden. Ich verstehe den Sinn dieses Screens nicht wirklich, aber wir müssen hier auch nichts weiter machen als auf „Return to pool details“ klicken.

cognito_app_client_creation_confirmation

Nun sind wir wieder in der Übersicht unseres User Pools. Wir überprüfen, ob unser eben angelegter App Client nun auch eingetragen ist und bestätigen die Zusammenfassung mit „Create Pool“.

Achtung: Wenn du diesen Guide in einem produktiven Umfeld verwendest, sind hier natürlich noch alle anderen, nicht Cognito-IoT-spezifischen Dinge zu konfigurieren, die dein Projekt mit sich bringt! Oder, besser, verzichte auf den Cognito User Pool und binde das Unternehmens-LDAP über SAML an! Das wird im Identity Pool gemacht, zu dem kommen wir im nächsten Abschnitt.

cognito_user_pool_creation_overview

Am oberen Bildschirmrand wird nun eine Bestätigung über das Anlegen des User Pools angezeigt. Mögliche nachfolgende rote Texte können ignoriert werden, zum Beispiel die Meldung über den Versand der Mails durch SES. Des Weiteren finden wir auf dem Bestätigungsbildschirm die „Pool Id“ sowie den „Pool ARN“. Diese benötigen wir später. Daher kopieren wir uns diese in ein Notepad, damit wir sie für später parat haben.

cognito_user_pool_creation_confirmation_message cognito_user_pool_id

Wir benötigen nun noch die ID des App Clients. Hierzu wählen wir links im Menü den Eintrag „App client settings“ aus.

cognito_user_pool_menu

Im folgenden Screen wird uns die App Client Id direkt unter dem Namen angezeigt.

cognito_app_client_id

Der Cognito Identity Pool

Der User Pool ist angelegt, nun folgt der Identity Pool! Von der Zusammenfassung des User Pools gelangt man zum Identity Pool über den Link „Federated Identities“ links oben im Screen. Alternativ kann man über den Servicenavigator wieder Cognito auswählen und auf dem Cognito Home Screen dann „Manage Identity Pools“ auswählen. Und ja, AWS bezeichnet es sowohl als „Federated Identities“ als auch als „Identity Pools“. Es steht aber beides für ein und dasselbe.

Über „Create new identity pool“ legen wir einen neuen Identity Pool an. Wie immer arbeiten wir später mit der ID, weshalb wir uns für den Namen etwas Hübsches überlegen können. Unter „Unauthenticated Identities“ wird der Zugriff für quasi die gesamte Welt festgelegt. Die Checkbox sollte bereits deaktiviert sein, so ist sichergestellt, dass nur User, die sich vorher authentifiziert haben, auch Zugriff auf die Ressourcen bekommen. Unter „Authentication Providers“ wählen wir den Reiter „Cognito“, um unseren eben angelegten User Pool als Provider zu konfigurieren. Hierdurch können wir später im User Pool neue User anlegen, die sich dann an unserem Dashbord einloggen können. Über „Create Pool“ wird der Pool erzeugt. Im Folgescreen kann man Cognito automatisch IAM-Rollen für den Identity Pool anlegen lassen. Berechtigungen konfigurieren wir später, das läuft mit IoT etwas anders ab als gewohnt. Lassen wir die Einstellungen alle so, wie sie sind und klicken einfach weiter. Der Pool ist angelegt!

Achtung: Wenn du diesen Guide in einem produktiven Umfeld verwendest, wird überlicherweise kein Cognito User Pool verwendet, sondern ein LDAP. Dieses wird hier über den Reiter „SAML“ angebunden. Hierzu sind weitere Konfigurationen im IAM notwendig, auf die ich in diesem Guide aber nicht eingehen werde.
Theoretisch kannst du den Login zu deiner Anlagensteuerung auch über Facebook oder Google Accounts ermöglichen. Ob das den Sicherheitsvorgaben entspricht, solltest du vorher aber gründlich prüfen.

cognito_create_identity_pool

Der erste User

Bevor wir nun die AWS-Konsole schließen und die IDE öffnen können, benötigen wir noch einen User, mit dem wir auch arbeiten und testen können. Wechseln wir daher nochmal in den User Pool zurück. Wenn du noch im Identity Pool Screen bist … Manchmal gibt es oben links den Link „User Pools“, manchmal nicht. Am einfachsten ist es, über den Servicenavigator Cognito zu wählen und im Cognito Home Screen „Manage User Pools“ zu wählen. In der User-Pool-Übersicht klicken wir dann auf „My Fancy User Pool“ und dann links im Menü auf den ersten Eintrag „Users and Groups“. Dann auf „Create User“.

cognito_user_management

Das Popup ist sieht zwar recht simpel aus, doch stecken hier ein paar Knackpunkte drin. Der Benutzername ist ein Pflichtfeld. Ja, ergibt Sinn. Als Passwort vergeben wir ein Initialpasswort, das wir später ändern müssen. Müssen! Die „Invitation“ soll per E-Mail versendet werden, da wir keine Telefonnummer hinterlegen werden. Daher muss auch die Checkbox für „Mark phone number as verified?“ deaktiviert sein. Als E-Mail vergeben wir eine Adresse, auf die wir Zugriff haben. Und wir markieren sie als verifiziert, indem wir die Checkbox für „Mark email as verified?“ setzen. Wenn wir nun auf „Create user“ klicken, wird der User angelegt und es wird eine Mail mit Benutzernamen und dem temporären Passwort an die hinterlegte Mailadresse gesendet.
Der Knackpunkt an dieser Sache ist nun aber folgender: Der User kann sich mit dem temporären Passwort problemlos an Cognito authentifizieren und erhält eine Cognito Session. Er erhält aber auch die Auth-Challenge, dass er ein neues Passwort setzen muss. Solange das nicht geschehen ist, erhält er keinen Zugriff auf AWS-Ressourcen! AWS Cognito bietet keine eigene UI für User an, Passwörter zu ändern. Dies müssen wir in unserem Dashboard selbst implementieren, hierzu aber später mehr, wenn wir die JavaScript-Implementierung angehen. Wenn du diesem Guide folgst und gleich Pause machen willst, dann noch folgender Hinweis: Das vom Administrator vergebene, temporäre Passwort ist nur sieben Tage gültig. Wird in diesem Zeitraum kein neues Passwort gesetzt, wird der User gesperrt. Sollte das passieren, kann man den User aber über „Manager User Pools“ wieder entsperren.

cognito_create_user

Zusammenfassung der AWS-Konfiguration

Geschafft! Zumindest die Konfiguration in AWS. Jetzt geht’s gleich in JavaScript weiter. Vorher möchte ich aber noch eine kleine Zusammenfassung geben, da wir ja nun doch schon einiges in der AWS-Konsole gemacht haben.
Wir haben einen User Pool angelegt, damit wir in diesem User anlegen und verwalten können. E-Mail-Adresse, Passwort, den ganzen Standard-Kram.
Dann haben wir noch einen App Client erstellt, der unsere Dashboard-App in der AWS repräsentiert und die User des User Pools mit dieser App assoziiert.
Dann haben wir einen Identity Pool angelegt, damit die User aus dem User Pool auch eine Identität erhalten, an der die Berechtigungen hängen. Hier haben wir keine speziellen Berechtigungen vergeben, da IoT nicht mit den IAM-Rechten arbeitet, sondern ein eigenes Rechtemanagemnt besitzt (okay, wir müssen später nochmal ganz kurz in die AWS-Konsole).
Und, damit wir unsere App auch testen können, haben wir noch einen User angelegt.

Amplify und Amplify JS

Wenn man ein AWS SDK in sein Projekt einbinden möchte, stößt man als Neuling schnell auf die Frage, was der Unterschied zwischen AWS-SDK, AWS-IOT-SDK und AWS Amplify ist. Nun, die Kurzfassung lautet: Das AWS-SDK konsolidiert die ganzen Einzel-SDKs. AWS-IOT-SDK ist ein solches Einzel-SDK. Und diese gibt es schon recht lange und sie verwenden teils „altbackene“ Methoden und Prinzipien.
Mit AWS Amplify wird ein moderneres SDK angeboten, das auf aktuelleren Prinzipien basiert und zudem viel Boiler Plating abnimmt.

Als kleines Beispiel:
Mit den alten SDKs musste man, nach der Authorisierung gegen Cognito, die Auth-Daten, also Session Token, Access ID, etc. manuell aus dem Auth-Kontext extrahieren, in der eigenen App abspeichern und bei den Requests gegen IoT manuell mitgeben. Mit AWS Amplify wird einem das abgenommen und man muss sich nicht um „Metadaten-Jonglieren“ kümmern.

AWS Amplify gibt es für verschiedene Sprachen, unter anderem auch für JavaScript als AmplifyJS-Implementierung.

Und AWS Amplify kommt nicht nur als reines Framework zum Einsatz im Hintergrund, nein, es bietet für diverse andere Frameworks auch integrationen, um direkt genutzt werden zu können. So gibt es Integrationen für Angular, React und Vue.js und bringt hierzu bereits fertige Komponenten mit.
Man muss in seine App dann nur noch die Komponente einfügen, etwa „<amplify-signin />“ und schon hat man ein vollständiges Login-Form inklusive des Backends, das den Login gegen Cognito durchführt. Kein weiterer Code von uns ist notwendig! Wow!

Einrichten der Auth-Verbindung

Als erstes richten wir die Autentifizierung ein:

import Amplify, {Auth} from 'aws-amplify';

Amplify.configure({
Auth: {
identityPoolId: "eu-central-1:2ace34f0-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
region: "eu-central-1",
identityPoolRegion: "eu-central-1",
userPoolId: "eu-central-xxxxxxxxxxx",
userPoolWebClientId: "xxxxxxxxxxxxxxxxxxxxxxxxxx",
mandatorySignIn: true,
cookieStorage: {
domain: 'localhost',
path: '/',
expires: 365,
secure: false
}
}
});

In diesem Code-Segment wird Amplify und explizit das Auth-Modul von Amplify in die App importiert und anschließend konfiguriert.
Die Konfiguration bezieht sich hier nur auf das „Auth“-Modul. Hier tragen wir dann die Daten ein, die wir uns vorhin im Notepad gespeichert haben.
Nun können wir die einzelnen Funktionen des Auth-Moduls aufrufen.

Auth.signIn({
username,
password,
}).then(user => {
Auth.currentUserCredentials().then((credentials => {
console.log("!!! Identity ID: " + credentials.identityId);
}));

if (user.challengeName === 'NEW_PASSWORD_REQUIRED') {
// User hat nur das Initial-Passwort und muss es ändern
} else {
// Weitere Challenges
}

}).catch(() => {
// Im Falle von andere Problemen landen wir hier
});

In diesem Code-Segment wird über „Auth.signIn“ ein Login-Prozess durchgeführt. Wenn der Login erfolgreich war, wird der then()-Block ausgeführt, in allen anderen Fällen landet man im catch(). War der Login erfolgreich, so kann man über Auth.currentUserCredentials() zusätzliche Informationen vom aktuellen User erhalten. Hierbei werden diese immer aktuell aus AWS abgerufen, weshalb es sich, wie auch beim Sign-In, um ein Promise handelt.
Achtung: Für die Berechtigung in IoT benötigen wir aus Auth.currentUserCredentials() die ID aus credentials.identityId. Man sollte diese ID in der UI anzeigen oder zumindest in der Browser-Konsole ausgeben, damit wir sie später verwenden können.
Leider ist diese ID in der AWS-Cognito-Konsole nirgends zu sehen, daher ist es zwingend notwendig, sich bereits jetzt einmal an der App einzuloggen und so die ID zu erhalten.

Ein weiterer Stolperstein ist die User Challenge. Wurde der User neu angelegt und hat vom Administrator ein Einmal-Passwort erhalten, so wird nach dem Login die User Challenge „NEW_PASSWORD_REQUIRED“ lauten. Der User muss sein eigenes Passwort setzen.
Solange er dies nicht getan hat, kann er sich zwar erfolgreich gegenüber Cognito authentifizeren, wird aber keine Rechte erhalten. Wenn ein AWS Service bei Cognito das Session Token verifizieren möchte, wird Cognito dieses einfach als ungültig ablehnen.
Folgender Code kann einmal in der App hinterlegt werden, um das Passwort neu zu setzen und die Challenge zu lösen.

Auth.signIn(username, initialPassword)
.then(user => {
Auth.completeNewPassword(user, "MyNewFancyPassword123!");
}

Dieser Code funktioniert zwar, hat aber das maximale Level an „Hackiness“ und hat auch den „Quickest and Dirties“ Award gewonnen.
Also bitte nur als „Grundsätzlich geht das so“ ansehen und ordentlich in die App integrieren.

Anlegen einer IoT Policy

Wir sind fast am Ziel! Wir haben nun das User Manamgenet in AWS konfiguriert und unsere App ist nun auch soweit, dass sich ein User über sie anmelden kann. Was nun noch fehlt, ist, dass der User das Recht hat, mit AWS IoT zu kommunizieren. Wie vorhin schon angekündigt, hat AWS IoT ein eigenes Rechtemanagement, weshalb die Cognito-Berechtigung über IAM-Rollen/-Policies in IoT nicht wirkt.

In der AWS-Konsole nutzen wir den Service Navigator, um zu „IoT Core“ zu navigieren.
Dort wählen wir im Menü links dann den Eintrag „Secure“ aus.

IoT Menü Secure

Hier klicken wir auf den „Create“-Button, rechts oben im Screen.
Nun geben wir der neuen Policy einen Namen. Und im Gegensatz zu allen bisherigen Namen wird dieser hier tatsächlich programmatisch verwendet.
Dann klicken wir auf „Advances Mode“, um die Berechtigungen textuell eingeben zu können.

iot_create_policy

Als Berechtigungen hinterlegen wir folgende Werte. Mit dieser Policy erhält man das Recht, sich mit dem IoT Broker zu verbinden und zu subscriben, aber kein Recht zu publishen. Es ist also ein reines Lese-Recht.

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "iot:Connect",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "iot:Subscribe",
"Resource": "*"
},
{
"Effect": "Allow",
"Action": "iot:Receive",
"Resource": "*"
}
]
}

Mit einem Click auf „Create“ wird die Policy angelegt. Und damit sind wir in der AWS-Konsole auch schon wieder fertig.

Zuweisen der IoT Policy an den Cognito User

Nun müssen wir die Policy an den Cognito-User hängen, damit der User die Leserechte erhält. Am einfachsten geht dies über das AWS CLI.

aws attach-principal-policy --policy-name "MyFancyDashboardPolicy" --principal "xxxxxxx"

Im Feld „–principal“ wird die ID eingetragen, die wir aus „credentials.identityId“ erhalten haben. Haben wir vorhin auch ins Notepad kopiert.

Noch eine kleine Anmerkung an diesen Befehl:
In der AWS-Dokumentation zu diesem Befehl liest man, dass er „deprecated“ ist und man stattdessen „attach-policy“ verwenden soll. Nun, attack-policy nimmt aber kein Principal entgegen. Ich habe es nicht geschafft, die Policy mit „attach-policy“ an mein Principal zu hängen. Ich bin mir aber nicht sicher, ob ich nur zu doof war, oder hier doch ein Fehler im AWS Tooling vorliegt.

Amplify-PubSub-Anbindung einrichten

Wir haben nun den User eingerichtet und die notwendigen Rechte zugewiesen. Nun sollte uns nichts mehr im Weg stehen, um mit dem User auf den IoT Broker zu connecten und Messages zu empfangen. Richten wir also PubSub ein, das Amplify-Modul, das die MQTT Broker Connection verwaltet.

import Amplify, {PubSub} from 'aws-amplify';
import {AWSIoTProvider} from '@aws-amplify/pubsub/lib/Providers';

Amplify.addPluggable(new AWSIoTProvider({
    aws_pubsub_region: 'eu-central-1',
    aws_pubsub_endpoint: 'wss://xxxxxxxxxxxxxxxxxxx.iot.eu-central-1.amazonaws.com/mqtt',
}));

In diesem Code-Segment importieren wir neben Amplify und dem Amplify-PubSub-Modul auch den AWSIoTProvider, der speziell für die AWS-IoT-MQTT-Kommunikation genutzt werden kann. Danach folgt die Konfiguration. Im Endpoint sehen wir, dass wir das „wss“-Protokoll verwenden, also Secure WebSockets.

Und hier finden wir auch den „endpoint“, über den wir bisher nicht gesprochen haben. Dies ist der MQTT Broker, der von AWS IoT automatisch eingerichtet und bereitgestellt wird. Den URL findet man in IoT Core unter dem Menüpunkt „Settings“ links unten.

iot_menu_settings

Und dort dann direkt im ersten Abschnitt unter „Custom endpoint“. Achtung, wir müssen noch die Endung „/mqtt“ anhängen!

iot_settings

Subscribe und Receive

Amplify besitzt kein explizites Verbinden zum Broker, stattdessen subscribt man einfach und fertig. Und so einfach das klingt, ist es auch:

PubSub.subscribe('$aws/things/BrennofenSensor/shadow/update/accepted')
.subscribe({
next: data => {
// Message behandeln
},
});

Anmerkung

Der Weg zu dieser Lösung war lang und steinig. Im Internet habe ich diverse Anleitungen gefunden, die immer nur einen Teilaspekt des Themas „Cognito + IoT“ beleuchten. Zum großen Teil waren diese Anleitungen auch veraltet und die dort erwähnten Optionen existieren in AWS aktuell nicht mehr. Ich fürchte, diesem Schicksal wird auch mein Guide nicht entgehen können. Daher die Bitte: Findest du Unstimmigkeiten oder gar Obsoletes im Guide, hinterlasse mir doch einen kurzen Kommentar und ich werde es aktualisieren.

Holger Apfel

Holger Apfel ist Senior IT Consultant bei der codecentric AG und unterstützt Kunden in der Umsetzung ihrer IT-Projekte.
Sowohl als Inhouse-Entwickler als auch als Consultant war Holger in verschiedenen Branchen Tätig und hat umfangreiche Erfahrungen gesammelt, seit er 2006 seine Karriere als IT Professional gestartet hat.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.