aws, authz, serverless

PEP und PDP für sichere Autorisierung mit Cognito

2025-01-30
This post cover image
aws cloud serverless AuthZ

Diese Datei wurde automatisch von KI ubersetzt, es konnen Fehler auftreten

In einem meiner früheren Beiträge Building a serverless connected BBQ as SaaS - Teil 4 - AuthZ habe ich das Thema Authentifizierung und Autorisierung mit verteilten PEPs (Policy Enforcement Points) und einem zentralen PDP (Policy Decision Point) angesprochen. In diesem Beitrag werde ich noch tiefer gehen und diesen Aufbau erweitern. Ich werde untersuchen, wie diese Konzepte in der Praxis funktionieren, welche Vorteile sie bieten und wie wir sie in unserer serverlosen Architektur mit AWS Lambda, API Gateway und Cognito User Pools nutzen können.

Außerdem werde ich über das Role-Based Access Control (RBAC)-Modell sprechen, wie man es mit Cognito Groups und DynamoDB implementiert und wie Caching die Leistung unseres Autorisierungsystems steigern kann.

Die gesamte Einrichtung, mit detaillierten Bereitstellungsanweisungen und allem Code, findet man auf Serverless Handbook PEP und PDP

Lassen Sie uns mit einer kurzen Wiederholung beginnen.

Authentifizierung (AuthN) vs. Autorisierung (AuthZ)

Es ist entscheidend, zwischen Authentifizierung und Autorisierung zu unterscheiden, zwei Begriffe, die oft vermischt werden und die ich in vielen Gelegenheiten erklären musste, aber sehr unterschiedliche Zwecke erfüllen.

Authentifizierung (AuthN)

Bei der Authentifizierung geht es darum, die Identität zu überprüfen. Sie beantwortet die Frage: Wer bist du? Wenn sich ein Benutzer in unserer Anwendung anmeldet, stellt die Authentifizierung sicher, dass er der ist, für den er sich ausgibt. Dies könnte etwas so Einfaches wie ein Benutzername und Passwort oder eine komplexere Multi-Faktor-Authentifizierung (MFA) sein.

Autorisierung (AuthZ)

Sobald die Identität eines Benutzers authentifiziert ist, kommt die Autorisierung ins Spiel. Dieser Prozess beantwortet die Frage: Was kannst du tun? Die Autorisierung bestimmt, auf welche Ressourcen, Daten und Aktionen ein Benutzer basierend auf seinen Rollen und Berechtigungen zugreifen darf.

Was sind PEP und PDP?

Bevor wir uns damit beschäftigen, wie man sie in AWS implementiert, ist es wichtig zu verstehen, welche Rollen PEP und PDP bei der Autorisierung spielen.

PEP (Policy Enforcement Point)

Einfach ausgedrückt, ist der PEP der Torwächter. Es sind die Punkte in unserem System, an denen Zugriffsentscheidungen durchgesetzt werden. Wenn ein Benutzer versucht, auf eine geschützte Ressource zuzugreifen, ist der PEP die Komponente, die überprüft, ob die Anfrage basierend auf den Berechtigungen des Benutzers erlaubt oder abgelehnt wird.

In unserem Fall fungiert der Lambda Authorizer in API Gateway als PEP. Der Lambda Authorizer fängt jede eingehende API-Anfrage ab, validiert das JWT-Token (typischerweise von Cognito User Pool oder einem anderen Identitätsanbieter) und leitet die Benutzerinformationen (Claims) zur Autorisierungsbewertung an den PDP weiter.

Der PEP stellt sicher, dass das JWT gültig ist, prüft dessen Ablaufdatum, überprüft die Signatur und validiert Claims (wie aud, iss und sub). Anschließend gibt er die Claims an den PDP weiter, um eine endgültige Entscheidung darüber zu treffen, ob der Benutzer berechtigt ist, auf die angeforderte Ressource zuzugreifen.

PDP (Policy Decision Point)

