commit c507e6d76086f6eec695bf1a4b1a05475d761103 Author: flashwave Date: Fri Jul 19 23:51:55 2024 +0000 Base application + authorisation code flow. diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bd99c27 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.DS_Store +[Dd]esktop.ini +/.debug +/vendor +/.rng diff --git a/LICENCE b/LICENCE new file mode 100644 index 0000000..c43cbfd --- /dev/null +++ b/LICENCE @@ -0,0 +1,12 @@ +Copyright (c) 2024 flashwave + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4224ff9 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Oatmeal - OAuth2 Tester + +I couldn't find a decent tool for testing Hanyuu so I GUESS I'll just have to write it myself them. + +It's called Oatmeal because: OAuth2 Tester -> OAT -> Oatmeal. + +Please enjoy your stay. diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..aa7f122 --- /dev/null +++ b/composer.json @@ -0,0 +1,15 @@ +{ + "minimum-stability": "dev", + "prefer-stable": true, + "require": { + "flashwave/index": "dev-master" + }, + "autoload": { + "psr-4": { + "Oatmeal\\": "src" + } + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..7dfb037 --- /dev/null +++ b/composer.lock @@ -0,0 +1,67 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "8be4aa570091b8e36082f761bd734626", + "packages": [ + { + "name": "flashwave/index", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://patchii.net/flash/index.git", + "reference": "e4c8ed711e045cffe840ba10a239ede14b0b171f" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.2" + }, + "suggest": { + "ext-mysqli": "Support for the Index\\Data\\MariaDB namespace (both mysqlnd and libmysql are supported).", + "ext-sqlite3": "Support for the Index\\Data\\SQLite namespace." + }, + "default-branch": true, + "type": "library", + "autoload": { + "files": [ + "index.php" + ], + "psr-4": { + "Index\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "bsd-3-clause-clear" + ], + "authors": [ + { + "name": "flashwave", + "email": "packagist@flash.moe", + "homepage": "https://flash.moe", + "role": "mom" + } + ], + "description": "Composer package for the common library for my projects.", + "homepage": "https://railgun.sh/index", + "time": "2024-04-10T23:40:14+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "flashwave/index": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/oatmeal.php b/oatmeal.php new file mode 100644 index 0000000..8c5fc35 --- /dev/null +++ b/oatmeal.php @@ -0,0 +1,31 @@ +.rng or something similar!!!'; + + return file_get_contents($path); +})()); +$oatmeal->register(new HomeRoutes); +$oatmeal->register(new AuthzCodeRoutes($oatmeal->getCSRFP())); +$oatmeal->register(new RefreshTokenRoutes); +$oatmeal->register(new ClientCredsRoutes); +$oatmeal->register(new PasswordRoutes); +$oatmeal->register(new DeviceCodeRoutes); diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..eff3b0d --- /dev/null +++ b/public/index.php @@ -0,0 +1,21 @@ +

Error 500'; + exit; +}); + +$oatmeal->dispatch(); diff --git a/src/AuthzCodeRoutes.php b/src/AuthzCodeRoutes.php new file mode 100644 index 0000000..98c53fb --- /dev/null +++ b/src/AuthzCodeRoutes.php @@ -0,0 +1,459 @@ +redirect('/authorization_code/authorise'); + } + + #[HttpGet('/authorization_code/authorise')] + public function getAuthorise(): string { + $state = XString::random(10); + $verifier = XString::random(60); + + return << + +Oatmeal / Authorisation code + +

Oatmeal / Authorisation code

+

+ You are intentionally allowed to use data that would be considered invalid by the spec, both because I cannot be bothered to check and because your server implementation should be able to handle those appropriately. + You will be able to enter your token POST endpoint during the next step for further verification. +

