Как получить доступ к AWS CloudFront, который подключен к S3 Bucket через токен-носитель определенного пользователя c (пользовательская аутентификация JWT) - PullRequest
1 голос
/ 21 июня 2020

Я использую бессерверную структуру для развертывания бессерверного стека на AWS. Мой стек состоит из некоторых лямбда-функций, таблиц DynamoDB и шлюза API.

Я защищен шлюзом API с помощью так называемого авторизатора лямбда-выражений . Кроме того, у меня есть настраиваемая автономная автономная служба аутентификации, которая может генерировать токены.

Таким образом, пользователь может запросить токен у этой службы (это IdentityServer4, размещенный на Azure), тогда пользователь может отправить запрос на шлюз API с токеном носителя, чтобы шлюз API попросил авторизатор лямбда сгенерировать роли iam, если токен правильный. Все это действительно и работает должным образом.

Вот пример определения лямбда-авторизатора в моем serverless.yml и то, как я использую его для защиты других конечных точек шлюза API: (Вы можете видеть, что функция addUserInfo имеет API, защищенный с помощью настраиваемого авторизатора)


functions:
    # =================================================================
    # API Gateway event handlers
    # ================================================================
  auth:
    handler: api/auth/mda-auth-server.handler

  addUserInfo:
     handler: api/user/create-replace-user-info.handler
     description: Create Or Replace user section
     events:
       - http:
           path: user
           method: post
           authorizer: 
             name: auth
             resultTtlInSeconds: ${self:custom.resultTtlInSeconds}
             identitySource: method.request.header.Authorization
             type: token
           cors:
             origin: '*'
             headers: ${self:custom.allowedHeaders}

Теперь я хотел расширить свои API, чтобы позволить пользователю добавлять изображения, поэтому я последовал этому подходу . Таким образом, в этом подходе пользователь инициирует так называемый подписанный URL-адрес S3, и я могу поместить изображение в свою корзину, используя этот подписанный URL-адрес S3.

Кроме того, корзина S3 не является общедоступной, а вместо этого подключается в распространение CloudFront. Теперь я упустил то, что здесь, я не могу понять, как я могу защитить свои изображения. Так или иначе, чтобы я мог защитить изображения в CDN CloudFront с помощью моей пользовательской службы аутентификации, чтобы пользователь, имеющий действующий токен, мог просто получить доступ к этим ресурсам? Как я могу защитить свой CDN (CloudFront) с помощью службы пользовательской аутентификации и настроить ее с помощью бессерверной инфраструктуры?

1 Ответ

0 голосов
/ 30 июня 2020

Это немного сложно, и у меня уходит около дня, чтобы все настроить.

Сначала у нас есть варианты:

  • Вместо аутентификации мы можем подписать URL и вернуть подписанный URL CloudFront или подписанный URL S3, и это довольно просто, но, очевидно, это не то, что я искал.
  • Второй вариант - использовать Lambda@Edge для авторизации запросов CloudFront и того, что Я последовал.

В итоге я создал отдельный стек для обработки всего S3, CloudFront и Lambda@Edge, потому что все они развернуты на краях, что означает, что регион не имеет значения, но для лямбда-края нам нужно развернуть его в основном регионе AWS ((Северная Вирджиния), us-east-1) Итак, я создал один стек для всех из них.

Сначала у меня есть приведенный ниже код в моем auth-service. js (Это просто некоторые помощники, которые позволяют мне проверить мой собственный jwt):

import * as jwtDecode from 'jwt-decode';
import * as util from 'util';
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';


export function getToken(bearerToken) {
    if(bearerToken && bearerToken.startsWith("Bearer "))
    {
        return bearerToken.replace(/^Bearer\s/, '');
    }
    throw new Error("Invalid Bearer Token.");
};

export function getDecodedHeader(token) {
        return jwtDecode(token, { header: true });
};

export async function getSigningKey(decodedJwtTokenHeader, jwksclient){
    const key = await util.promisify(jwksclient.getSigningKey)(decodedJwtTokenHeader.kid);
    const signingKey = key.publicKey || key.rsaPublicKey;
    if (!signingKey) {
        throw new Error('could not get signing key');
    }
    return signingKey;
  };

export async function verifyToken(token,signingKey){
    return await jwt.verify(token, signingKey);
};

export function getJwksClient(jwksEndpoint){
    return jwksClient({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 10,
        jwksUri: jwksEndpoint
      });
};

