如何通过特定用户的Bearer令牌访问与S3存储桶连接的AWS CloudFront(JWT自定义身份验证) [英] How to access AWS CloudFront that connected with S3 Bucket via Bearer token of a specific user (JWT Custom Auth)

查看:108
本文介绍了如何通过特定用户的Bearer令牌访问与S3存储桶连接的AWS CloudFront(JWT自定义身份验证)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在使用无服务器框架将无服务器堆栈部署到AWS.我的堆栈由一些lambda函数,DynamoDB表和API网关组成.

I am using a serverless framework to deploy a serverless stack to AWS. My stack consists of some lambda functions, DynamoDB tables and API Gateway.

我使用称为

I am protected The API Gateway using what's called lambda authorizer. Also, I have a custom standalone self-hosted Auth service that can generate tokens.

因此,方案是用户可以从此服务(Azure上托管的IdentityServer4)请求令牌,然后用户可以将带有承载令牌的请求发送到API网关,以便API网关要求lambda授权者生成IAM角色(如果令牌正确).所有这些都是有效的,并且可以按预期工作.

So the scenario is that the user can request a token from this service (It's IdentityServer4 hosted on Azure) then the user can send a request to the API Gateway with the bearer token so the API gateway will ask the lambda authorizer to generate iam roles if the token is correct. All of that is valid and works as expected.

这是我的serverless.yml中的lambda授权者定义的示例,以及如何使用它来保护其他API网关端点:(您可以看到addUserInfo函数具有使用自定义授权者保护的API)

Here is an example of the lambda authorizer definition in my serverless.yml and how I use it to protect other API gateway endpoints: (You can see the addUserInfo function has API that protected using the custom authorizer )


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,以便允许用户添加图像,因此我遵循了

Now I wanted to extend my APIs so I will allow the user to add images, so I followed this approach. So in this approach, the user will initiate what's called a signed S3 URL and I can put an image to my bucket using this S3 signed URL.

此外,S3存储桶不可公开访问,而是连接到CloudFront发行版.现在我错过了这里的东西,我不明白如何保护自己的图像.无论如何,这样我就可以使用自定义身份验证服务保护CloudFront CDN中的映像,以便拥有有效令牌的用户可以访问这些资源?如何使用自定义身份验证服务保护CDN(CloudFront)并使用无服务器框架对其进行配置?

Also, the S3 bucket is not publicly accessible but instead, it's connected to CloudFront distribution. Now I missed the things here, I can't understand how I can protect my images. Is it anyway so I can protect the Images in the CloudFront CDN with my custom Authentication service so the user that has a valid token can just access those resources? How can I protect my CDN (CloudFront) using my Custom Authentication service and configure that using the serverless framework?

推荐答案

这有点棘手,需要大约一天的时间才能完成所有设置.

This is a bit tricky and it takes from me around a day to get all set.

首先,我们在这里有其他选择:

First we have options here:

  • 代替身份验证,我们可以签名URL并返回签名的CloudFront URL或签名的S3 URL,这很简单,但显然不是我想要的.
  • 第二种选择是使用Lambda @ Edge授权CloudFront的请求以及我遵循的内容.

所以我最终创建了一个单独的堆栈来处理所有S3,CloudFront和Lambda @ Edge东西,因为它们都部署在边缘上,这意味着该区域无关紧要,但是对于lambda边缘,我们需要将其部署到主要的AWS区域((弗吉尼亚北部),us-east-1),所以我最终为所有这些区域创建了一个堆栈.

So I ended up create a separate stack to handle all the S3, CloudFront, and Lambda@Edge stuff cause they are all deployed on edges which means that the region doesn't matter but for lambda edge we need to deploy it to the main AWS region ((N. Virginia), us-east-1) So i ended up creating one stack for all of them.

首先,我的auth-service.js中包含以下代码(它只是帮助我验证自定义jwt的一些辅助工具):

First I have the below code in my auth-service.js (It's just some helpers to allow me to verify my custom 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里面是我的文件:

Then inside the serverless.yml here is my file:

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在这里很重要.
  • 使用无服务器框架创建任何lambda边缘有点棘手,不切实际,因此我用它来配置功能,然后在此云形成模板内resources/s3-cloudfront.yml我添加了所有需要的位.
  • The us-east-1 important here.
  • It's a bit tricky and not practical to create any lambda edge using the serverless framework so I used it to just configure the function and then inside this cloud formation template resources/s3-cloudfront.yml I added all the needed bits.

然后是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

与此文件有关的一些要点:

Some quick points related to this file:

  • 在这里我创建了S3存储桶,其中将包含我的所有私有和公共资源.
  • 此存储桶是私有的,不可访问,您会发现一个角色,该角色只授予CDN和lambda边缘访问权限.
  • 我决定创建一个CloudFront(CDN),其两个原始源分别指向S3的公共文件夹和私有源,以将其指向S3的私有文件夹,并配置CloudFront私有源的行为以将lambda edge函数用于通过查看者请求事件类型进行身份验证.
  • 您还将找到用于创建函数版本的代码以及具有其作用的另一个名为PublishLambdaVersion的函数,它有助于在部署时为lambda边缘提供正确的权限.
  • Here I created the S3 bucket that will contain all my private and public resources.
  • This bucket is private and not accessible and you will find a role who just give the CDN and the lambda edge access to it.
  • I decided to create a CloudFront (CDN) with two origins public to be pointed to the S3's public folder and private to point it to the S3's private folder and configure the behavior of the CloudFront private origin to use my lambda edge function for the authentication through the viewer-request event type.
  • You will find also a code to create the function version and another function called PublishLambdaVersion with its role and it helps to give the lambda edge the correct permissions while deploying.

最后,这里是用于CDN身份验证的lambda edge函数的实际代码:

Finally here it the actually code for the lambda edge function used for CDN auth:

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并将其作为Azure中的docker映像托管,并将其用作自定义授权者.

In case you are interested, I am using IdentityServer4 and hosted it as a docker image in Azure and using it as a custom authorizer.

因此,现在有了完整的方案,我们有了一个完全私有的S3存储桶.仅可通过CloudFront来源访问.如果请求是通过公共来源提供的,则不需要身份验证,但如果是通过私有来源提供的,那么我会触发所谓的lambda边缘对它进行身份验证并验证承载令牌.

So the full scenario now that we have an S3 bucket that totally private. It's only accessible through the CloudFront origins. If the request served through the public origin so no authentication needed but if it's served through the private origin so is I am triggering what's called lambda edge to authenticate it and validate the bearer token.

在深入了解所有这些内容之前,我对AWS堆栈完全陌生,但是AWS非常简单,因此我最终以完美的方式配置了所有内容.如果有不清楚的地方或有任何疑问,请告诉我.

I was totally new to AWS stack before going deep into all of those but AWS is quite easy so I end up configured everything in a perfect way. Please let me know in case there is something not clear or if there are any questions.

这篇关于如何通过特定用户的Bearer令牌访问与S3存储桶连接的AWS CloudFront(JWT自定义身份验证)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