diff --git a/.gitignore b/.gitignore index 58f394d..6987955 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /vendor/ .DS_Store +.phpunit.result.cache + diff --git a/src/IndieAuth/Client.php b/src/IndieAuth/Client.php index 5945faf..bb783f3 100644 --- a/src/IndieAuth/Client.php +++ b/src/IndieAuth/Client.php @@ -236,11 +236,37 @@ private static function _isIssuerValid($issuer, $metadata_endpoint) { $metadata_endpoint = self::normalizeMeURL($metadata_endpoint); - if (strpos($metadata_endpoint, $issuer) !== 0) { + // The issuer is a prefix of the metadata URL (IndieAuth), or the metadata + // URL is the RFC 8414 well-known location for the issuer, which inserts + // /.well-known/oauth-authorization-server between the host and the + // issuer's path. The next IndieAuth draft uses the RFC 8414 form; servers + // such as IndieKey.id already do. + if (strpos($metadata_endpoint, $issuer) === 0) { + return true; + } + + return rtrim($metadata_endpoint, '/') === self::wellKnownMetadataURL($issuer); + } + + /** + * The RFC 8414 (section 3) metadata location for an issuer identifier: + * https://host/.well-known/oauth-authorization-server followed by the + * issuer's path. Without a trailing slash, for comparison. False when the + * issuer is not a URL. + * + * @param string $issuer a normalized issuer URL + * @return string|false + */ + public static function wellKnownMetadataURL($issuer) { + $parts = parse_url($issuer); + if(!is_array($parts) || !isset($parts['scheme'], $parts['host'])) { return false; } - return true; + $base = strtolower($parts['scheme']) . '://' . strtolower($parts['host']) . (isset($parts['port']) ? ':' . $parts['port'] : ''); + $path = rtrim($parts['path'] ?? '', '/'); + + return $base . '/.well-known/oauth-authorization-server' . $path; } private static function _fetchHead($url) { diff --git a/tests/MetadataTest.php b/tests/MetadataTest.php index 10182fb..16bf06f 100644 --- a/tests/MetadataTest.php +++ b/tests/MetadataTest.php @@ -128,6 +128,66 @@ public function testIsIssuerValid() { ); $this->assertTrue($result); + + # RFC 8414: the well-known path is inserted between the host and the issuer's path + $result = $this->_invokeStaticMethod( + Client::class, + '_isIssuerValid', + ['https://example.com/s/abc/', 'https://example.com/.well-known/oauth-authorization-server/s/abc'] + ); + + $this->assertTrue($result); + + # OpenID Connect discovery has its own rules and is not IndieAuth metadata + $result = $this->_invokeStaticMethod( + Client::class, + '_isIssuerValid', + ['https://example.com/s/abc/', 'https://example.com/.well-known/openid-configuration/s/abc'] + ); + + $this->assertFalse($result); + + # ... and for an issuer without a path (also a prefix match) + $result = $this->_invokeStaticMethod( + Client::class, + '_isIssuerValid', + ['https://example.com/', 'https://example.com/.well-known/oauth-authorization-server'] + ); + + $this->assertTrue($result); + + # the well-known location for a different path is not this issuer's + $result = $this->_invokeStaticMethod( + Client::class, + '_isIssuerValid', + ['https://example.com/s/abc/', 'https://example.com/.well-known/oauth-authorization-server/s/xyz'] + ); + + $this->assertFalse($result); + + # nor is the well-known location on another host + $result = $this->_invokeStaticMethod( + Client::class, + '_isIssuerValid', + ['https://example.com/s/abc/', 'https://example.org/.well-known/oauth-authorization-server/s/abc'] + ); + + $this->assertFalse($result); + + # http is still refused, well-known or not + $result = $this->_invokeStaticMethod( + Client::class, + '_isIssuerValid', + ['http://example.com/s/abc/', 'http://example.com/.well-known/oauth-authorization-server/s/abc'] + ); + + $this->assertFalse($result); + } + + public function testWellKnownMetadataURL() { + $this->assertEquals('https://example.com/.well-known/oauth-authorization-server/s/abc', Client::wellKnownMetadataURL('https://example.com/s/abc/')); + $this->assertEquals('https://example.com:8443/.well-known/oauth-authorization-server', Client::wellKnownMetadataURL('https://example.com:8443/')); + $this->assertFalse(Client::wellKnownMetadataURL('not a url')); } public function testDiscoverIssuer() @@ -175,6 +235,22 @@ public function testDiscoverIssuerNotAPrefix() $this->assertInstanceOf(ErrorResponse::class, $result); } + /** + * `issuer` may also name the RFC 8414 well-known location the metadata was fetched from + */ + public function testDiscoverIssuerAtWellKnownLocation() + { + $metadata_endpoint = 'https://example.com/.well-known/oauth-authorization-server/s/abc'; + Client::setMetadata($metadata_endpoint, '{"issuer":"https://example.com/s/abc/","authorization_endpoint":"https://example.com/s/abc/auth","token_endpoint":"https://example.com/token"}'); + + $result = $this->_invokeStaticMethod( + Client::class, + 'discoverIssuer', + [$metadata_endpoint] + ); + $this->assertEquals('https://example.com/s/abc/', $result); + } + public function testDiscoverRevocationEndpoint() { Client::setMetadata('https://example.com/', $this->default_metadata);