Der PDP ist der Ort, an dem die Autorisierungslogik resides. Nachdem der PEP das JWT überprüft und sichergestellt hat, dass das Token gültig ist, entscheidet der PDP, ob der Benutzer berechtigt ist, auf die angeforderte Ressource zuzugreifen, basierend auf seinen Rollen, Berechtigungen oder Richtlinien.

Der PDP ist ein separater Mikrodienst. In unserem Fall eine Lambda-Funktion, die die eigentliche Autorisierungsentscheidung trifft. Er überprüft die Rollen des Benutzers, die im groups-Claim im JWT (von Cognito) gespeichert sind, und vergleicht sie mit den Berechtigungen, die für den Zugriff auf eine bestimmte Ressource erforderlich sind und in einem Datenspeicher abgelegt sind. In unserem Fall werden wir DynamoDB verwenden.

Der PDP validiert, ob der Benutzer die notwendigen Berechtigungen hat (wie Admin, User oder Manager), um auf die Ressource zuzugreifen (z. B. GET /admin, POST /profile). Der PDP kann auch zusätzliche Geschäftslogik einbeziehen, wie z. B. die Überprüfung von zeitbasiertem Zugriff oder Geo-Fencing.

Vorteile der Verwendung von PEP und PDP bei der Autorisierung

Die Implementierung von verteilten PEP und zentralen PDP bietet mehrere Vorteile, insbesondere wenn unsere Anwendungen skalieren.

Trennung von Aufgaben Durch die Aufteilung der Aufgaben in Durchsetzung (PEP) und Entscheidung (PDP) erhalten wir saubereren, besser wartbaren Code. Der Lambda Authorizer (PEP) konzentriert sich rein auf Validierung und Durchsetzung. Während der PDP der Richtlinienauswertung gewidmet ist.

Reduzierte Latenz: Indem wir PEPs nahe an den Ort platzieren, an dem Entscheidungen durchgesetzt werden müssen, können wir die Latenz reduzieren, mit einer Caching-Strategie kann diese noch weiter reduziert werden.

Verwaltung: Mit einem zentralen PDP ist unsere gesamte Autorisierungslogik an einem Ort zentriert. Dies erleichtert die Verwaltung und Aktualisierung von Richtlinien, wenn sich unsere Anforderungen weiterentwickeln. Ob es sich um die Änderung von Rollen oder die Hinzufügung neuer Berechtigungssätze handelt, ein zentraler PDP reduziert den Aufwand, Richtlinien an mehreren Stellen zu aktualisieren.

Konsistenz und Compliance: Jede Anfrage wird gegen denselben Satz von Richtlinien ausgewertet, was konsistente Entscheidungen in unserem gesamten System gewährleistet.

Skalierbarkeit: Sowohl die PEP- als auch die PDP-Komponenten skalieren unabhängig nach Bedarf. Wenn unser System ein größeres Anfragenvolumen bewältigen muss, können API Gateway und Lambda automatisch skalieren. Darüber hinaus kann der PDP durch die Implementierung von Caching für die Leistung optimiert werden.

Flexibilität: Ein PDP ermöglicht es uns, das Autorisierungsmodell an unsere Bedürfnisse anzupassen. Wenn sich unsere Anforderungen ändern (zum Beispiel die Umstellung auf attributbasierte Zugriffssteuerung (ABAC) oder die Einführung eines feineren Berechtigungssystems), können wir den PDP leicht anpassen, um diese Änderungen zu berücksichtigen, ohne andere Teile des Systems zu beeinflussen.

Verwenden von PEP und PDP in AWS mit serverloser Architektur

In AWS passt die Integration von PEP und PDP perfekt zu serverlosen Komponenten wie Lambda und API Gateway.

PEP - API Gateway Lambda Authorizer

Wenn ein Client eine Anfrage an unseren API Gateway-Endpunkt sendet, fängt der Lambda Authorizer (PEP) die Anfrage ab, bevor sie unseren Backendservice erreicht. Unsere Implementierung wird mehrere Schlüsselschritte ausführen.

JWT-Validierung: Er decodiert das JWT, validiert die Signatur und prüft, ob das Token abgelaufen ist.

