Protéger les routes d'une API avec un mécanisme d'autorisation JWT

AWSBeginner
Pratiquer maintenant

Introduction

L'API de commandes doit exiger une connexion pour les lectures et les écritures tout en gardant le contrôle de santé public. Vous attacherez des contrôles de jetons aux deux routes de commandes, testerez les requêtes acceptées et rejetées et confirmerez que les appels rejetés ne modifient pas les données métier.

Terminez d'abord Ajouter la connexion à une application avec Cognito et Construire et valider une API de commandes. Cet environnement neuf fournit un backend public fonctionnel et des identités fictives de démonstration ; les ressources et jetons précédents ne sont pas réutilisés.

Lien avec les certifications

Ce laboratoire propose une pratique des sujets d’examen suivants.

Inspecter les routes publiques et se connecter

Dans cette étape, vous établirez l'état public actuel des routes et obtiendrez un vrai jeton d'accès pour le client d'application prévu.

Entrez dans l'espace de travail et gardez les nouveaux fichiers de jetons privés :

cd /home/labex/project
umask 077

Ouvrez AWS View à côté de Terminal pour comparer les requêtes de l'API, les exécutions de la fonction et les commandes stockées. Les journaux des requêtes de la passerelle et les journaux d'exécution Lambda sont distincts : un jeton rejeté doit arrêter la requête avant l'exécution de la fonction.

Lisez l'inventaire non secret des ressources de démonstration. Il identifie le groupe et le client prévus, un autre client, un autre émetteur et le groupe de référence indépendant :

cat scenario.json

Utilisez jq -r pour sélectionner les identifiants principaux et stockez la sortie avec la substitution de commande $(...) :

POOL_ID=$(jq -r '.main.pool' scenario.json)
CLIENT_ID=$(jq -r '.main.client' scenario.json)

Trouvez l'API HTTP préparée par son nom et enregistrez son identifiant généré pour le nettoyage :

API_ID=$(aws apigatewayv2 get-apis \
  --query "Items[?Name=='labex-a05'].ApiId | [0]" \
  --output text)
printf '%s\n' "$API_ID" > api-id.txt

Inspectez les types d'autorisation des routes :

aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query 'Items[].{Route:RouteKey,Authorization:AuthorizationType}'

Les trois routes utilisent initialement NONE. L'annuaire d'utilisateurs seul n'a pas protégé l'API. Construisez son adresse dans l'espace de travail :

API_URL="http://127.0.0.1:8081/api/$API_ID"
curl -i "$API_URL/health"

Vous devez obtenir HTTP 200 et healthy: true, avec une véritable exécution du contrôle de santé dans AWS View et aucune commande.

Connectez l'utilisateur fictif préparé via le client public prévu, en stockant la réponse réelle dans un fichier privé :

aws cognito-idp initiate-auth \
  --client-id "$CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > tokens.json

Extrayez les jetons d'accès et d'identité sans les afficher :

jq -r '.AccessToken' tokens.json > access-token.txt
jq -r '.IdToken' tokens.json > id-token.txt

Utilisez uniquement le jeton d'accès pour lire le nom réel de l'utilisateur de l'application :

aws cognito-idp get-user \
  --access-token "$(cat access-token.txt)" \
  --query Username \
  --output text

Vous devez obtenir labex-demo. AWS View affiche l'utilisateur confirmé et le client préparés. La vérification indépendante valide le jeton réellement émis et l'identité réelle ; elle ne vous reconnecte pas.

Exiger l'autorisation sur les deux routes de commandes

Dans cette étape, vous configurerez l'émetteur et l'audience prévus et appliquerez le même mécanisme d'autorisation JWT aux lectures et aux écritures privées.

Un mécanisme d'autorisation JWT, ou JWT authorizer, vérifie un jeton signé avant qu'API Gateway n'invoque une route protégée. L'émetteur est l'identité signée du groupe Cognito. L'audience est le client d'application prévu : API Gateway valide aud s'il est présent, sinon client_id. Construisez l'émetteur à partir de l'identifiant réel du groupe :

ISSUER="https://cognito-idp.us-east-1.amazonaws.com/$POOL_ID"

