Private-Webmention

From IndieWeb


The Private Webmention protocol is an extension to Webmention that supports sending and verifying Webmentions for posts that require access control. This spec depends on understanding Webmention. Please read that first.

Status
This is a Living Specification yet mature enough to encourage implementations and feedback.
Latest Published Version
https://indieweb.org/Private-Webmention
Participate
feedback
discuss
Editors
Aaron Parecki
Authors
Other contributors: revision history
License
Per CC0, to the extent possible under law, the editor(s) and contributors have waived all copyright and related or neighboring rights to this work. In addition, as of 2024-03-19, the editor(s) and contributors (2016-09-25 onward) have made this specification available under the Open Web Foundation Agreement Version 1.0.

Summary

  • Alice creates a private post on her site which is only accessible given proper authorization.
  • Alice sends a Webmention to Bob to notify him about this post.
    • The Webmention contains an additional parameter (code) that indicates to Bob this post requires authorization.
  • Bob's Webmention endpoint exchanges the code for an access token at Alice's Token Endpoint.
  • Bob's Webmention endpoint can then fetch Alice's post using the access token, and continue Webmention processing as normal.

Why

The main Webmention spec only works with URLs that do not require authentication to access. In order for Webmention to work with private posts, we need to specify a mechanism by which the receiver can authenticate a request when verifying the Webmention.

Protocol

Discovery

The Sender discovers the Receiver's Webmention endpoint as normal.

Note that any 401 status code (when sending a webmention to a private post) should not be considered an error when discovering the endpoint.

Auth Code Generation

Before sending the Webmention request, the Sender first generates a temporary authorization code. The authorization code MUST expire shortly after it is issued to mitigate the risk of leaks. A maximum authorization code lifetime of 10 minutes and minimum of 60 seconds is recommended. The sender SHOULD associate this authorization code with the intended recipient of the Webmention, and SHOULD generate a "realm" value that is unique to the intended recipient or audience. The code and realm values MUST NOT include characters outside of the ASCII ranges %x20-21 / %x23-5B / %x5D-7E.

Sending the Webmention

The Sender sends the Webmention request and includes an additional parameter "code" (and optionally "realm") containing the authorization code and realm values. For example:

POST /webmention HTTP/1.1
Host: barnaby.example
Content-Type: application/x-www-form-urlencoded

source=https://aaronpk.example/private-post&
target=https://barnaby.example/another-post&
code=zliNDUzZGY5ODYyNmFiODZiNmY4NTY3NTAxMm&
realm=jNhYjI2NDEx

Verifying the Webmention

The Receiver receives a Webmention and checks for the presence of a "code" parameter. If it sees the "code" parameter, the Receiver knows that fetching the source URL will require authentication.

If the Receiver receives a Webmention containing a previously seen "realm" for which it already has a valid access token, the endpoint SHOULD skip the code exchange and use the existing token immediately to fetch the source URL and avoid generating a new access token.

Obtaining an Access Token

The Receiver then needs to exchange the authorization code for an access token. First, the receiver discovers the sender's Token Endpoint by making an HTTP HEAD (or optionally GET) request to the source URL, and looking for an HTTP Link header containing rel="token_endpoint". For example:

HEAD /private-post HTTP/1.1
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Link: <https://aaronpk.example/token>; rel="token_endpoint"

The Sender MUST return 401 Unauthorized and the WWW-Authenticate header in addition to the Link header for URLs that require authentication to access. The WWW-Authenticate header MAY include a "realm" value indicating the realm of tokens that are valid, although some implementations may wish to not reveal this information to unauthenticated requests. The Link header specifying the token endpoint MAY be a relative URL.

The Receiver makes a POST request to the token endpoint to exchange the authorization code for an access token. The request contains the parameter grant_type=authorization_code and the parameter code containing the authorization code given.

POST /token HTTP/1.1
Host: aaronpk.example
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=zliNDUzZGY5ODYyNmFiODZiNmY4NTY3NTAxMm
Note: See Issues below

The Token Endpoint checks that the authorization code provided is valid and has not expired, and then issues an access token and sends the response. The following parameters are sent in the body of the response with the application/json media type.

  • access_token REQUIRED. The access token issued by the authorization server.
  • token_type REQUIRED. The type of the token issued. For the flow described in this document, "bearer" is the only valid value.
  • expires_in RECOMMENDED. The lifetime in seconds of the access token. For example, if the value is 3600 it indicates to the Receiver that the access token is valid for one hour, and that the access token can be re-used within that timeframe. If the token does not expire, this parameter is omitted.
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"A0MTUxM2Q3MWZmNjI5YjYyMmNiYTMwYi",
  "token_type":"bearer",
  "expires_in":3600
}