Weiterleitung von Claims: Nach der Überprüfung des Tokens leitet der Lambda Authorizer die Claims (wie sub, groups und role) zur weiteren Autorisierungsprüfung an den PDP weiter. In unserer Lösung werden wir tatsächlich das gesamte JWT-Token weiterleiten.

Um die Anzahl der Aufrufe an unseren PEP und auch PDP zu reduzieren, können wir das Autorisierungscache nutzen, das in API Gateway existiert.

PDP - Autorisierungslogik Lambda-Funktion

Der PDP ist in unserer Fall als separate Lambda-Funktion implementiert und erhält das gesamte JWT-Token oder Claims, um die Autorisierungslogik auszuführen, die mehrere Schritte umfasst.

  • Überprüfen der Rolle des Benutzers (mit dem groups-Claim von Cognito).
  • Abfragen einer DynamoDB-Tabelle, die Rollen-zu-Berechtigungs-Zuordnungen enthält (z. B. welche Rollen Zugriff auf welche API-Endpunkte haben).
  • Auswerten, ob die Rolle des Benutzers mit den erforderlichen Berechtigungen für die angeforderte Ressource oder das API-Endpunkt übereinstimmt.

ID-Token vs Access-Token

Während wir den PEP- und PDP-Workflow implementieren, ist es wichtig, den Unterschied zwischen ID-Tokens und Access-Tokens zu verstehen, da beide oft in AutorisierungsWorkflows verwendet werden.

ID-Token

Das ID-Token wird hauptsächlich für die Authentifizierung verwendet und enthält Informationen darüber, wer der Benutzer ist. Es enthält Claims über die Identität des authentifizierten Benutzers, wie Name, E-Mail und Telefonnummer.

Access-Token

Das Access-Token wird verwendet, um dem Benutzer Zugriff auf geschützte Ressourcen zu gewähren, Autorisierung. Das Access-Token enthält Informationen über die Berechtigungen des Benutzers, wie z. B. welche Ressourcen er zugreifen darf und welche Bereiche ihm gewährt wurden, die definieren, was der Benutzer tun kann (z. B. read:profile, write:profile). Das Access-Token enthält nicht den aud-Claim.

Tokenanpassung in Cognito

Mit dem Pre Token Generation Lambda-Trigger konnten wir früher nur das ID-Token anpassen, daher wurde es oft auch für die Autorisierung verwendet. Mit der Einführung des neuen V2-Events in Cognito User Pools können wir sowohl das ID- als auch das Access-Token anpassen.

Implementierung von PEP und PDP

Nach dieser Einführung lassen Sie uns in die Implementierung eines PEP und PDP mit RBAC eintauchen. Unser PEP wird der Lambda Authorizer in API Gateway sein und unser PDP wird eine separate Lambda-Funktion sein. Der PDP wird Cognito Groups und DynamoDB für die RBAC-Autorisierungslogik verwenden.

Architekturüberblick

Nur als Erinnerung, der gesamte Code und die gesamte Architektur finden Sie auf Serverless Handbook PEP und PDP

In dieser Lösung werden wir unseren PEP mit Lambda Authorizer in API Gateway implementieren. Der PDP wird in diesem Fall ebenfalls mit einer Lambda-Funktion implementiert. Wir werden Benutzern eine Rolle zuweisen, indem wir Cognito Groups verwenden, und wir speichern eine Rollen-Berechtigungs-Zuordnung in DynamoDB.

Bild zeigt den Architekturüberblick

Um den Ablauf während eines API-Zugriffs besser zu verstehen.

Bild zeigt den Aufrufablauf

Wie man sieht, werden wir kein API Gateway für unseren PDP verwenden. Stattdessen wird unser PEP die PDP-Lambda-Funktion aufrufen. Es gibt natürlich Vor- und Nachteile bei diesem Ansatz. Auf der Pro-Seite haben wir eine geringere Latenz, ein direkter Lambda-Aufruf ist oft schneller als ein API-Aufruf. Geringere Kosten, da wir nicht für die API-Gateway-Aufrufe bezahlen müssen. Auf der Rückseite schaffen wir eine engere Kopplung und die Änderung der PDP-Implementierung könnte schwieriger werden. Wir müssten einen separaten Cache im PDP implementieren, mit einem API Gateway könnten wir den API Gateway-Cache nutzen.

