6 Ways for CloudFront Functions Authentication, Authorization & Accounting
Florian Sch?ffler
Remote Freelancer, 7 x AWS Certified, Serverless & Node.js Expert
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
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.
?? 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.
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
Head of Business Transformation | Quema | Building scalable and secure IT infrastructures and allocating dedicated IT engineers from our team
2 年Florian, thanks for sharing!
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.