Fetching the Source URL

Now that the Receiver has obtained an access token, it can use that token to fetch the source URL and continue performing the rest of the Webmention verification.

The Receiver includes the token in the HTTP Authorization header as a Bearer Token.

GET /private-post HTTP/1.1
Authorization: Bearer A0MTUxM2Q3MWZmNjI5YjYyMmNiYTMwYi

The server verifies the access token is valid and returns the page.

How to Send Private Webmentions

Create an access controlled page

Using a method of your choosing, create a URL that will require authentication (in the form of an access token) to access.

The URL must respond successfully when presented with a valid access token in the HTTP Authorization header:

GET /private-post HTTP/1.1
Authorization: Bearer A0MTUxM2Q3MWZmNjI5YjYyMmNiYTMwYi

Your URL must respond with the following when unauthenticated requests are made to the page:

  • HTTP 401 Unauthorized header
  • WWW-Authenticate: Bearer header
  • Link header with rel=token_endpoint pointing to your token endpoint (see below)

For example:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Link: <https://aaronpk.example/token>; rel="token_endpoint"

Sending the Webmention

Discover the target's endpoint using normal Webmention discovery.

Generate a temporary authorization code, and store it somewhere that the token endpoint (described below) can find it. The authorization code should be a random string long enough to not be guessable. You will need some sort of association between the authorization code and the source URL you are about to send.

Optionally, generate a realm value that the client can use to know whether it already has a valid access token for the realm. Typically the realm value can be associated with the people the post is directed to.

Send the Webmention to the Webmention endpoint and include the additional parameters:

POST /webmention HTTP/1.1
Host: barnaby.example
Content-Type: application/x-www-form-urlencoded

source=https://aaronpk.example/private-post&
target=https://barnaby.example/another-post&
code=zliNDUzZGY5ODYyNmFiODZiNmY4NTY3NTAxMm&
realm=jNhYjI2NDEx

Generating Access Tokens

The Receiver will recognize the Webmention will require authorization to access the source URL and will attempt to get an access token from your token endpoint. To create a token endpoint, it needs to be able to issue an access token when it receives a valid authorization code.

A request will come to the token endpoint and will contain an authorization code.

POST /token HTTP/1.1
Host: aaronpk.example
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=zliNDUzZGY5ODYyNmFiODZiNmY4NTY3NTAxMm

The token endpoint looks up the authorization code to find out which realm or posts the code is valid for, then generates an access token that can be used to retrieve the source URL. The access token should expire in the future, typically between a couple hours to a couple days. The endpoint returns the token to the Receiver in an HTTP JSON response:

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"A0MTUxM2Q3MWZmNjI5YjYyMmNiYTMwYi",
  "token_type":"bearer",
  "expires_in":3600
}

The Receiver will later use this token to fetch the source URL.

How to Receive Private Webmentions

First, set up standard Webmention receiving, including advertising your Webmention endpoint and verifying Webmentions by fetching the source URL and looking for the link back. This is described in w3.org/TR/webmention.

Recognizing private Webmentions

When your Webmention endpoint gets a Webmention request, check for the presence of an HTTP parameter called code. If a code is present, this indicates that the source URL will require authorization to fetch.

Discover the token endpoint

Once you know you will need to obtain an access token, you first need to discover the token endpoint for the source URL. Make an HTTP HEAD (or GET) request to the source URL, and look for the HTTP Link header with rel=token_endpoint. The response will typically look like the below:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer
Link: <https://aaronpk.example/token>; rel="token_endpoint"

Note that the Link header may be a relative URL so be sure to resolve that to a full URL using the source URL as the base when resolving.

Exchange the code for an access token

Make an HTTP POST request to the token endpoint containing the code you obtained in the initial Webmention request.

POST /token HTTP/1.1
Host: aaronpk.example
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=zliNDUzZGY5ODYyNmFiODZiNmY4NTY3NTAxMm

You will get an access token as a response. The token may expire, so the lifetime in seconds of the access token will be included in the response.

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"A0MTUxM2Q3MWZmNjI5YjYyMmNiYTMwYi",
  "token_type":"bearer",
  "expires_in":3600
}

You can store this token or just use it once to fetch the source URL.

Fetch the source URL