Die von Ihnen gewählte Vorgehensweise muss jedoch fallweise entschieden werden, es gibt keine allgemeine Regel, wie genau dies implementiert werden soll.

Bereitstellung von Authentifizierung und Cognito

Das Erste, was wir tun werden, ist, Cognito und die benötigten Ressourcen für die Anmeldung bereitzustellen und einzurichten. Wir werden den Cognito User Pool einrichten, die verwaltete Anmeldung konfigurieren und eine einfache Website, die die Rückmeldungen von Cognito verarbeitet und unsere JWT-Tokens anzeigt. Der Einfachheit halber wird es nur eine statische HTML-Seite von CloudFront und einige Lambda@Edge-Funktionen sein. Ich werde das Setup verwenden, das ich in diesem Blog-Beitrag beschrieben habe, also empfehle ich Ihnen für eine detaillierte Betrachtung, diesen zu lesen.

Als ersten Schritt deployen wir also Lambda@Edge, CloudFront-Distribution und SSL-Zertifikat von Serverless Handbook PEP und PDP

Als nächstes lassen Sie uns Cognito bereitstellen und einrichten. Wir werden den UserPool, einen Client, den Anmelde-Stil usw. erstellen.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Erstellt den User Pool und Client für die Authentifizierung
Parameter:
  ApplicationName:
    Type: String
    Description: Die Anwendung, die diesen Aufbau besitzt.
  DomainName:
    Type: String
    Description: Der Domänenname, der für Cloudfront verwendet werden soll
  HostedAuthDomainPrefix:
    Type: String
    Description: Der Domänenpräfix, der für die UserPool-gehostete UI verwendet wird <HostedAuthDomainPrefix>.auth.[region].amazoncognito.com

Ressourcen:
  UserPool:
    Type: AWS::Cognito::UserPool
    Eigenschaften:
      UsernameConfiguration:
        CaseSensitive: false
      AutoVerifiedAttributes:
        - email
      UserPoolName: !Sub ${ApplicationName}-user-pool
      Schema:
        - Name: email
          AttributeDataType: String
          Mutable: false
          Required: true
        - Name: name
          AttributeDataType: String
          Mutable: true
          Required: true

  UserPoolClient:
    Type: AWS::Cognito::UserPoolClient
    Eigenschaften:
      UserPoolId: !Ref UserPool
      GenerateSecret: True
      AllowedOAuthFlowsUserPoolClient: true
      CallbackURLs:
        - !Sub https://${DomainName}/signin
      AllowedOAuthFlows:
        - code
        - implicit
      AllowedOAuthScopes:
        - phone
        - email
        - openid
        - profile
      SupportedIdentityProviders:
        - COGNITO

  HostedUserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Eigenschaften:
      Domain: !Ref HostedAuthDomainPrefix
      ManagedLoginVersion: 2
      UserPoolId: !Ref UserPool

  ManagedLoginStyle:
    Type: AWS::Cognito::ManagedLoginBranding
    Eigenschaften:
      ClientId: !Ref UserPoolClient
      UserPoolId: !Ref UserPool
      UseCognitoProvidedValues: true

  UserPoolIdParameter:
    Type: AWS::SSM::Parameter
    Eigenschaften:
      Name: !Sub /${ApplicationName}/userPoolId
      Type: String
      Value: !Ref UserPool
      Beschreibung: SSM-Parameter für die User Pool-Id
      Tags:
        ApplicationName: !Ref ApplicationName

  UserPoolHostedUiParameter:
    Type: AWS::SSM::Parameter
    Eigenschaften:
      Name: !Sub /${ApplicationName}/userPoolHostedUi
      Type: String
      Value: !Sub https://${HostedAuthDomainPrefix}.auth.${AWS::Region}.amazoncognito.com/login?client_id=${UserPoolClient}&response_type=code&scope=email+openid+phone+profile&redirect_uri=https://${DomainName}/signin
      Beschreibung: SSM-Parameter für die User Pool-gehostete UI
      Tags:
        ApplicationName: !Ref ApplicationName

