Cognito-basierte Authentifizierung für durch CloudFront geschützte Ressourcen

By Gerald Mücke | September 21, 2017

Cognito-basierte Authentifizierung für durch CloudFront geschützte Ressourcen

Cognito ist ein relativ neues Angebot zur Identitätsverwaltung für Apps und Dienste, einschließlich Profilverwaltung und Multi-Faktor-Authentifizierung. CloudFront ist der Content Delivery Network-Dienst von Amazon Web Services. CloudFront bietet sowohl öffentlich zugänglichen als auch privaten Inhalt. Privater Inhalt kann über entweder signierte URLs oder signierte Cookies zugegriffen werden. Cognito generiert jedoch OAuth-Zugriffstoken. Dieser Artikel beschreibt, wie man einen Dienst zum Erstellen signierter Cookies für Cloudfroint mithilfe der Zugriffskontrolle von Cognito erstellt.

Einrichten des Cognito-Clients

Zuerst verbinden wir uns mit Cognito. Amazon bietet ein funktionsreiches SDK für seine Dienste, einschließlich Cognito. Für Cognito benötigen wir die folgende Maven-Abhängigkeit in unserem Projekt:

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-cognitoidp</artifactId>
<version>1.11.176</version>
</dependency>

Weiter benötigen wir die poolId und die clientId und optional den Geheimhash, wenn Ihr Pool so konfiguriert ist, dass er einen verwendet. Alle Informationen sind vom Cognito-Dienst erhältlich.

Dann erstellen wir eine Cognito-Clientinstanz mit dem Builder

AWSCognitoIdentityProviderClientBuilder builder = AWSCognitoIdentityProviderClientBuilder.standard();

Wenn Sie die AWS CLI für Ihr lokales System nicht konfiguriert haben und daher keine Region konfiguriert haben - was normalerweise bei Servern der Fall ist - müssen Sie die Region Ihres Cognito-Pools angeben.

cognitoBuilder.setRegion("us-east-1");

Jetzt erstellen wir den Client:

AWSCognitoIdentityProvider client = builder.build();

Authentifizierung in einem Schritt

Alle Authentifizierungsworkflows werden auf die gleiche Weise gestartet. Abhängig davon, ob der Pool oder Benutzer eine Multi-Faktor-Authentifizierung benötigt, ist ein zusätzlicher Schritt erforderlich, der im nächsten Abschnitt beschrieben wird. Wenn sich der Benutzer zum ersten Mal anmeldet, gibt es immer eine Herausforderung, ein neues Passwort festzulegen. Aber sobald das Passwort festgelegt ist, ist der initiale Authentifizierungsschritt der letzte Schritt für die Authentifizierung in einem Schritt.

Um die Authentifizierung zu initiieren, muss der Benutzer eine Map mit den Authentifizierungsparametern füllen und eine AdminInitiateAuthRequest erstellen. Wir verwenden den admin Authentifizierungsfluss, weil der Code auf einem sicheren Backend Server 1 laufen soll und die Authentifizierung im Namen des eigentlichen Benutzers durchführt, um den Dienst des Erstellens von signierten CloudFront-Cookies anzubieten. Eine App, die Cognito verwendet, würde einen “normalen” Authentifizierungsschritt durchführen.

final String username = ...;
final String password = ...;
final String secretHash = ...; //optional

final Map<String, String> authParams = new HashMap<>();
authParams.put("USERNAME", username);
authParams.put("PASSWORD", password);

if (secretHash != null) {
authParams.put("SECRET_HASH", secretHash);
}

final AdminInitiateAuthRequest authRequest = new AdminInitiateAuthRequest();
authRequest.withAuthFlow(AuthFlowType.ADMIN_NO_SRP_AUTH)
.withClientId(clientId)
.withUserPoolId(poolId)
.withAuthParameters(authParams);

Sobald wir die Anfrage erstellt haben, könnten wir sie an den Client senden und ein AuthenticationResult erhalten

AdminInitiateAuthResult result = client.adminInitiateAuth(authRequest);

Das Ergebnis kann entweder einen Erfolg oder eine weitere Herausforderung für die Anwendung anzeigen, auf die reagiert werden muss. Im Fall, dass sich der Benutzer zum ersten Mal anmeldet - wie oben beschrieben - antwortet Cognito mit einer Neupasswort-Herausforderung.

