Avatar (Fabio Alessandro Locati|Fale)'s blog

Implement WebFinger with AWS CloudFront and AWS Lambda

July 24, 2023

This website is hosted on AWS S3 and uses AWS CloudFront as CDN. I use a couple of AWS Lambda@Edge functions to make AWS CloudFront a little brighter. When I decided to self-host a Fediverse instance, it became immediately evident that I would have to set up WebFinger on my domain to be able to use my root domain as the account domain. There is documentation on the web on how to set up WebFinger, but it is aimed at different setups, so I had to configure it myself.

What WebFinger is

The WebFinger protocol is an IETF proposed standard that dates back to 2013. The idea is to create a well-known endpoint to query a server for resource locations. It works by performing a GET /.well-known/webfinger?resource=acct%3Auser%40example.com and expecting a JSON Resource Descriptor back. As you can notice, WebFinger requires query parameters, which are a pain in static website contexts and with Content Delivery Networks (CDNs).

WebFinger and AWS CloudFront

AWS CloudFront, like many other CDN, tends to truncate query parameters to allow a more efficient caching of the pages. To ensure that WebFinger can be used behind CloudFront, you must ensure that - at least - the resource query string gets preserved. To do so, you need to:

Now CloudFront will avoid dropping the resource query string so that the AWS Lambda function will be able to read it.

WebFinger and AWS Lambda

Now that we ensured that the resource query string arrives at the AWS Lambda function, we can create or modify the function to handle it. We will need to alter one of the response functions. Personally, I use the “Origin Response” one so that it is cached.

All the code discussed must be placed in the exports.handler function. The first part, the one that you will want to change based on your needs, is the declaration of the users:

// Webfinger
let users = [
    {
        "user": "fale",
        "account-domain": "fale.io",
        "host": "gts.fale.io"
    }
]

You should create multiple objects in the users array if you have multiple users. Given your Fediverse account name (in my case: @fale@fale.io), the user is the part of your between the @ (in my case: fale), the account-domain is the part after the latter @ (in my case: fale.io). The host is the Fully Qualified Domain Name (FQDN) of your Fediverse instance.

You can also add the following code, which is the part that will actively handle the requests based on the users content.

const querystring = require('querystring');
const params = querystring.parse(event.Records[0].cf.request.querystring);
for (var i=0, iLen=users.length; i<iLen; i++) {
    if ((params['resource'] == `acct:${users[i]['user']}@${users[i]['account-domain']}`)
     || (params['resource'] == `acct:@${users[i]['user']}@${users[i]['account-domain']}`)){
        const res = {
                status: '200',
                statusDescription: 'OK',
                headers: {
                    'content-type': [
                        {key: 'Content-Type', value: 'application/jrd+json'}
                    ]
                },
                body: JSON.stringify({
                    "subject":`acct:${users[i]['user']}@${users[i]['account-domain']}`,
                    "aliases":[
                        `https://${users[i]['host']}/users/${users[i]['user']}`,
                        `https://${users[i]['host']}/@${users[i]['user']}`
                    ],
                    "links":[
                        {"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":`https://${users[i]['host']}/@${users[i]['user']}`},
                        {"rel":"self","type":"application/activity+json","href":`https://${users[i]['host']}/users/${users[i]['user']}`}
                    ]
                }),
            };
            callback(null, res);
            return;
    }
}

You can now proceed with deploying and publishing your Lambda@Edge to the CDN. If you used the “Origin Response” Lambda@Edge function, you’d need to invalidate the CloudFront cache, at least for the /.well-known/webfinger path; otherwise, you risk having the old reply in the cache.

Validating WebFinger

You can proceed to check your configuration by going to WebFinger.net and looking up your Fediverse account name (eg: @fale@fale.io).

Conclusions

Having to read through the specification, WebFinger felt a very badly designed protocol since it would have been very simple to better use REST (eg: /.well-known/webfinger/acct:MY_ACCOUNT instead of /.well-known/webfinger?resource=acct:MY_ACCOUNT), which would have greatly simplified the implementation with CDNs. Overall, the solution works, and it is not too duct-taped, so I’m fairly happy about the result, and I hope this short how-to can help others as well.