Затем внутри serverless.yml вот мой файл:

service: mda-app-uploads

plugins:
  - serverless-offline
  - serverless-pseudo-parameters
  - serverless-iam-roles-per-function
  - serverless-bundle


custom:
  stage: ${opt:stage, self:provider.stage}
  resourcesBucketName: ${self:custom.stage}-mda-resources-bucket
  resourcesStages:
    prod: prod
    dev: dev
  resourcesStage: ${self:custom.resourcesStages.${self:custom.stage}, self:custom.resourcesStages.dev}


provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, 'dev'}
  region: us-east-1
  versionFunctions: true

functions: 
  oauthEdge:
    handler: src/mda-edge-auth.handler
    role: LambdaEdgeFunctionRole
    memorySize: 128
    timeout: 5


resources:
  - ${file(resources/s3-cloudfront.yml)}

Быстрые точки здесь:

  • Здесь важен us-east-1.
  • Создание любого лямбда-ребра с использованием бессерверной структуры немного сложно и непрактично, поэтому я использовал его, чтобы просто настроить функцию, а затем внутри этот шаблон формирования облака resources/s3-cloudfront.yml Я добавил все необходимые биты.

Затем вот содержимое resources/s3-cloudfront.yml:

Resources:

    AuthEdgeLambdaVersion:
        Type: Custom::LatestLambdaVersion
        Properties:
            ServiceToken: !GetAtt PublishLambdaVersion.Arn
            FunctionName: !Ref OauthEdgeLambdaFunction
            Nonce: "Test"

    PublishLambdaVersion:
        Type: AWS::Lambda::Function
        Properties:
            Handler: index.handler
            Runtime: nodejs12.x
            Role: !GetAtt PublishLambdaVersionRole.Arn
            Code:
                ZipFile: |
                    const {Lambda} = require('aws-sdk')
                    const {send, SUCCESS, FAILED} = require('cfn-response')
                    const lambda = new Lambda()
                    exports.handler = (event, context) => {
                        const {RequestType, ResourceProperties: {FunctionName}} = event
                        if (RequestType == 'Delete') return send(event, context, SUCCESS)
                        lambda.publishVersion({FunctionName}, (err, {FunctionArn}) => {
                        err
                            ? send(event, context, FAILED, err)
                            : send(event, context, SUCCESS, {FunctionArn})
                        })
                    }

    PublishLambdaVersionRole:
        Type: AWS::IAM::Role
        Properties:
            AssumeRolePolicyDocument:
                Version: '2012-10-17'
                Statement:
                - Effect: Allow
                  Principal:
                    Service: lambda.amazonaws.com
                  Action: sts:AssumeRole
            ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
            Policies:
            - PolicyName: PublishVersion
              PolicyDocument:
                Version: '2012-10-17'
                Statement:
                - Effect: Allow
                  Action: lambda:PublishVersion
                  Resource: '*'

    LambdaEdgeFunctionRole:
        Type: "AWS::IAM::Role"
        Properties:
            Path: "/"
            ManagedPolicyArns:
                - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
            AssumeRolePolicyDocument:
                Version: "2012-10-17"
                Statement:
                -
                    Sid: "AllowLambdaServiceToAssumeRole"
                    Effect: "Allow"
                    Action: 
                        - "sts:AssumeRole"
                    Principal:
                        Service: 
                            - "lambda.amazonaws.com"
                            - "edgelambda.amazonaws.com"
    LambdaEdgeFunctionPolicy:
        Type: "AWS::IAM::Policy"
        Properties:
            PolicyName: MainEdgePolicy
            PolicyDocument:
                Version: "2012-10-17"
                Statement:
                    Effect: "Allow"
                    Action: 
                        - "lambda:GetFunction"
                        - "lambda:GetFunctionConfiguration"
                    Resource: !GetAtt AuthEdgeLambdaVersion.FunctionArn
            Roles:
                - !Ref LambdaEdgeFunctionRole


    ResourcesBucket:
        Type: AWS::S3::Bucket
        Properties:
            BucketName: ${self:custom.resourcesBucketName}
            AccessControl: Private
            CorsConfiguration:
                CorsRules:
                -   AllowedHeaders: ['*']
                    AllowedMethods: ['PUT']
                    AllowedOrigins: ['*']

    ResourcesBucketPolicy:
        Type: AWS::S3::BucketPolicy
        Properties:
            Bucket:
                Ref: ResourcesBucket
            PolicyDocument:
                Statement:
                # Read permission for CloudFront
                -   Action: s3:GetObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
                -   Action: s3:PutObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        AWS: !GetAtt LambdaEdgeFunctionRole.Arn

                -   Action: s3:GetObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        AWS: !GetAtt LambdaEdgeFunctionRole.Arn

    
    CloudFrontOriginAccessIdentity:
        Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
        Properties:
            CloudFrontOriginAccessIdentityConfig:
                Comment:
                    Fn::Join: 
                        - ""
                        -
                            - "Identity for accessing CloudFront from S3 within stack "
                            - 
                                Ref: "AWS::StackName"
                            - ""


    # Cloudfront distro backed by ResourcesBucket
    ResourcesCdnDistribution:
        Type: AWS::CloudFront::Distribution
        Properties:
            DistributionConfig:
                Origins:
                    # S3 origin for private resources
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
                        Id: S3OriginPrivate
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                    # S3 origin for public resources           
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
                        Id: S3OriginPublic
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                Enabled: true
                Comment: CDN for public and provate static content.
                DefaultRootObject: index.html
                HttpVersion: http2
                DefaultCacheBehavior:
                    AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                    Compress: true
                    TargetOriginId: S3OriginPublic
                    ForwardedValues:
                        QueryString: false
                        Headers:
                        - Origin
                        Cookies:
                            Forward: none
                    ViewerProtocolPolicy: redirect-to-https
                CacheBehaviors:
                    - 
                        PathPattern: 'private/*'
                        TargetOriginId: S3OriginPrivate
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        LambdaFunctionAssociations:
                            - 
                                EventType: viewer-request
                                LambdaFunctionARN: !GetAtt AuthEdgeLambdaVersion.FunctionArn
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https
                    - 
                        PathPattern: 'public/*'
                        TargetOriginId: S3OriginPublic
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https

                PriceClass: PriceClass_200