if("NEW_PASSWORD_REQUIRED".equals(result.getChallengeName()){

//wir brauchen immer noch den Benutzernamen
String username = ...;

final Map<String, String> challengeResponses = new HashMap<>();
challengeResponses.put("USERNAME", username);
challengeResponses.put("PASSWORD", credentials.getString("password"));

//fügen Sie das neue Passwort der Params-Map hinzu
challengeResponses.put("NEW_PASSWORD", credentials.getString("password_new"));

//füllen Sie die Challenge-Antwort aus
final AdminRespondToAuthChallengeRequest request = new AdminRespondToAuthChallengeRequest();
request.withChallengeName(ChallengeNameType.NEW_PASSWORD_REQUIRED)
.withChallengeResponses(challengeResponses)
.withClientId(clientId)
.withUserPoolId(poolId)
.withSession(credentials.getString("session"));

Wichtig hierbei ist, dass während der Neupasswort-Herausforderung fehlende, aber erforderliche Benutzerattribute ebenfalls übergeben werden müssen. Die Antwort auf die init-auth-Anfrage enthält Herausforderungsparameter.

Map<String,String> challengeParmas = result.getChallengeParameters()

Diese Map enthält mindestens zwei Einträge:

  • requiredAttributes : ein string-codiertes JsonArray mit den erforderlichen Benutzerattributen
  • userAttributes : ein string-codiertes JsonObject mit den aktuellen Benutzerattributen

Beispielweise:

"requiredAttributes" : "[\"userAttributes.family_name\",\"userAttributes.given_name\"]",
"userAttributes" : "{\"email_verified\":\"true\",\"phone_number_verified\":\"true\",\"phone_number\":\"+12345\",\"given_name\":\"\",\"family_name\":\"\",\"email\":\"me@you.us\"}"

Um die erforderlichen Parameter festzulegen, müssen die Benutzerattribute in der Params-Map mit demselben Attributnamen wie in der Liste der erforderlichen Attribute festgelegt werden, einschließlich des userAttributes.-Präfixes.

challengeResponses.put("userAttributes.myAttributed", "attrValue");

Sobald Sie die Parameter für das neue Passwort und die erforderlichen Benutzerattribute bereit haben, können Sie die Antwort an Cognito senden

AdminRespondToAuthChallengeResult result = client.adminRespondToAuthChallenge(request)

Wenn das Ergebnis einer beliebigen Herausforderungsantwort oder der init-auth-Anfrage keinen Herausforderungsnamen, sondern einen AuthenticationResultType enthält, war die Authentifizierung erfolgreich. Das Ergebnis der erfolgreichen Authentifizierung enthält die OAuth-Zugriffstoken und die Benutzerinformationen. Sie können diese in einer Sitzung oder einem Cookie speichern oder einfach verwerfen, denn wir möchten jetzt die signierten Cookies für CloudFront erstellen und benötigen daher nur die Information, dass der Benutzer derjenige ist, der er sein soll.

Aber bevor wir zum CloudFront-Teil kommen, schauen wir uns - der Vollständigkeit halber - die

Zwei-Schritt-Authentifizierung an

An diesem Punkt haben wir die Authentifizierung entweder erfolgreich initiiert oder optional bereits ein neues Passwort festgelegt. Der Benutzer ist immer noch nicht authentifiziert, und das Ergebnis beider Operationen enthält eine Herausforderung mit dem Namen SMS_MFA. Dies deutet darauf hin, dass der Benutzer einen Einmal-Token per SMS (oder E-Mail) erhalten hat, den sie eingeben muss.

Das Verfahren ist grundsätzlich dasselbe wie bei der Neupasswort-Herausforderung, wir geben die zusätzlichen Details ein:

if("SMS_MFS".equals(result.getChallengeName()){

final String username = ...; //wir brauchen immer noch den Benutzernamen
final String mfaCode = ...; //der Benutzer muss diesen Code eingeben

final Map<String, String> authParams = new HashMap<>();
authParams.put("USERNAME", username);
authParams.put("SMS_MFA_CODE", mfaCode);

final AdminRespondToAuthChallengeRequest mfaRequest = new AdminRespondToAuthChallengeRequest();
mfaRequest.withChallengeName(ChallengeNameType.SMS_MFA)
.withChallengeResponses(authParams)
.withClientId(clientId)
.withUserPoolId(poolId)
.withSession(credentials.getString("session"));

Und senden die Herausforderungsantwort-Anfrage an Cognito

AdminRespondToAuthChallengeResult result = client.adminRespondToAuthChallenge(request)

Jetzt ist der Benutzer hoffentlich authentifiziert, sodass wir mit

Erstellen signierter Cookies fortfahren können

Bevor wir beginnen, benötigen Sie für alle Signaturen einen privaten Schlüssel. Für AWS können Sie Ihr Schlüsselpaar für CloudFront in der IAM-Konsole generieren. Unter “Meine Sicherheitsberechtigungen” finden Sie Ihre “CloudFront-Schlüsselpaare”, wo Sie ein neues Schlüsselpaar generieren oder ein bestehendes hochladen können. In jedem Fall müssen Sie die keypairId notieren, die wir später benötigen.

Die erhaltenen Schlüssel sind im PEM-Format, das in Java nicht so einfach zu lesen ist. Daher müssen Sie es in ein anderes Format konvertieren, z. B. DER. Dies kann mit OpenSSL erfolgen:

openssl pkcs8 -topk8 -nocrypt -in <keyfile.pem> -inform PEM -out <keyfile.der> -outform DER

Jetzt haben wir unseren Schlüssel. Das Lesen des Schlüssels aus der Datei in ein RSAPrivateKey erfordert nur vier Zeilen:

final byte[] privKeyByteArray = Files.readAllBytes(Paths.get("path/to/keyfile.der"));
final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privKeyByteArray);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
final PrivateKey privateKey = keyFactory.generatePrivate(keySpec);

Zusätzlich zur keypairId und dem Schlüssel selbst müssen wir den Distributionsdomänennamen Ihrer CloudFront-Verteilung kennen, d. h. den Domänennamen Ihrer Verteilung einschließlich des optionalen Subdomains, ohne Protokoll, Port oder Pfad. Zum Beispiel test.mydomain.com. Dies muss mit dem CNAME Ihrer Verteilung übereinstimmen.

Bevor wir mit dem Erstellen der Cookies beginnen, müssen wir die CloudFront SDK-API zu unseren Maven-Abhängigkeiten hinzufügen:

<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-cloudfront</artifactId>
<version>1.11.176</version>
</dependency>

Das Erstellen des signierten Cookies bedeutet eigentlich, drei Cookies in einem Schritt zu erstellen:

  • die Richtlinie, die beschreibt, worauf Sie Zugriff haben
  • die Signatur
  • die keypairId zur Überprüfung der Signatur

Alle drei Cookies sind in einem Objekt enthalten, dem CookiesForCustomPolicy

//Sie müssen diese drei Parameter bereitstellen
final String distributionDomain = ...;
final String keypairId = ...;
final PrivateKey privateKey = ...;

//das Protokoll für den Zugriff auf Ihre CloudFront-Verteilung, https wird empfohlen
final Protocol protocol = Protocol.https;

//für welche Ressource möchten Sie die signierten Cookies erstellen, verwenden Sie * für alle
final String resourcePath = "*";

//das Datum, an dem das Cookie abläuft, verwenden Sie den folgenden String für eine 30-tägige Lebensdauer
final Date expireDate = Date.from(LocalDate.now().plus(Period.ofDays(30)).atStartOfDay(ZoneId.systemDefault()).toInstant())

//der CIDR-IP-Bereich für den zugreifenden Client, lassen Sie null für keine Einschränkung
final String ipRange = null;

CookiesForCustomPolicy cookies = CloudFrontCookieSigner.getCookiesForCustomPolicy(protocol,
distributionDomain,
privateKey,
resourcePath,
keypairId,
expireDate,
ipRange);

Jetzt haben Sie alle Ihre Cookies in einer Datei, jetzt könnten Sie die Cookies über extrahieren

cookies.getPolicy();
cookies.getSignature();
cookies.getKeyPairId();

All diese können als Domain-Cookies mit einem Web-Framework Ihrer Wahl gesetzt werden, um Zugriff auf eingeschränkte CloudFront-Seiten zu gewähren.

comments powered by Disqus