From 6c3c38105da25b87075e1167b14f49b7fed9d3ed Mon Sep 17 00:00:00 2001 From: carrvo Date: Mon, 2 Dec 2024 17:09:07 -0700 Subject: [PATCH 1/9] modified to serve as an OAuth2 introspection endpont --- endpoint.php | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/endpoint.php b/endpoint.php index 5333fde..1d18de3 100644 --- a/endpoint.php +++ b/endpoint.php @@ -254,14 +254,46 @@ function invalidRequest(): void exit(); } $revoke = filter_input(INPUT_POST, 'action', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '@^revoke$@']]); + $token = filter_input(INPUT_POST, 'token', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '@^[0-9a-f]+_[0-9a-f]+$@']]); + // check if is POST+revoke request if (is_string($revoke)) { - $token = filter_input(INPUT_POST, 'token', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '@^[0-9a-f]+_[0-9a-f]+$@']]); if (is_string($token)) { revokeToken($token); } header('HTTP/1.1 200 OK'); exit(); } + // check if is POST+introspection request + if (is_string($token)) { + // TODO: authorize resource server as per specification (see https://indieauth.spec.indieweb.org/#access-token-verification-response-p-1) + // meaning verify that the Basic user is the client_id, and ignore the Basic password + $tokenInfo = retrieveToken($token); + if (!isset($tokenInfo)) { + exit(json_encode([ + 'token_type' => 'Bearer', + 'me' => '', + 'sub' => '', + 'client_id' => '', + 'aud' => '', + 'scope' => '', + 'iat' => time(), + 'exp' => time(), + 'active' => false, + ])); + } + exit(json_encode([ + 'token_type' => 'Bearer', + 'me' => $tokenInfo['auth_me'], + 'sub' => $tokenInfo['auth_me'], + 'client_id' => $tokenInfo['auth_client_id'], + 'aud' => $tokenInfo['auth_client_id'], + 'scope' => $tokenInfo['auth_scope'], + 'iat' => strtotime($tokenInfo['created']), + 'exp' => strtotime($tokenInfo['revoked']), + 'active' => time() < strtotime($tokenInfo['revoked']), + ])); + } + // else is a POST+authorization request $request = array_merge( filter_input_array(INPUT_POST, [ 'grant_type' => [ From 4dec874dbc285e11299f2ce7af7adc437b056c45 Mon Sep 17 00:00:00 2001 From: carrvo Date: Tue, 3 Dec 2024 16:11:30 -0700 Subject: [PATCH 2/9] Apply suggestions from code review Co-authored-by: Martijn van der Ven --- endpoint.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/endpoint.php b/endpoint.php index 1d18de3..2c52b83 100644 --- a/endpoint.php +++ b/endpoint.php @@ -268,16 +268,8 @@ function invalidRequest(): void // TODO: authorize resource server as per specification (see https://indieauth.spec.indieweb.org/#access-token-verification-response-p-1) // meaning verify that the Basic user is the client_id, and ignore the Basic password $tokenInfo = retrieveToken($token); - if (!isset($tokenInfo)) { + if ($tokenInfo === null || $tokenInfo['active'] === '0') { exit(json_encode([ - 'token_type' => 'Bearer', - 'me' => '', - 'sub' => '', - 'client_id' => '', - 'aud' => '', - 'scope' => '', - 'iat' => time(), - 'exp' => time(), 'active' => false, ])); } @@ -290,7 +282,7 @@ function invalidRequest(): void 'scope' => $tokenInfo['auth_scope'], 'iat' => strtotime($tokenInfo['created']), 'exp' => strtotime($tokenInfo['revoked']), - 'active' => time() < strtotime($tokenInfo['revoked']), + 'active' => true, ])); } // else is a POST+authorization request From 23aff03d95000854422542c0f3245f3b4e74cde6 Mon Sep 17 00:00:00 2001 From: carrvo Date: Tue, 3 Dec 2024 16:16:23 -0700 Subject: [PATCH 3/9] ensure sending HTTP headers before exit --- endpoint.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/endpoint.php b/endpoint.php index 2c52b83..945005c 100644 --- a/endpoint.php +++ b/endpoint.php @@ -268,6 +268,8 @@ function invalidRequest(): void // TODO: authorize resource server as per specification (see https://indieauth.spec.indieweb.org/#access-token-verification-response-p-1) // meaning verify that the Basic user is the client_id, and ignore the Basic password $tokenInfo = retrieveToken($token); + header('HTTP/1.1 200 OK'); + header('Content-Type: application/json;charset=UTF-8'); if ($tokenInfo === null || $tokenInfo['active'] === '0') { exit(json_encode([ 'active' => false, From 63b7be914a3062bd83809d6f291d2fe5fa055619 Mon Sep 17 00:00:00 2001 From: carrvo Date: Tue, 3 Dec 2024 16:18:19 -0700 Subject: [PATCH 4/9] minimumize instrospection fields --- endpoint.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/endpoint.php b/endpoint.php index 945005c..6de5bc1 100644 --- a/endpoint.php +++ b/endpoint.php @@ -278,9 +278,7 @@ function invalidRequest(): void exit(json_encode([ 'token_type' => 'Bearer', 'me' => $tokenInfo['auth_me'], - 'sub' => $tokenInfo['auth_me'], 'client_id' => $tokenInfo['auth_client_id'], - 'aud' => $tokenInfo['auth_client_id'], 'scope' => $tokenInfo['auth_scope'], 'iat' => strtotime($tokenInfo['created']), 'exp' => strtotime($tokenInfo['revoked']), From cb4e18f3b0e9e11596772ac32705535ac14ea0ab Mon Sep 17 00:00:00 2001 From: carrvo Date: Tue, 3 Dec 2024 16:44:21 -0700 Subject: [PATCH 5/9] client Basic auth --- endpoint.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/endpoint.php b/endpoint.php index 6de5bc1..45f7780 100644 --- a/endpoint.php +++ b/endpoint.php @@ -265,16 +265,27 @@ function invalidRequest(): void } // check if is POST+introspection request if (is_string($token)) { - // TODO: authorize resource server as per specification (see https://indieauth.spec.indieweb.org/#access-token-verification-response-p-1) - // meaning verify that the Basic user is the client_id, and ignore the Basic password $tokenInfo = retrieveToken($token); - header('HTTP/1.1 200 OK'); - header('Content-Type: application/json;charset=UTF-8'); if ($tokenInfo === null || $tokenInfo['active'] === '0') { + header('HTTP/1.1 200 OK'); + header('Content-Type: application/json;charset=UTF-8'); exit(json_encode([ 'active' => false, ])); } + // Authorize resource server as per specification (see https://indieauth.spec.indieweb.org/#access-token-verification-response-p-1) + // For us this means we are expecting the Basic user to be the client ID of the consumer. + // With basic, everything after the first colon (:) is considered the password. + // Since we are working with URIs to identify, we need to handle + // the pattern scheme://domain/path:password + // To do this, we assume (and enforce) that the password is a single underscore (_). + if ($tokenInfo['auth_client_id'] . ':_' !== $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']) { + header('WWW-Authenticate: Basic'); + header('HTTP/1.0 401 Unauthorized'); + exit('Unauthorized'); + } + header('HTTP/1.1 200 OK'); + header('Content-Type: application/json;charset=UTF-8'); exit(json_encode([ 'token_type' => 'Bearer', 'me' => $tokenInfo['auth_me'], From 7aab53c451b12338e7d28133a606233e3a25cbc4 Mon Sep 17 00:00:00 2001 From: carrvo Date: Wed, 4 Dec 2024 12:07:20 -0700 Subject: [PATCH 6/9] document endpoints - using cURL --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 80d24f8..dd00051 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,60 @@ I do not have a need for a token endpoint like this myself, thus developing one (The `href` must point at your `endpoint.php` file.) +### Metadata Discovery +Note that the use of `authorization_endpoint` and `token_endpoint` are deprecated but *SHOULD* be included for backwards compatibility. + +Instead the [spec supports a metadata discovery endpoint](https://indieauth.spec.indieweb.org/#discovery). + +Assuming the same configuration from above, the minimal metadata endpoint would look like +```json +{ + "issuer": "https://example.com", + "authorization_endpoint": "https://example.com/auth/", + "token_endpoint": "https://example.com/auth/endpoint.php", + "introspection_endpoint": "https://example.com/auth/endpoint.php", + "code_challenge_methods_supported": ["S256"] +} +``` +And the full recommended metadata endpoint would look like +```json +{ + "issuer": "https://example.com", + "authorization_endpoint": "https://example.com/auth/", + "token_endpoint": "https://example.com/auth/endpoint.php", + "introspection_endpoint": "https://example.com/auth/endpoint.php", + "response_types_supported": ["code"], + "response_modes_supported": ["query"], + "grant_types_supported": ["authorization_code"], + "introspection_endpoint_auth_methods_supported": ["client_secret_basic"], + "service_documentation": "https://indieauth.spec.indieweb.org/#discovery", + "code_challenge_methods_supported": ["S256"] +} +``` + +## Endpoints + +### Authorize +```curl +curl --include -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=&redirect_uri=https%3A%2F%2Fexample.com%2Fclient%2Fredirect.php&client_id=https%3A%2F%2Fexample.com%2Fclient%2F&code_verifier=' 'https://example.com/auth/endpoint.php' +``` + +### Consume +```curl +curl --include --oauth2-bearer https://example.com/selfauth/token.php +``` + +### Revoke +```curl +curl --include -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'action=revoke&token=' https://example.com/selfauth/token.php +``` + +### Introspection +```curl +curl --include -u https://example.com/client/:_ -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'token=' https://example.com/selfauth/token.php +``` +Note that this endpoint requires a fixed password of `_`. + ## License The BSD Zero Clause License (0BSD). Please see the LICENSE file for From 2b4ddd7d204f2119ac4576ca70ec5d209a7f9ff9 Mon Sep 17 00:00:00 2001 From: carrvo Date: Wed, 4 Dec 2024 13:57:39 -0700 Subject: [PATCH 7/9] mod_oauth encodes basic user before sending --- endpoint.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/endpoint.php b/endpoint.php index 45f7780..c8b185f 100644 --- a/endpoint.php +++ b/endpoint.php @@ -279,7 +279,10 @@ function invalidRequest(): void // Since we are working with URIs to identify, we need to handle // the pattern scheme://domain/path:password // To do this, we assume (and enforce) that the password is a single underscore (_). - if ($tokenInfo['auth_client_id'] . ':_' !== $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']) { + $basicAuth = $_SERVER['PHP_AUTH_USER'] . ':' . $_SERVER['PHP_AUTH_PW']; + $storedAuth = $tokenInfo['auth_client_id'] . ':_'; + $storedAuthEncoded = urlencode($tokenInfo['auth_client_id']) . ':_'; + if ($storedAuth !== $basicAuth && $storedAuthEncoded !== $basicAuth) { header('WWW-Authenticate: Basic'); header('HTTP/1.0 401 Unauthorized'); exit('Unauthorized'); From fc36a39dd75da5da82480f08d010f80d018c9d2c Mon Sep 17 00:00:00 2001 From: carrvo Date: Wed, 4 Dec 2024 14:23:27 -0700 Subject: [PATCH 8/9] differentiate endpoints by query parameter --- README.md | 6 +++--- endpoint.php | 9 ++++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index dd00051..542cc78 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ And the full recommended metadata endpoint would look like ### Authorize ```curl -curl --include -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=&redirect_uri=https%3A%2F%2Fexample.com%2Fclient%2Fredirect.php&client_id=https%3A%2F%2Fexample.com%2Fclient%2F&code_verifier=' 'https://example.com/auth/endpoint.php' +curl --include -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'grant_type=authorization_code&code=&redirect_uri=https%3A%2F%2Fexample.com%2Fclient%2Fredirect.php&client_id=https%3A%2F%2Fexample.com%2Fclient%2F&code_verifier=' 'https://example.com/auth/endpoint.php?action=authorize' ``` ### Consume @@ -96,12 +96,12 @@ curl --include --oauth2-bearer https://example.com/selfau ### Revoke ```curl -curl --include -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'action=revoke&token=' https://example.com/selfauth/token.php +curl --include -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'token=' https://example.com/selfauth/token.php?action=revoke ``` ### Introspection ```curl -curl --include -u https://example.com/client/:_ -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'token=' https://example.com/selfauth/token.php +curl --include -u https://example.com/client/:_ -X POST -H "Content-Type: application/x-www-form-urlencoded" -d 'token=' https://example.com/selfauth/token.php?action=introspect ``` Note that this endpoint requires a fixed password of `_`. diff --git a/endpoint.php b/endpoint.php index c8b185f..2203688 100644 --- a/endpoint.php +++ b/endpoint.php @@ -253,10 +253,13 @@ function invalidRequest(): void header('HTTP/1.1 415 Unsupported Media Type'); exit(); } - $revoke = filter_input(INPUT_POST, 'action', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '@^revoke$@']]); + $action = filter_input(INPUT_GET, 'action', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '@^(revoke|introspect|authorize)$@']]); $token = filter_input(INPUT_POST, 'token', FILTER_VALIDATE_REGEXP, ['options' => ['regexp' => '@^[0-9a-f]+_[0-9a-f]+$@']]); + if (!is_string($action)) { + invalidRequest(); + } // check if is POST+revoke request - if (is_string($revoke)) { + if ($action === 'revoke') { if (is_string($token)) { revokeToken($token); } @@ -264,7 +267,7 @@ function invalidRequest(): void exit(); } // check if is POST+introspection request - if (is_string($token)) { + if ($action === 'introspect') { $tokenInfo = retrieveToken($token); if ($tokenInfo === null || $tokenInfo['active'] === '0') { header('HTTP/1.1 200 OK'); From 895cebc9070aef56bb73b903feec6a7860de9c6a Mon Sep 17 00:00:00 2001 From: carrvo Date: Thu, 5 Dec 2024 10:56:02 -0700 Subject: [PATCH 9/9] Duplicate 'sub' and 'me' for mod_oauth2 --- endpoint.php | 1 + 1 file changed, 1 insertion(+) diff --git a/endpoint.php b/endpoint.php index 2203688..d4c3661 100644 --- a/endpoint.php +++ b/endpoint.php @@ -295,6 +295,7 @@ function invalidRequest(): void exit(json_encode([ 'token_type' => 'Bearer', 'me' => $tokenInfo['auth_me'], + 'sub' => $tokenInfo['auth_me'], 'client_id' => $tokenInfo['auth_client_id'], 'scope' => $tokenInfo['auth_scope'], 'iat' => strtotime($tokenInfo['created']),