6 Ways for CloudFront Functions Authentication, Authorization & Accounting
Photo Credits: iStock.com/Sitthiphong

6 Ways for CloudFront Functions Authentication, Authorization & Accounting

Comparison Ways of AAA
Comparison of Ways for AAA

The table above shows the possibilities and challenges for each of the six authentication, authorization, and accounting methods.

For accounting, most methods have yes (async) as a value. This means we can't actively track the usage but need to get it from CloudWatch logs or S3 log files via ETL (extract, transform, load) processes, which happens asynchronously.

Background

In my previous article on "Low Latency APIs on AWS", I described the use case of REST APIs via CloudFront Functions. This article takes it one step further by implementing authentication, authorization, and accounting for CloudFront Functions.

Signed Cookies & Signed URLs

Signed cookies and signed URLs are very similar. The main difference is how authorization data is transported to the CloudFront Distribution.

Process for using signed cookies or URLs

  1. Create key pair and upload the public key to CloudFront.
  2. Create a key group - which can have multiple key pairs - in the CloudFront settings.
  3. Attach the key group to the distribution behavior that we want to protect.
  4. Create a signed cookie or URL with the key pair's private key.
  5. Use this resulting data to make requests to the protected resource.

This authentication mechanism is highly secure and reliable. However, it needs some custom logic inside, e.g. an API that signs the resource request.

const AWS = require('aws-sdk'

const createCredentials = (keyPairId, privateKey) => {
  const signer = new AWS.CloudFront.Signer(keyPairId, privateKey)

  // 15 minutes in seconds
  const expiry = Math.floor(new Date().getTime() / 1000) + 15 * 60

  const policy = JSON.stringify({
    Statement: [
      {
        Resource: 'https://api.domain.com/*',
        Condition: {
          DateLessThan: {
            'AWS:EpochTime': expiry
          }
        }
      }
    ]
  })

  const signedURL = signer.getSignedUrl({
    url: 'https://api.domain.com',
    policy
  })

  const signedCookie = signer.getSignedCookie({ policy })

  return { signedCookie, signedURL }
}

module.exports = { createCredentials })        

Unfortunately, CloudFront doesn't do any failover from the primary to the failover origin in case the validation of the signed request fails. This restricts us from redirecting a client to a different location where we can create a new signed request.

Description:
I've set up a CloudFront distribution with an origin group.
The failover in the origin group is defined for the status
code 403 (forbidden). Behaviors in the primary origin require
signed cookies via public key pair. In case, e.g. no public
key pair ID is provided, the primary origin responds with a
403 (forbidden)

Expected Result: If the primary origin fails with a
403 (forbidden) - even if that comes from, e.g. a missing
key pair ID - the request gets re-triggered at the failover
origin.

Actual Result: The failover origin never gets triggered, and a
403 (forbidden) is returned to the client.        

If our use case allows for a 50 - 75 ms higher latency, we can solve this by putting two CloudFront distributions in a row. The first distribution will check for the existence of the parameter needed for a signed request. If they are missing or expired, a CloudFront Function will place the request at a custom behavior. Otherwise, the request is forwarded to the second distribution while keeping the needed cookies, headers, and query string parameters in place.

CloudFront to CloudFront
CloudFront to CloudFront

?? The signer for signed cookies and URLs must have the key pair's private key. The key should be stored and accessed securely, like AWS Secrets Manager.

JWT Token (HMAC-SHA256)

JSON Web Tokens (JWT) contain a header, payload, and signature section.

  • Header: Metadata like when the token will expire or when it was created.
  • Payload: Any data that we need to processings made with the token. The more payload data we add, the longer the length of the JWT token will be.
  • Signature: This is header and payload data encrypted with a private secret key. If we want to validate the signature, we need to have the private secret key.

var crypto = require('crypto'

var SECRET = '075a9b322660e51cf5b66a2a1c632429da6e142497b405874b8e11f1cdbc39f7'

var isAuthorized = (event, secret) => {
  var timestamp = Date.now()
  var token = event.request.headers.authorization.value

  if (!token) {
    return false
  }

  var segments = token.split('.')

  if (segments.length !== 3) {
    return false
  }

  var headerSegment = segments[0]
  var payloadSegment = segments[1]
  var signatureSegment = segments[2]

  var payload = JSON.parse(String.bytesFrom(payloadSegment, 'base64url'))

  if (
    payload.nbf && (timestamp < payload.nbf * 1000) ||
    payload.exp && (timestamp > payload.exp * 1000)
  ) {
    return false
  }

  var calculated = crypto.createHmac('sha256',secret)
    .update([headerSegment, payloadSegment].join('.'))
    .digest('base64url')

  if (signatureSegment.length != calculated.length) {
    return false
  }

  var xorMemory = 0

  for (var i = 0; i < signatureSegment.length; i++) {
    xorMemory |= (signatureSegment.charCodeAt(i) ^ calculated.charCodeAt(i))
  }
  
  return 0 === xorMemory
}

var handler = (event) => {
  if (!isAuthorized(event, SECRET)) {
    // redirect request to a different origin path which will create a new token
    event.request.uri = `/sessions${event.request.uri}`
    return event.request
  }

  runLogic()
})        