Utilisez un marqueur de document intégré sans guillemets pour que le shell développe les deux identifiants dans un fichier de configuration ordinaire :

cat > jwt-config.json <<JSON
{
  "Issuer": "$ISSUER",
  "Audience": ["$CLIENT_ID"]
}
JSON

Créez un mécanisme d'autorisation JWT. Les guillemets simples préservent la source d'identité littérale $request.header.Authorization au lieu de la développer comme une variable du shell :

AUTH_ID=$(aws apigatewayv2 create-authorizer \
  --api-id "$API_ID" \
  --name OrdersUsers \
  --authorizer-type JWT \
  --identity-source '$request.header.Authorization' \
  --jwt-configuration file://jwt-config.json \
  --query AuthorizerId \
  --output text)

Trouvez l'identifiant généré de chaque route de commandes :

READ_ROUTE_ID=$(aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query "Items[?RouteKey=='GET /orders'].RouteId | [0]" \
  --output text)
WRITE_ROUTE_ID=$(aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query "Items[?RouteKey=='POST /orders'].RouteId | [0]" \
  --output text)

API Gateway ne distingue pas systématiquement les jetons d'accès des jetons d'identité. Ces jetons d'accès Cognito natifs issus du flux de mot de passe contiennent aws.cognito.signin.user.admin ; exiger cette portée rejette le jeton d'identité, qui ne la contient pas. Cette portée désigne l'accès aux opérations Cognito que l'utilisateur effectue pour lui-même, pas l'administration AWS ni la propriété des commandes. Cet exercice ne crée pas de portée métier personnalisée.

Exigez JWT et cette portée sur la route de lecture :

aws apigatewayv2 update-route \
  --api-id "$API_ID" \
  --route-id "$READ_ROUTE_ID" \
  --authorization-type JWT \
  --authorizer-id "$AUTH_ID" \
  --authorization-scopes aws.cognito.signin.user.admin \
  --query '{Route:RouteKey,Authorization:AuthorizationType,Scopes:AuthorizationScopes}'

Appliquez la même exigence à la route d'écriture :

aws apigatewayv2 update-route \
  --api-id "$API_ID" \
  --route-id "$WRITE_ROUTE_ID" \
  --authorization-type JWT \
  --authorizer-id "$AUTH_ID" \
  --authorization-scopes aws.cognito.signin.user.admin \
  --query '{Route:RouteKey,Authorization:AuthorizationType,Scopes:AuthorizationScopes}'

Le stage $default préparé utilise AutoDeploy : les modifications des routes s'appliquent donc automatiquement. Relisez tous les types de routes :

aws apigatewayv2 get-routes \
  --api-id "$API_ID" \
  --query 'Items[].{Route:RouteKey,Authorization:AuthorizationType}'

Vous devez obtenir JWT sur les deux routes de commandes et NONE sur GET /health. Dans AWS View, comparez l'émetteur réel, l'audience prévue, les identifiants des mécanismes d'autorisation des routes et la portée exigée. Créer un mécanisme d'autorisation sans l'attacher à chaque route privée ne protège pas ces routes.

Exemple AWS View avec un contrôle de santé public et des routes de commandes protégées par JWT

Exemple : les deux routes de commandes partagent le mécanisme d'autorisation et la portée prévus, tandis que le contrôle de santé reste public. Les identifiants générés des ressources diffèrent dans votre VM.

Le mécanisme d'autorisation accepte ou rejette la requête privée avant l'exécution du gestionnaire.

Tester les requêtes acceptées et rejetées

Dans cette étape, vous prouverez l'accès métier réel de l'utilisateur prévu et le rejet, avant Lambda, des jetons absents ou inadaptés.

Un jeton au porteur authentifie toute personne qui le présente. Lisez-le depuis le fichier privé de transmission dans l'en-tête Authorization. Continuez à utiliser curl -i pour afficher uniquement les en-têtes et le corps de la réponse ; n'utilisez pas la journalisation détaillée des en-têtes de requête. Envoyez une commande valide avec une quantité de 4 :

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat access-token.txt)" -H 'Content-Type: application/json' --data '{"id":"signed-order","quantity":4}'

Vous devez obtenir HTTP 201 et un total de 1100 (4 × 250 + 100). Récupérez la même commande stockée avec le jeton d'accès :