+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ PKCE: + + + +
+
+ +
+
+ +
+
+ + +
+
+Return + +HTML; + } + + #[HttpPost('/authorization_code/authorise')] + public function postAuthorise($response, $request) { + if(!$request->isFormContent()) + return 400; + $content = $request->getContent(); + + $csrfp = (string)$content->getParam('csrfp'); + if(!$this->csrfp->verifyToken($csrfp)) + return 403; + + $authoriseUri = (string)$content->getParam('authorise_uri'); + $redirectUri = (string)$content->getParam('redirect_uri'); + $clientId = (string)$content->getParam('client_id'); + $scope = (string)$content->getParam('scope'); + $state = (string)$content->getParam('state'); + $pkce = (string)$content->getParam('pkce'); + $pkceOmit = !empty($content->getParam('pkce_omit')); + $pkceVerifier = (string)$content->getParam('pkce_verifier'); + + if(filter_var($authoriseUri, FILTER_VALIDATE_URL) === false) { + $response->setStatusCode(400); + return << + +Oatmeal / Authorisation code +

Oatmeal / Authorisation code

+

Provided Authorise URI was not a valid absolute URI.

+Return +HTML; + } + + $query = [ + 'response_type' => 'code', + 'client_id' => $clientId, + 'scope' => $scope, + ]; + + if($redirectUri !== '') + $query['redirect_uri'] = $redirectUri; + if($state !== '') + $query['state'] = $state; + + if($pkce === 'plain') { + $query['code_challenge'] = $pkceVerifier; + if(!$pkceOmit) + $query['code_challenge_method'] = 'plain'; + } elseif($pkce === 'S256') { + $query['code_challenge'] = UriBase64::encode(hash('sha256', $pkceVerifier, true)); + $query['code_challenge_method'] = 'S256'; + } + + $query = Tools::shuffleArray($query); + + $authoriseUri .= strpos($authoriseUri, '?') === false ? '?' : '&'; + $authoriseUri .= http_build_query($query, '', '&', PHP_QUERY_RFC3986); + $authoriseUri = htmlspecialchars($authoriseUri); + + return << + +Oatmeal / Authorisation code +

Oatmeal / Authorisation code

+

Constructed the following authorisation URI for you: {$authoriseUri}

+Restart +HTML; + } + + #[HttpGet('/authorization_code/callback')] + public function getCallback($response, $request) { + $state = htmlspecialchars((string)$request->getParam('state')); + $error = htmlspecialchars((string)$request->getParam('error')); + $errorDescription = htmlspecialchars((string)$request->getParam('error_description')); + $errorUri = htmlspecialchars((string)$request->getParam('error_uri')); + + if($error !== '') + return << + +Oatmeal / Authorisation code + +

Oatmeal / Authorisation code

+

Authorisation was cancelled with an error code, please verify that this error code is expected for the input you provided.

+
+ +
+
+ +
+
+ +
+
+ +
+Restart + +HTML; + + $code = htmlspecialchars((string)$request->getParam('code')); + + return << + +Oatmeal / Authorisation code + +

Oatmeal / Authorisation code

+

+ Enter all data necessary to complete the access token request, this can vary based on what data you used during the authorisation request and how your application is set up with the service provider. +

+
+ +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ Authentication: + + + +
+
+ + + +
+
+ +HTML; + } + + #[HttpPost('/authorization_code/callback')] + public function postCallback($response, $request) { + if(!$request->isFormContent()) + return 400; + $content = $request->getContent(); + + $csrfp = (string)$content->getParam('csrfp'); + if(!$this->csrfp->verifyToken($csrfp)) + return 403; + + $tokenUri = (string)$content->getParam('token_uri'); + if(filter_var($tokenUri, FILTER_VALIDATE_URL) === false) { + $response->setStatusCode(400); + return << + +Oatmeal / Authorisation code +

Oatmeal / Authorisation code

+

Provided Token URI was not a valid absolute URI.