When your Webmention receiver fetches the source URL to verify the link there exists, you'll include the access token you were given along with the request. This is sent in an HTTP Authorization header like the below:

GET /private-post HTTP/1.1
Authorization: Bearer A0MTUxM2Q3MWZmNjI5YjYyMmNiYTMwYi

Check for existing access tokens

This is an optional optimization your endpoint can do to potentially avoid needing to generate tokens every time a private webmention is received.

When you receive the Webmention, the request may also include a parameter named "realm". When you obtain an access token, store the token somewhere along with the associated realm. When you receive a future Webmention request and the realm is provided, check your token database to see if you already have a non-expired access token for the given source domain and realm.

IndieWeb Examples

IndieWeb Examples of receiving and sending Private Webmentions, in order of implementation.

Aaron Parecki

Aaron Parecki can send private webmentions since 2016-09-30 and receive since 2016-10-20. (example)

Private webmention responses are only visible to aaronpk when he's logged in to his website. However, the counter (number of likes, replies, etc) does increment publicly when a private webmention is received.

sknebel

Sven Knebel can receive private webmentions since 2016-10-01, and send them since 2017-05-21.

First successful private webmention between independent implementations was sent 2016-10-01 from Aaron Parecki to Sven Knebel. Private post: https://aaronparecki.com/2016/09/30/15/

TheGillies

TheGillies can send and receive private webmentions on an elixir demo app. He doesn't have a permanent site setup yet. Although, a test site can be found to be up periodically.

Sebastiaan Andeweg

Sebastiaan Andeweg can send private webmentions as of 2017-02-14, following gRegor Morrills JWT style, but different:

  • authorization code is a JWT that expires after 2 minutes, with keys
    • source: webmention source URL
    • target: webmention target URL
  • access token is a JWT that expires after 1 hour, with keys
    • me: not the source but the target, because that's the 'me' from the visitors perspective.
    • page: the source of the webmention, so a page on my site, to which the token is scoped.
    • nonce: random nonce.
  • sucessfully tested 2017-05-19 against Sven Knebels endpoint.

Since 2017-05-21, he can also receive them. This was tested on the same day, again with Sven Knebels site.

add yourself!

Past Examples

gRegor Morrill

gRegor Morrill: gregorlove.com could send/receive private webmentions between 2016-10-01 and 2020-10-25. I initially set this up as an experiment and never moved beyond the initial proof-of-concept. In theory the code still worked, but I decided to remove it from my site 2020-10-25 since I wasn't actively using it.

Implementation details:

  • private post: https://gregorlove.com/2016/09/this-is-a-test-private/
  • authorization code is a JWT that expires after 5 minutes, with keys
    • me: webmention source URL
    • target: webmention target URL
  • access token is a JWT that expires after 1 hour, with keys
    • me: same as authorization code
    • nonce: random nonce
    • extra: array of any of the extra parameters from the authorization code. Currently this only includes the target

Design Considerations

Rely on https for security

The transport security of this spec is achieved through the use of https for all endpoints involved. If any of the four URLs involved in the flow do not support https, the security of the data cannot be guaranteed.

By relying on https for security, we avoid needing to complicate the spec with cryptographic signing or encryption. Configuring https on a domain has many additional benefits, and so is a reasonable requirement to place on the parties involved.

Limit scope to Webmention verification

This spec is intentionally limited to Webmention verification.

Specifically, this means that this spec does not describe a way for site A to obtain an access token to read posts on site B except when site B initiates the flow by sending a private Webmention. By having this limitation in place, we avoid a lot of security considerations that would otherwise apply with site A being able to arbitrarily request access tokens from site B.

The result of this limitation is that this spec does not enable full access to private posts or feeds, and is only applicable to verifying a Webmention from a single private post.

FAQ

how does this work with static sites

Static websites typically don't have the ability to require authentication to access pages. Note however this is not always true, since you could have a "static site" (a collection of HTML files) that is hosted on a web server that provides authentication (e.g. Apache .htaccess file requiring HTTP Basic Auth).

If your web server is unable to limit access to posts by requiring authentication, then your only other option is to use "secret" URLs (URLs with long random strings in them) as the source URL of your webmention. In this case, you don't even need to use this spec since receivers will be able to verify the webmention by fetching the source URL with no authentication as normal. This is only private in the sense that the receiver is the only one who knows the source URL you send them. This is often enough security for some implementations and often is not enough for others.

is this the same as private or direct messaging

