Supporting WebFinger with Static Files and Nginx
Update 2023: I now use Caddy instead of nginx, and have an equivalent Caddy snippet here.
A few weeks ago, I switched my website from WordPress to a staticly generated site. In doing so, I had to find alternative solutions to some of the things I was doing with WordPress that can’t be easily handled just using static files. One of those things is WebFinger (aka RFC 7033), which is a simple discovery protocol for URIs that might not be usable as locators otherwise, such as account or email URIs. It’s actually not too heavily used right now aside from OpenID Connect, but it’s still something I want to continue supporting.
In its simplest form, WebFinger involves sending a GET request to a well-known URL, /.well-known/webfinger
, with an
identifier passed in the resource
query parameter. The server responds with a JSON document describing the requested
resource. Given that the contents returned depend on the value of a query parameter, it can’t really be done with just
static files, but it can with a little help from nginx.
Requirements
I wanted to follow the WebFinger spec as closely as possible, so the basic requirements for my setup were:
- requests for any of my identifiers return my WebFinger document
- requests for unrecognized identifiers return a 404 (required by section 4.2)
- requests with a missing or malformed identifier return a 400 (required by section 4.2)
- responses include a content type of “application/jrd+json” (required by section 4.2)
- responses include an appropriate CORS header (required by section 5)
There were a few optional parts of the spec I explicitly chose not to support:
- no support for content negotiation to request a different response format (allowed in section 4.2)
- no support for the
rel
parameter to filter the contents of the response (allowed in section 4.3)
My setup
So, my final nginx configuration for supporting WebFinger is:
location = /.well-known/webfinger {
if ($request_method !~ ^(GET|HEAD)$) { return 405; }
set_by_lua $resource 'return ngx.unescape_uri(ngx.req.get_uri_args()["resource"])';
if ($resource = "") { return 400; }
if ($resource = "acct:[email protected]") { rewrite .* /webfinger.json last; }
if ($resource = "mailto:[email protected]") { rewrite .* /webfinger.json last; }
if ($resource = "https://willnorris.com") { rewrite .* /webfinger.json last; }
if ($resource = "https://willnorris.com/") { rewrite .* /webfinger.json last; }
}
location = /webfinger.json {
types { application/jrd+json json; }
add_header Access-Control-Allow-Origin "*";
}
My first location block is an exact match for the well-known WebFinger path. Within that block, I first enforce that
only GET
and HEAD
requests are accepted, all others receive a 405 response. This isn’t required by the spec, but
seems like a good idea.
Next, I use a small lua snippet to URL decode the resource
query parameter and store it in a variable. By default,
parameters are not URL decoded, but need to be for the string comparison in the next step. The embedded lua module
is typically not included in standard nginx installations, but is part of the nginx-extras
apt package in Debian and
Ubuntu. Alternately, this could be done with the embedded perl module which may be more common, but is a little more
work to use since the perl_set
directive can’t be used inside location blocks. Or there is the ngx_set_misc
module which provides a set_unescape_uri
directive specifically for URL decoding values, but that typically
requires building nginx from source, which I didn’t want to deal with.
Next, I compare the decoded resource value, returning a 400 response if empty, rewriting the request to my WebFinger
document if it matches one of my identifiers, or (implicitly) returning a 404 response if nothing else matched. Because
this is my personal domain, I can easily hardcode the identifiers I want to support, and rewrite these requests to a
single webfinger.json
resource. If I needed to support multiple users or identities, I would likely rewrite the
request based on the resource
value similar to what Aaron Parecki does with his Apache config.
Finally, I have a location block for my webfinger.json
document which adds the correct content type and CORS header.
Possible alternatives
Instead of rewriting the original request, I could have just as easily returned a redirect to my webfinger.json
document, but I chose to do it in a single request. Similarly, I also tried moving this file to
/.well-known/webfinger
directly which worked fine, but the nginx config to ensure that invalid resource
values
returned a 404 wasn’t quite as readable.
For Jekyll users, Eric Mill’s jekyll-webfinger plugin generates a WebFinger document from a yaml source file, which is really nice. It just focuses on file generation, so it still requires additional webserver configuration like the above to handle proper error codes and content types. Personally, I just edit the JSON file by hand.
Comments and responses
So, notes on my website should be automatically posted on App.net and Twitter. Also, webmentions should be sent & webfinger works.
P.S. OAuth 1.0 is awful.
That #WebFinger requires a query param for an identifier is a critical design flaw. If the ID is a natural key then it’s preferable to rewrite a URI component to a query parameter if a back-end requires it than vice-versa.
Case: static generation.
Plenty old post at willnorris.com/2014/07/webfin⦠but it just helped me setup my #nginx, so my #Fediverse account can now be found by searching for my mail address. And if I ever decide to move it’s just editing one file, and the search finds the right place.
Cute.