curl -i "$API_URL/orders?id=signed-order" -H "Authorization: Bearer $(cat access-token.txt)"

Vous devez obtenir HTTP 200 avec le même identifiant, la même quantité et le même total calculé. Inspectez indépendamment l'élément natif :

aws dynamodb get-item \
  --table-name labex-a05-orders \
  --key '{"id":{"S":"signed-order"}}' \
  --consistent-read \
  --query Item

Répétez maintenant la lecture sans jeton :

curl -i "$API_URL/orders?id=signed-order"

Vous devez obtenir HTTP 401 Unauthorized, sans enregistrement privé dans la réponse. Testez aussi une écriture anonyme :

curl -i -X POST "$API_URL/orders" -H 'Content-Type: application/json' --data '{"id":"reject-missing","quantity":9}'

Elle doit également renvoyer 401 et ne créer aucun élément. Les lectures et les écritures privées exigent toutes deux une autorisation.

Le jeton de démonstration fourni avec une signature incorrecte contient un octet de signature modifié. Il n'est pas fiable, même si ses revendications visibles semblent plausibles :

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat bad-signature-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-signature","quantity":9}'

Vous devez obtenir 401. Un jeton réellement émis pour un autre client d'application est également inadapté à cette API. Lisez cet identifiant client non secret, connectez-vous via ce client et extrayez son jeton d'accès dans un fichier privé :

WRONG_CLIENT_ID=$(jq -r '.wrong_client' scenario.json)
aws cognito-idp initiate-auth \
  --client-id "$WRONG_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > wrong-client.json
jq -r '.AccessToken' wrong-client.json > wrong-client-token.txt
curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat wrong-client-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-client","quantity":9}'

Vous devez obtenir 401 : une signature valide seule ne correspond pas à l'audience configurée. Utilisez ensuite l'autre émetteur Cognito préparé, qui possède son propre utilisateur fictif et son client public :

OTHER_POOL_ID=$(jq -r '.other.pool' scenario.json)
OTHER_CLIENT_ID=$(jq -r '.other.client' scenario.json)
aws cognito-idp initiate-auth \
  --client-id "$OTHER_CLIENT_ID" \
  --auth-flow USER_PASSWORD_AUTH \
  --auth-parameters file://sign-in.json \
  --query AuthenticationResult \
  --output json > wrong-issuer.json
jq -r '.AccessToken' wrong-issuer.json > wrong-issuer-token.txt
curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat wrong-issuer-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-issuer","quantity":9}'

Vous devez obtenir 401, car l'émetteur signé diffère du groupe attendu. Le jeton expiré fourni est signé avec un exp déjà passé ; il teste la règle temporelle sans attendre l'expiration d'une session réelle :

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat expired-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-expired","quantity":9}'

Vous devez obtenir 401. Un jeton d'identité est signé pour ce client, mais ne contient pas la portée de jeton d'accès exigée par la route :

curl -i -X POST "$API_URL/orders" -H "Authorization: Bearer $(cat id-token.txt)" -H 'Content-Type: application/json' --data '{"id":"reject-id-token","quantity":9}'

Vous devez obtenir 403 pour une portée insuffisante. La signature, l'émetteur, l'audience et la durée de validité du jeton sont nécessaires, mais ne fournissent pas les permissions manquantes.

Vérifiez que le contrôle de santé reste public et que seule l'écriture métier valide a été stockée :

curl -i "$API_URL/health"
aws dynamodb scan --table-name labex-a05-orders --query Items

Vous devez obtenir 200 pour le contrôle de santé et exactement signed-order avec un total de 1100. Dans AWS View, comparez les API requests réelles avec CloudWatch Logs : une autorisation rejetée produit un résultat 401/403 à la passerelle et aucune exécution correspondante de la fonction. Les requêtes privées réussies ont de véritables événements et réponses Lambda, avec les en-têtes d'identifiants d'accès exclus. Ces résultats d'exécution/API et les données natives, et non les captures d'écran ou les marqueurs locaux de réussite, constituent les preuves d'évaluation.

Exemple de requêtes réelles de l'API acceptées et rejetées