Ausgaben:
  CognitoUserPoolJwksUri:
    Value: !Sub https://cognito-idp.${AWS::Region}.amazonaws.com/${UserPool}/.well-known/jwks.json
    Beschreibung: Die UserPool-jwks-URI
    Export:
      Name: !Sub ${AWS::StackName}:jwks-url
  CognitoUserPoolID:
    Value: !Ref UserPool
    Beschreibung: Die UserPool-ID
  CognitoAppClientID:
    Value: !Ref UserPoolClient
    Beschreibung: Der App-Client
    Export:
      Name: !Sub ${AWS::StackName}:app-audience
  CognitoUrl:
    Beschreibung: Die URL
    Value: !GetAtt UserPool.ProviderURL
  CognitoHostedUI:
    Value: !Sub https://${HostedAuthDomainPrefix}.auth.${AWS::Region}.amazoncognito.com/login?client_id=${UserPoolClient}&response_type=code&scope=email+openid+phone+profile&redirect_uri=https://${DomainName}/signin
    Beschreibung: Die gehostete UI-URL

Nach dieser Bereitstellung können wir zur Konsole wechseln und Gruppen erstellen, denen Benutzer hinzugefügt werden können. Ich werde drei Gruppen erstellen, Admin, Developer und Test. Klicken Sie auf Gruppe erstellen und geben Sie einen Namen ein. Der Gruppenname würde die Rolle darstellen, die der Benutzer haben wird und bestimmen, welche Berechtigungen er/sie erhalten wird, mehr zu diesem Setup weiter unten.

Bild zeigt Cognito-Gruppen

Wir können dann einige Benutzer erstellen und ihnen eine der Gruppen zuweisen.

Um dieses Setup zu testen, können wir zur mit der CloudFront-Distribution bereitgestellten Webseite navigieren und die JWT-Tokens, Cookies inspizieren.

Bild zeigt Cookies

Wenn wir das Access-Token kopieren und entschlüsseln, verwende ich jwt.io, können wir sehen, dass mein Benutzer den Claim cognito:groups hat, den unser PEP und PDP später für Berechtigungen verwenden werden.

Einrichtung und Bereitstellung von PDP

Als nächstes können wir unseren Autorisierungsdienst, unseren PDP, bereitstellen, der für die Entscheidungsfindung bezüglich Berechtigungen verantwortlich ist.

Die Logik wird in einer Lambda-Funktion implementiert und um rollenbasierte Berechtigungen zu verwalten, erstellen wir eine DynamoDB-Tabelle, die Berechtigungen für jede Rolle speichert. Jede Berechtigung definiert, auf welche Ressourcen der Benutzer zugreifen kann, dies könnte ein bestimmtes API-Ende oder eine HTTP-Methode sein, aber natürlich nicht darauf beschränkt. Wir werden die Tabellendaten modellieren

PK (Partition Key): Die Rolle (z. B. Admin, User). SK (Sort Key): Die Ressource, zum Beispiel Ende und Methode z. B. GET /unicorn. Action: Die Aktion z. B. GET, PUT, WRITE, READ, LIST usw. Ressource: Die Ressource, zum Beispiel das Ende /unicorn Effect: Der Effekt, Erlauben oder Verweigern Beschreibung: Eine Beschreibung der Berechtigung.

PKSKAktionRessourceEffektBeschreibung
AdminGET /unicornGET/unicornErlaubenAdmin kann alle Einhörner zugreifen
TestPOST /unicornPOST/unicornErlaubenTest kann Einhörner posten
EntwicklerDELETE /unicornLÖSCHEN/unicornVerweigernManager kann ein Einhorn nicht löschen