Некоторые быстрые вопросы, связанные с этим файлом:

  • Здесь я создал корзину S3, которая будет содержать все мои личные и c ресурсы.
  • Эта корзина является частной и недоступной, и вы найдете роль, которая просто дает CDN и доступ к нему по краю лямбда. S3 и настройте поведение частного источника CloudFront, чтобы использовать мою функцию лямбда-края для аутентификации через тип события запроса на просмотр.
  • Вы также найдете код для создания версии функции и другой функции, называемой PublishLambdaVersion, с ее ролью, которая помогает дать лямбда-краю правильные разрешения при развертывании.

Наконец, вот собственно код для используемой лямбда-функции для аутентификации CDN:

import {getJwksClient, getToken, getDecodedHeader, getSigningKey, verifyToken} from '../../../../libs/services/auth-service';
import config from '../../../../config';

const response401 = {
    status: '401',
    statusDescription: 'Unauthorized'
};

exports.handler = async (event) => {
    try{
        const cfrequest = event.Records[0].cf.request;
        const headers = cfrequest.headers;
        if(!headers.authorization) {
            console.log("no auth header");
            return response401;
        }
        const jwtValue = getToken(headers.authorization);
        const client = getJwksClient(`https://${config.authDomain}/.well-known/openid-configuration/jwks`);
        const decodedJwtHeader = getDecodedHeader(jwtValue);
        if(decodedJwtHeader)
        {
          const signingKey = await getSigningKey(decodedJwtHeader, client);
          const verifiedToken = await verifyToken(jwtValue, signingKey);
          if(verifiedToken)
          {
            return cfrequest;
          }
      }else{
        throw Error("Unauthorized");
      }

    }catch(err){
      console.log(err);
      return response401;
    }
};

Если вам интересно, я использую IdentityServer4 и размещаю его как изображение docker в Azure и использую его как настраиваемый авторизатор.

Итак, полный сценарий теперь, когда у нас есть полностью приватная корзина S3. Доступен только через источники CloudFront. Если запрос обслуживается через источник publi c, поэтому аутентификация не требуется, но если он обслуживается через частный источник, то я запускаю так называемое лямбда-ребро для его аутентификации и проверки токена-носителя.

Я был полностью новичок в стеке AWS, прежде чем углубляться во все это, но AWS довольно прост, поэтому я все настроил идеально. Пожалуйста, дайте мне знать, если что-то неясно или возникнут вопросы.

...