?? The private secret key needs to get integrated into the CloudFront Function source code.

?? CloudFront Functions can create JWT tokens. However, if we want to control who can get a JWT token, we need access to the internet or a database, which can't be done inside a CloudFront Function.

?? Once created, a JWT token is valid until it has expired or the secret key has been changed. Therefore, we can't use a long expiry time as no token invalidation is possible.

IP-Whitelisting (AWS WAF)

When an AWS WAF (Web Application Firewall) is placed before a CloudFront Distribution, we can restrict access via an IP whitelist.

?? IP Sets are limited to 10.000 IPs, and this value can't get increased. A Web ACL Rule can have up to 50 references to an IP Set. This gives us a hard limit of 500.000 IPs.

Static API Key

This authorization method stores API keys inside the source code that is deployed. Therefore, we need a deployment whenever new API keys are added or existing ones get deleted.

var API_KEYS = 
  'a8015b00-e59b-41b1-957d-b749ae9d064f',
  'c9c44bfe-b6be-4e08-8a87-dc78b64f3464',
  'd6bb43e7-09f5-4205-91ba-1673e436e75f'
]

var handler = (event) => {
  var apiKey = event.request.headers['Authorization']

  if (!apiKey || API_KEYS.includes(apiKey.value)) {
    return {
      statusCode: 401,
      statusDescription: 'UNAUTHORIZED',
      headers: {},
      cookies: {}
    }
  }

  runLogic()
}

module.exports = { handler }[        

?? Within CloudFront Functions, we don't have Internet access and are limited by the amount of resources we can use. This includes the limit of 10 KB function size. The hard limit with UUIDs is 250 API keys per CloudFront Function.

?? Static API keys need to get built and deployed alongside the CloudFront Funtion's logic.

Security Through Obscurity

By Security Through Obscurity (STO), our resources are hidden by cryptic URLs. For CloudFront Distributions, this can get implemented with a dynamic behavior path pattern.

Example

Path Pattern: /sto/f79ae2??-*

Match: https://www.domain.com/sto/f79ae2ac-f294-4b26-b8e2-cdf95b63d2c4

No Match: https://www.domain.com/sto/abc123-f294-4b26-b8e2-cdf95b63d2c4

?? Anyone who has this link can access the resources.

?? There's a soft limit of 25 behaviors per distribution. This value can be increased.

Conclusion

Which of the provided six methods for AAA is the most suited depends - as always - on your use case. For this decision, you need to consider how users and systems will access the resources, how many users you anticipate, and what authorization expiration is acceptable.

For the example REST API that I'm currently building, the approach with JWT tokens is used due to its negligible impact on latency.


Are you looking for AWS, IT Architecture, Serverless, Node.js, or Go help? Let's connect (Florian Sch?ffler?- Remote Freelancer, 7 x AWS Certified, Serverless & Node.js Expert) and discuss how I can support your current and upcoming projects.

CV:?largun.com/cv?| Book a Meeting:?largun.com/meeting

#aws?#solutions?#architecture?#api?#cloudfront #cloudfrontfunctions?#aaa #auth #authentification #authorization #accounting #security

Kristina Chaurova

Head of Business Transformation | Quema | Building scalable and secure IT infrastructures and allocating dedicated IT engineers from our team

2 年

Florian, thanks for sharing!

回复
Florian Sch?ffler

Remote Freelancer, 7 x AWS Certified, Serverless & Node.js Expert

2 年

I'm really curious about more ways to secure CloudFront Functions. I've thought about AAA for the last two weeks, and I would be more than happy to get more ideas on it.

要查看或添加评论,请登录

Florian Sch?ffler的更多文章

  • API Versioning

    API Versioning

    API versioning is the practice of managing changes to an API and ensuring that these changes are made without…

  • Low-Latency APIs on AWS

    Low-Latency APIs on AWS

    Latency is a time delay between the cause and the effect of change being observed. In the context of an API, latency…

  • Available for Freelance Work! AWS Expert / Serverless / Node.js / Back-End

    Available for Freelance Work! AWS Expert / Serverless / Node.js / Back-End

    An upvote/share/like of this post would be amazingly helpful as I'll be available for new projects very soon. Open for…

    1 条评论

社区洞察

其他会员也浏览了