Dies ermöglicht es uns, effizient die Berechtigungen für jede Rolle mit einer einfachen DynamoDB-Abfrage nachzuschlagen.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Connected BBQ Application Tenant Service
Parameter:
  ApplicationName:
    Type: String
    Description: Name der übergeordneten Anwendung
  UserManagementStackName:
    Type: String
    Description: Der Name des Stacks, der den Benutzermanagement-Teil enthält, z. B. den Cognito UserPool

Globals:
  Funktion:
    Timeout: 30
    MemorySize: 2048
    Architectures:
      - arm64
    Runtime: python3.12

Ressourcen:
  PermissionsTable:
    Type: AWS::DynamoDB::Table
    Eigenschaften:
      TableName:
        Fn::Sub: ${ApplicationName}-pdp-role-permission-map
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
      - AttributeName: PK
        AttributeType: S
      - AttributeName: SK
        AttributeType: S
      KeySchema:
      - AttributeName: PK
        KeyType: HASH
      - AttributeName: SK
        KeyType: RANGE

  LambdaPDPFunction:
    Type: AWS::Serverless::Function
    Eigenschaften:
      CodeUri: Lambda/AuthZ
      Handler: authz.handler
      Policies:
        - DynamoDBReadPolicy:
            TableName: !Ref PermissionsTable
      Umgebung:
        Variablen:
          JWKS_URL:
            Fn::ImportValue: !Sub ${UserManagementStackName}:jwks-url
          AUDIENCE:
            Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
          PERMISSIONS_TABLE:
            !Ref PermissionsTable

Ausgaben:
  PDPLambdaArn:
    Value: !GetAtt LambdaPDPFunction.Arn
    Beschreibung: Die ARN der PDP-Lambda-Funktion
    Export:
      Name: !Sub ${AWS::StackName}:pdp-lambda-arn
  PDPLambdaName:
    Value: !Ref LambdaPDPFunction
    Beschreibung: Der Name der PDP-Lambda-Funktion
    Export:
      Name: !Sub ${AWS::StackName}:pdp-lambda-name

Rollenautorisierungslogik

Die PDP-Lambda decodiert das JWT, ruft die Rolle aus dem cognito:groups-Claim ab und fragt die DynamoDB-Tabelle ab, um zu überprüfen, ob die Rolle die Berechtigung hat, auf die angeforderte Ressource zuzugreifen.

import os
import json
import jwt
import boto3
from jwt import PyJWKClient
from botocore.exceptions import ClientError

dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(os.environ["PERMISSIONS_TABLE"])
JWKS_URL = os.environ["JWKS_URL"]
AUDIENCE = os.environ["AUDIENCE"]


def handler(event, context):
  data = event
    jwt_token = data["jwt_token"]
    resource = data["resource"]
    action = data["action"]

    return check_authorization(jwt_token, action, resource)


def check_authorization(jwt_token, action, resource):
    try:
        jwks_client = PyJWKClient(JWKS_URL)
        signing_key = jwks_client.get_signing_key_from_jwt(jwt_token)

        decoded_token = jwt.decode(
            jwt_token,
            signing_key.key,
            algorithms=["RS256"],
            audience=AUDIENCE,
        )

        role = (
            decoded_token["cognito:groups"][0]
            if "cognito:groups" in decoded_token
            else None
        )

        if not role:
            raise Exception("Unautorisiert: Rolle nicht im Token gefunden")

        if validate_permission(role, action, resource):
            response_body = generate_access(
                decoded_token["sub"], "Erlauben", action, resource
            )

            return {
                "statusCode": 200,
                "body": json.dumps(response_body),
                "headers": {"Content-Type": "application/json"},
            }

    except Exception as e:
        print(f"Autorisierungsfehler: {str(e)}")

    response_body = generate_access(decoded_token["sub"], "Verweigern", action, resource)

    return {
        "statusCode": 403,
        "body": json.dumps(response_body),
        "headers": {"Content-Type": "application/json"},
    }


