diff --git a/docs/en/030_Supported_Protocols/040_Microsub.md b/docs/en/030_Supported_Protocols/040_Microsub.md
new file mode 100644
index 00000000..f7c659bf
--- /dev/null
+++ b/docs/en/030_Supported_Protocols/040_Microsub.md
@@ -0,0 +1,19 @@
+[TOC]
+
+# About
+
+
+ - Supported since
+ - 0.9.0
+ - Base URL
+ - /
+ - API endpoint
+ - /u/username
+ - /microsub
+ - Specifications
+ - Microsub, IndieAuth
+
+
+The Microsub protocol is a facet of the [IndieWeb movement](https://indieweb.org/), and unlike other protocols is designed as a first-class interface between news-reading clients and servers.
+
+As IndieWeb technology is sprawling and complex, The Arsse only implements enough so that Microsub clients can be used with The Arsse. Consequently The Arsse does not function as a generic IndieAuth authorizer, will not work with arbitrary IndieWeb identifier URLs, does not implement any Micropub functionality, and so on.
diff --git a/lib/REST.php b/lib/REST.php
index 989205f4..fa2b1ec4 100644
--- a/lib/REST.php
+++ b/lib/REST.php
@@ -17,32 +17,41 @@ use Zend\Diactoros\Response\EmptyResponse;
class REST {
const API_LIST = [
'ncn' => [ // NextCloud News version enumerator
- 'match' => '/index.php/apps/news/api',
- 'strip' => '/index.php/apps/news/api',
+ 'match' => "/index.php/apps/news/api",
+ 'strip' => "/index.php/apps/news/api",
'class' => REST\NextCloudNews\Versions::class,
],
'ncn_v1-2' => [ // NextCloud News v1-2 https://github.com/nextcloud/news/blob/master/docs/externalapi/Legacy.md
- 'match' => '/index.php/apps/news/api/v1-2/',
- 'strip' => '/index.php/apps/news/api/v1-2',
+ 'match' => "/index.php/apps/news/api/v1-2/",
+ 'strip' => "/index.php/apps/news/api/v1-2",
'class' => REST\NextCloudNews\V1_2::class,
],
'ttrss_api' => [ // Tiny Tiny RSS https://git.tt-rss.org/git/tt-rss/wiki/ApiReference
- 'match' => '/tt-rss/api',
- 'strip' => '/tt-rss/api',
+ 'match' => "/tt-rss/api",
+ 'strip' => "/tt-rss/api",
'class' => REST\TinyTinyRSS\API::class,
],
'ttrss_icon' => [ // Tiny Tiny RSS feed icons
- 'match' => '/tt-rss/feed-icons/',
- 'strip' => '/tt-rss/feed-icons/',
+ 'match' => "/tt-rss/feed-icons/",
+ 'strip' => "/tt-rss/feed-icons/",
'class' => REST\TinyTinyRSS\Icon::class,
],
'fever' => [ // Fever https://web.archive.org/web/20161217042229/https://feedafever.com/api
- 'match' => '/fever/',
- 'strip' => '/fever/',
+ 'match' => "/fever/",
+ 'strip' => "/fever/",
'class' => REST\Fever\API::class,
],
+ 'microsub' => [ // Microsub https://indieweb.org/Microsub
+ 'match' => "/microsub",
+ 'strip' => "",
+ 'class' => REST\Microsub\API::class,
+ ],
+ 'microsub_auth' => [ // IndieAuth for Microsub https://indieauth.spec.indieweb.org/
+ 'match' => "/u/",
+ 'strip' => "/u/",
+ 'class' => REST\Microsub\Auth::class,
+ ],
// Other candidates:
- // Microsub https://indieweb.org/Microsub
// Google Reader http://feedhq.readthedocs.io/en/latest/api/index.html
// Feedbin v2 https://github.com/feedbin/feedbin-api
// CommaFeed https://www.commafeed.com/api/
diff --git a/lib/REST/Microsub/Auth.php b/lib/REST/Microsub/Auth.php
new file mode 100644
index 00000000..c4e7f07b
--- /dev/null
+++ b/lib/REST/Microsub/Auth.php
@@ -0,0 +1,57 @@
+getRequestTarget())['path'] ?? "";
+ if (!strlen($id) || strpos($id, "/") !== false) {
+ return new EmptyResponse(404);
+ }
+ $id = rawurldecode($id);
+ // gather the query parameters and act on the "proc" parameter
+ $method = "do".ucfirst(strtolower($req->getQueryParams()['proc'] ?? "discovery"));
+ if (!method_exists($this, $method)) {
+ return new EmptyResponse(404);
+ } else {
+ return $this->$method($id, $req);
+ }
+ }
+
+ protected function doDiscovery(string $user, ServerRequestInterface $req): ResponseInterface {
+ // construct the base user identifier URL; the user is never checked against the database
+ // as this route is publicly accessible, for reasons of privacy requests for user discovery work regardless of whether the user exists
+ $s = $req->getServerParams();
+ $https = (strlen($s['HTTPS'] ?? "") && $s['HTTPS'] !== "off");
+ $port = (int) $s['SERVER_PORT'];
+ $port = (!$port || ($https && $port == 443) || (!$https && $port == 80)) ? "" : ":$port";
+ $base = URL::normalize(($https ? "https" : "http")."://".$s['HTTP_HOST'].$port."/");
+ $id = $base."u/".rawurlencode($user);
+ // prepare authroizer, token, and Microsub endpoint URLs
+ $urlAuth = $id."?proc=login";
+ $urlToken = $id."?proc=issue";
+ $urlService = $base."microsub";
+ // output an extremely basic identity resource
+ $html = '';
+ return new Response($html, 200, [
+ "Link: <$urlAuth>; rel=\"authorization_endpoint\"",
+ "Link: <$urlToken>; rel=\"token_endpoint\"",
+ "Link: <$urlService>; rel=\"microsub\"",
+ ]);
+ }
+}