Exemple : les écritures et lectures valides de commandes ont renvoyé 201/200 ; les jetons inadaptés ont renvoyé 401/403. La liste des exécutions de la fonction et la table native confirment indépendamment que les requêtes rejetées n'ont effectué aucune écriture métier.

Supprimer les routes protégées et vos ressources

Dans cette étape, vous supprimerez votre API, les identités de démonstration et les données, en conservant uniquement les références indépendantes.

Votre inventaire comprend cette API et son mécanisme d'autorisation, la fonction et son groupe de journaux, le groupe de journaux des requêtes de la passerelle, la table des commandes, la politique OrdersData du rôle, le groupe principal (deux clients et un utilisateur), le groupe de l'autre émetteur (un client et un utilisateur) et les fichiers privés d'entrée et de réponse. Le groupe, la table et les journaux de référence ainsi que la politique FunctionLogs du rôle sont indépendants et doivent rester.

Supprimez l'API, y compris son mécanisme d'autorisation, ses routes, son intégration et son stage :

aws apigatewayv2 delete-api --api-id "$API_ID"
aws lambda delete-function --function-name labex-a05-orders-api
aws logs delete-log-group --log-group-name /aws/lambda/labex-a05-orders-api
aws logs delete-log-group --log-group-name /aws/apigateway/labex-a05
aws dynamodb delete-table \
  --table-name labex-a05-orders \
  --query TableDescription.TableName \
  --output text
aws iam delete-role-policy --role-name labex-a05-execution --policy-name OrdersData

Supprimez les utilisateurs fictifs et chaque client d'application créé, puis leurs groupes :

aws cognito-idp admin-delete-user --user-pool-id "$POOL_ID" --username labex-demo
aws cognito-idp delete-user-pool-client --user-pool-id "$POOL_ID" --client-id "$CLIENT_ID"
aws cognito-idp delete-user-pool-client \
  --user-pool-id "$POOL_ID" \
  --client-id "$WRONG_CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$POOL_ID"
aws cognito-idp admin-delete-user --user-pool-id "$OTHER_POOL_ID" --username labex-demo
aws cognito-idp delete-user-pool-client \
  --user-pool-id "$OTHER_POOL_ID" \
  --client-id "$OTHER_CLIENT_ID"
aws cognito-idp delete-user-pool --user-pool-id "$OTHER_POOL_ID"

Interrogez les inventaires avec succès pour prouver l'absence des ressources ; les erreurs de réseau ou d'authentification ne prouvent pas le nettoyage :

aws apigatewayv2 get-apis --query 'Items[].Name'
aws lambda list-functions --query 'Functions[].FunctionName'
aws cognito-idp list-user-pools --max-results 60 --query 'UserPools[].Name'
aws dynamodb list-tables --query TableNames

Vous devez obtenir des listes d'API et de fonctions vides, uniquement labex-a05-reference dans les listes des groupes et des tables, et les données et journaux de référence conservés dans AWS View. Supprimez uniquement les fichiers privés listés :

rm -f tokens.json access-token.txt id-token.txt wrong-client.json wrong-client-token.txt wrong-issuer.json wrong-issuer-token.txt expired-token.txt future-iat-token.txt future-nbf-token.txt bad-signature-token.txt sign-in.json

La suppression des seuls fichiers locaux ne constitue pas une révocation générale des JWT côté serveur. Vos utilisateurs fictifs, clients, groupes et API ont également été supprimés. Les ressources de référence, la révocation finale des identifiants d'accès et l'arrêt de la VM restent à la charge de l'auteur après ces contrôles de ressources en lecture seule.

Résumé

Vous avez configuré un mécanisme d'autorisation JWT Cognito et l'avez exigé pour les lectures et écritures privées tout en gardant le contrôle de santé public. Vous avez testé des requêtes réelles acceptées et les rejets de jetons absents, altérés, destinés au mauvais client, issus du mauvais émetteur, expirés ou de type identité. Vous avez corrélé les résultats réels de la passerelle avec les exécutions Lambda natives et les données stockées, puis supprimé vos ressources d'API, d'identité et de données ainsi que les fichiers privés, en conservant les références. L'autorisation JWT n'a pas mis en œuvre la propriété de chaque commande.