def validate_permission(role, action, resource):
    print(f"validate_permission Rolle: {role}, Aktion: {action}, Ressource: {resource}")
    try:
        response = table.query(
            KeyConditionExpression="PK = :role AND SK = :endpoint",
            ExpressionAttributeValues={
                ":role": role,
                ":endpoint": f"{action} {resource}",
            },
        )
        if response["Items"] and response["Items"][0]["Effect"] == "Erlauben":
            return True
        else:
            return False
    except ClientError as e:
        print(f"Fehler bei der DynamoDB-Abfrage: {e}")
        return False


def generate_access(principal, effect, action, resource):
    auth_response = {
        "principalId": principal,
        "effect": effect,
        "action": action,
        "resource": resource,
    }
    return auth_response

Bereitstellung von API und PEP

Jetzt können wir unsere API und PEP, Lambda Authorizer, bereitstellen.

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: Erstellen Sie die API für die Selbstbedienung von Zertifikaten
Parameter:
  ApplicationName:
    Type: String
    Description: Name der übergeordneten Anwendung
  UserManagementStackName:
    Type: String
    Description: Der Name des Stacks, der den Benutzermanagement-Teil enthält, z. B. den Cognito UserPool
  PDPStackName:
    Type: String
    Description: Der Name des Stacks, der den PDP-Dienst enthält

Globals:
  Funktion:
    Timeout: 30
    MemorySize: 2048
    Runtime: python3.12

Ressourcen:
  LambdaGetUnicorn:
    Type: AWS::Serverless::Function
    Eigenschaften:
      CodeUri: Lambda/API/GetUnicorn
      Handler: handler.handler
      Ereignisse:
        GetUnicorns:
          Type: Api
          Eigenschaften:
            Path: /unicorn
            Methode: get
            RestApiId: !Ref UnicornApi

  UnicornApi:
    Type: AWS::Serverless::Api
    Eigenschaften:
      Beschreibung: API zum Erstellen und Verwalten von Einhörnern
      Name: !Sub ${ApplicationName}-api
      StageName: prod
      OpenApiVersion: '3.0.1'
      AlwaysDeploy: true
      EndpointConfiguration: REGIONAL
      Cors:
        AllowMethods: "'GET,PUT,POST,DELETE,OPTIONS'"
        AllowHeaders: "'Content-Type,Authorization,X-Amz-Date,X-Api-Key,X-Amz-Security-Token'"
        AllowOrigin: "'*'"
      Auth:
        AddDefaultAuthorizerToCorsPreflight: false
        Authorizers:
          LambdaRequestAuthorizer:
            FunctionArn: !GetAtt LambdaApiAuthorizer.Arn
            FunctionPayloadType: REQUEST
            Identity: 
              Headers: 
                - Authorization
              ReauthorizeEvery: 600
        StandardAuthorizer: LambdaRequestAuthorizer

  LambdaApiAuthorizer:
    Type: AWS::Serverless::Function
    Eigenschaften:
      CodeUri: Lambda/Authorizer/
      Handler: auth.handler
      Policies:
        - LambdaInvokePolicy:
            FunctionName: 
              Fn::ImportValue: !Sub ${PDPStackName}:pdp-lambda-name
      Umgebung:
        Variablen:
          JWKS_URL:
            Fn::ImportValue: !Sub ${UserManagementStackName}:jwks-url
          AUDIENCE:
            Fn::ImportValue: !Sub ${UserManagementStackName}:app-audience
          PDP_AUTHZ_ENDPOINT: 
            Fn::ImportValue: !Sub ${PDPStackName}:pdp-lambda-name

Wir setzen unseren PEP als Standard-Autorisierer, damit er zu jeder Ressource und Methode hinzugefügt wird. Um die Anzahl der Aufrufe an unseren PDP zu reduzieren, wird das Autorisierungscache in API Gateway mit einer TTL von 600 Sekunden verwendet.

PEP-Autorisierungslogik

Der PEP Lambda Authorizer decodiert das JWT, überprüft die Gültigkeit und ruft dann den PDP für eine endgültige Berechtigungsentscheidung auf.

import os
import json
import jwt
import boto3
from jwt import PyJWKClient

lambda_client = boto3.client("lambda")