While you can use this for direct messaging with another person, this spec was written assuming the use case of sending webmentions from posts that typically appear in a stream shared with one or more people. It is likely that there is a different spec more optimized for the specific use case of direct messaging.

how do I know if a receiver supports private webmentions

This spec does not currently provide a mechanism for the sender to know if the receiver supports private webmentions before sending. If you send a private webmention to someone who doesn't support them, the worst that happens is they fetch the source URL with no authentication and they are unable to verify the webmention. No data is leaked. The sender could also know that the sender doesn't support private webmentions if they notice that the source URL was fetched without authentication immediately after sending the webmention.

can a receiver re-verify a private webmention

The Webmention spec suggests that receivers may periodically re-verify Webmentions. The token exchange that occurs for private Webmention verification results in an access token that may expire after a relatively short time. This means the receiver may be in a situation in which it does not have a valid access token to access a previously-received private Webmention. This spec intentionally leaves out unsolicited token exchange, so it is a known limitation that this spec may prevent private Webmentions from being re-verified at a later date.

is there a privacy indicator in the posts themselves

Problem: When a post is private/group-visible, replies most often should also have the same visibility. To avoid leaking private content in public responses, (micropub) clients and the reply-display-code on the user's website need to be able to find out that the user is responding to a private post.

why is there an extra step of exchanging an auth code for an access token

When you send a Webmention, you are sending an unsolicited payload to the receiver. The authorization code is not requested by the receiver, so you cannot guarantee they will be protecting it if they aren't expecting it. (It may be logged in intermediate proxy servers, written to log files on the server, etc.) If the receiver supports Private Webmentions, then they will be explicitly requesting an access token by exchanging the authorization code, so you can be more confident they will take measures to protect the access token. This way the access token is only delivered via an explicitly requested HTTPS connection to your server.

should I send Webmentions for all links in my post

Likely not. Since sending a private Webmention reveals the post to the receiver, the mention should only be sent to explicitly selected links. A clear example are links with in-reply-to - a reply is addressed to the author of the post being replied to, so a private Webmention should be sent. If another URL is just mentioned in the post, it likely should not receive a Webmention. Implementers (at least Sven Knebel, Sebastiaan Andeweg) have been using in-reply-to also for homepage mentions to start a conversation.

  • Sven Knebel's site automatically only sents mentions to replied-to posts for private posts


Issues

specific parameters for token exchange

When the receiver exchanges the authorization code for an access token, this is similar to the OAuth 2.0 auth code exchange.

  • Should the Receiver also include the source and/or target URL in this request?
    • It would need to be a new grant type if so.
    • Is there any additional security obtained?
    • Does it help the token endpoint by indicating more explicitly that this request is from the webmention auth code?
  • Since OAuth 2.0 requires client_id if there is no other client authentication, should we require client_id here, or create a new grant type that doesn't require client_id?
    • OAuth 2.0 client_id is used for client identification, but in this case the sender doesn't know the client when it sends the webmention so the sender isn't expecting a particular value of client_id

link rel value

Currently this spec is re-using token_endpoint from IndieAuth, which borrowed it from OpenID Connect's provider metadata. There is however an OAuth 2.0 draft describing link relations for OAuth 2.0, which may be a better one to use. This would mean changing token_endpoint to oauth2-token.

redirects

Should redirects when fetching the post be allowed? If yes, the auth header has to be sent on subsequent requests.

timing critical

Since it relies on short-lived tokens, webmention receivers have to work quickly, and delays due to high-load situations, both accidential and malicious, increase in impact from slower service to actual loss of data.

  • Martijn van der Ven can set his background task no quicker than once every 5 minutes on Binero. Risking that many short-lived codes will have timed-out.

breaks asynchronised flows

Verification of webmentions โ€œshould be handled asynchronously to prevent DoS (Denial of Service) attacksโ€ [1], but putting a private webmention on a queue can easily reach the timeout for the code-value. Private webmentions cannot be prioritised by the TTL of the code either, as this value is only known to the sender.

Access tokens โ€œshould expire in the future, typically between a couple hours to a couple daysโ€, which should be plenty of time for asynchronised handling. Obtaining an access token however involves a POST request to fetch an unknown payload from a sender specified URL (the token endpoint). If everything is well, this should go really quickly and just return some JSON, but there are no guarantees.

The POST request to the token endpoint can be equally dangerous as any other GET request done during verification. If verification should be done async, so should token retrieval. But because of the timing critical nature this is very hard to implement.

See Also