+Return +HTML; + } + + $clientId = (string)$content->getParam('client_id'); + $clientSecret = (string)$content->getParam('client_secret'); + $code = (string)$content->getParam('code'); + $codeVerifier = (string)$content->getParam('pkce_verifier'); + $redirectUri = (string)$content->getParam('redirect_uri'); + $auth = (string)$content->getParam('auth'); + + $headers = []; + $body = [ + 'grant_type' => 'authorization_code', + 'code' => $code, + ]; + + if($clientSecret === '') + $body['client_id'] = $clientId; + elseif($auth === 'body' || ($auth !== 'header' && mt_rand(0, 10) > 5)) { + $body['client_id'] = $clientId; + $body['client_secret'] = $clientSecret; + } else + $headers[] = sprintf('Authorization: Basic %s', base64_encode(sprintf('%s:%s', $clientId, $clientSecret))); + + if($codeVerifier !== '') + $body['code_verifier'] = $codeVerifier; + + if($redirectUri !== '') + $body['redirect_uri'] = $redirectUri; + + $body = Tools::shuffleArray($body); + $response = Tools::fetch($tokenUri, headers: $headers, body: $body); + + $tokenUri = htmlspecialchars($tokenUri); + $headers = htmlspecialchars(json_encode($headers, JSON_PRETTY_PRINT)); + $body = htmlspecialchars(json_encode($body, JSON_PRETTY_PRINT)); + + $decoded = json_decode($response); + if($decoded !== null) + $response = json_encode($decoded, JSON_PRETTY_PRINT); + + $response = htmlspecialchars($response); + + return << + +Oatmeal / Authorisation code + +

Oatmeal / Authorisation code

+

Below is the request and response data from your authorisation code result. If the response contains a refresh_token field, you can move on to the Refresh token flow!

+
+ +
+
+ +
+
+ +
+
+ +
+Return +HTML; + } +} diff --git a/src/ClientCredsRoutes.php b/src/ClientCredsRoutes.php new file mode 100644 index 0000000..37d6cfe --- /dev/null +++ b/src/ClientCredsRoutes.php @@ -0,0 +1,11 @@ + + +Oatmeal - OAuth2 Testing Utility +
+

Oatmeal

+

+ Hello and welcome to Oatmeal! + This is a quick and dirty utility for testing your OAuth2 server implementation. + It is tailored to my own Hanyuu project for Flashii ID, however it should do for OAuth2.1 and the Device Code extension. + I'm not going to bother with making this page look nice, the most you'll get it functional Javascript and/or CSS touch ups if really necessary. +

+
+

Select A Flow

+ + +HTML; + } +} diff --git a/src/OatmealContext.php b/src/OatmealContext.php new file mode 100644 index 0000000..8a6dc60 --- /dev/null +++ b/src/OatmealContext.php @@ -0,0 +1,33 @@ +csrfp = new CSRFP($rng, $_SERVER['REMOTE_ADDR'] ?? '::'); + + $this->router = new HttpRouter; + $this->router->use('/', fn($resp) => $resp->setPoweredBy('Oatmeal')); + } + + public function getCSRFP(): CSRFP { + return $this->csrfp; + } + + public function register(IRouteHandler $handler): void { + $this->router->register($handler); + } + + public function getRouter(): IRouter { + return $this->router; + } + + public function dispatch(...$args): void { + $this->router->dispatch(...$args); + } +} diff --git a/src/PasswordRoutes.php b/src/PasswordRoutes.php new file mode 100644 index 0000000..d6da5cd --- /dev/null +++ b/src/PasswordRoutes.php @@ -0,0 +1,11 @@ + false, + CURLOPT_FAILONERROR => false, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TCP_FASTOPEN => true, + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_PROTOCOLS => CURLPROTO_HTTPS, + CURLOPT_TIMEOUT => 15, + CURLOPT_USERAGENT => 'Oatmeal/20240720', + CURLOPT_HTTPHEADER => $headers, + ]); + + if($hasBody) + curl_setopt_array($req, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + ]); + + $response = curl_exec($req); + if($response === false) + throw new RuntimeException(curl_error($req)); + } finally { + curl_close($req); + } + + return $response; + } +}