def handler(event, context):
    print(f"Event: {json.dumps(event)}")
    token = event["headers"].get("authorization", "")
    path = event["path"]
    method = event["httpMethod"]

    if not token:
        raise Exception("Unautorisiert")

    token = token.replace("Bearer ", "")

    decoded_token = None
    try:
        jwks_url = os.environ["JWKS_URL"]

        jwks_client = PyJWKClient(jwks_url)
        signing_key = jwks_client.get_signing_key_from_jwt(token)

        decoded_token = jwt.decode(
            token,
            signing_key.key,
            algorithms=["RS256"],
            audience=os.environ["AUDIENCE"],
        )

        data = {
            "jwt_token": token,
            "resource": path,
            "action": method,
        }
        
        response = lambda_client.invoke(
            FunctionName=os.environ["PDP_AUTHZ_ENDPOINT"],
            InvocationType="RequestResponse",
            Payload=json.dumps(data),
        )

        response_payload = json.loads(response["Payload"].read())
        body = json.loads(response_payload["body"])
        effect = body["effect"]

        return generate_policy(
            decoded_token["sub"], effect, event["methodArn"], decoded_token
        )

    except Exception as e:
        print(f"Autorisierungsfehler: {str(e)}")

    return generate_policy(
        decoded_token["sub"], "Verweigern", event["methodArn"], decoded_token
    )


def generate_policy(principal_id, effect, resource):
    auth_response = {
        "principalId": principal_id,
        "policyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {"Action": "execute-api:Invoke", "Effect": effect, "Resource": resource}
            ],
        },
    }
    return auth_response

Bedeutung von Caching

Caching ist wichtig für die Optimierung unseres Autorisierungsflusses. Durch die Reduzierung der Aufrufe an den PDP und die Beschleunigung der Entscheidungsfindung hilft Caching, die Gesamtleistung, Skalierbarkeit und Kosteneffizienz unserer Anwendung zu verbessern.

Reduzierte Latenz: Durch das Caching von Rollen- und Berechtigungsdaten vermeidet der PEP wiederholte Aufrufe an unseren PDP, was zu schnelleren Antwortzeiten und geringerer Latenz für jede Anfrage führt. Geringere PDP-Last: Caching minimiert die Anzahl der Aufrufe an unseren PDP, wodurch das Risiko der Erreichung von Ratenbegrenzungen oder Throttling verringert wird. Verbesserte Skalierbarkeit: Mit weniger Anfragen an unseren PDP kann unsere Architektur effizienter skalieren. Geringere Kosten: Caching reduziert den Bedarf an wiederholten PDP-Aufrufen, was direkt die Lambda-Aufrufkosten senkt.

Zusammenfassung und Fazit

Die Implementierung von PEP und PDP in unserem Autorisierungsfluss bietet eine hochgradig skalierbare, flexible und sichere Möglichkeit, den Zugriff auf Ressourcen zu kontrollieren. Durch die Nutzung von AWS Lambda und API Gateway können wir ein serverloses Autorisierungssystem erstellen, das Authentifizierungs- und Autorisierungsprobleme trennt, bei Bedarf skaliert und die Richtlinienerwaltung vereinfacht.

Mit der Hinzufügung von Role-Based Access Control und DynamoDB zur Speicherung von Berechtigungen, kombiniert mit In-Memory-Caching für verbesserte Leistung, können wir eine Autorisierungslösung erstellen, die sowohl den aktuellen als auch den zukünftigen Anforderungen entspricht.

Das Verständnis des Unterschieds zwischen ID-Tokens und Access-Tokens stellt sicher, dass unser System jedes angemessen verwendet, wodurch wir ein sichereres und effizienteres Autorisierungssystem aufbauen können.

Viel Spaß beim Codieren und bleiben Sie sicher!

Quellcode

Die gesamte Einrichtung, mit detaillierten Bereitstellungsanweisungen und allem Code, finden Sie auf Serverless Handbook PEP und PDP

Abschließende Worte

Vergessen Sie nicht, mir auf LinkedIn und X zu folgen, um weitere Inhalte zu erhalten, und lesen Sie den Rest meiner Blogs

Wie Werner sagt! Jetzt loslegen!