Added client credentials flow.

This commit is contained in:
flash 2024-07-20 01:44:36 +00:00
parent ce7506d816
commit 338260a1d8
3 changed files with 154 additions and 5 deletions

View file

@ -26,5 +26,5 @@ $oatmeal = new OatmealContext((function() {
$oatmeal->register(new HomeRoutes);
$oatmeal->register(new AuthzCodeRoutes($oatmeal->getCSRFP()));
$oatmeal->register(new RefreshTokenRoutes($oatmeal->getCSRFP()));
$oatmeal->register(new ClientCredsRoutes);
$oatmeal->register(new ClientCredsRoutes($oatmeal->getCSRFP()));
$oatmeal->register(new DeviceCodeRoutes);

View file

@ -1,11 +1,160 @@
<?php
namespace Oatmeal;
use Index\Http\Routing\{HttpGet,RouteHandler};
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler};
use Index\Security\CSRFP;
final class ClientCredsRoutes extends RouteHandler {
public function __construct(
private CSRFP $csrfp
) {}
#[HttpGet('/client_credentials')]
public function getIndex(): string {
return 'client creds routes go here';
public function getClientCredentials(): string {
return <<<HTML
<!doctype html>
<meta charset=utf-8>
<title>Oatmeal / Client credentials</title>
<style>
div { margin: .5em 0; }
span { display: inline-block; min-width: 200px; text-align: right; }
input[type=text], input[type=url], input[type=password] { min-width: 500px; }
</style>
<h1>Oatmeal / Client credentials</h1>
<p>
This is an option only available to confidential clients that allows you to get an access token that represent the app itself rather than a user.
Usually doesn't provide refresh tokens because you can literally just dispense them on demand like what are you doing hello?
</p>
<form method=post action=/client_credentials>
<input type=hidden name=csrfp value="{$this->csrfp->createToken()}">
<div>
<label>
<span>Token URI (POST):</span>
<input type=url name=token_uri required>
</label>
</div>
<div>
<label>
<span>Client ID:</span>
<input type=text name=client_id required>
</label>
</div>
<div>
<label>
<span>Client secret:</span>
<input type=password name=client_secret required>
</label>
</div>
<div>
<span>Authentication:</span>
<label>
<input type=radio name=auth value=random checked>
Random
</label>
<label>
<input type=radio name=auth value=header>
Authorization header
</label>
<label>
<input type=radio name=auth value=body>
Request body
</label>
</div>
<div>
<button>Submit</button>
<button type=reset>Reset</button>
</div>
</form>
<a href=/>Return</a>
HTML;
}
#[HttpPost('/client_credentials')]
public function postClientCredentials($response, $request): string {
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 <<<HTML
<!doctype html>
<meta charset=utf-8>
<title>Oatmeal / Client credentials</title>
<h1>Oatmeal / Client credentials</h1>
<p>Provided Token URI was not a valid absolute URI.</p>
<a href="javascript:history.back();">Return</a>
HTML;
}
$clientId = (string)$content->getParam('client_id');
$clientSecret = (string)$content->getParam('client_secret');
$auth = (string)$content->getParam('auth');
$headers = [];
$body = ['grant_type' => 'client_credentials'];
if($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)));
$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 <<<HTML
<!doctype html>
<meta charset=utf-8>
<title>Oatmeal / Client credentials</title>
<style>
div { margin: .5em 0; }
span { display: inline-block; min-width: 200px; text-align: right; }
input[type=text] { min-width: 500px; }
textarea { min-width: 100%; max-width: 100%; min-height: 200px; box-sizing: border-box; }
</style>
<h1>Oatmeal / Client credentials</h1>
<p>Below is the request and response data from your client credentials result.</p>
<div>
<label>
<span>Token URI (POST):</span>
<input type=text value="{$tokenUri}">
</label>
</div>
<div>
<label>
<span>Headers:</span><br>
<textarea readonly>{$headers}</textarea>
</label>
</div>
<div>
<label>
<span>Request body (sent as application/x-www-form-urlencoded, presented as JSON for consistency):</span><br>
<textarea readonly>{$body}</textarea>
</label>
</div>
<div>
<label>
<span>Response body:</span><br>
<textarea readonly>{$response}</textarea>
</label>
</div>
<a href=/client_credentials>Return</a>
HTML;
}
}

View file

@ -140,7 +140,7 @@ HTML;
textarea { min-width: 100%; max-width: 100%; min-height: 200px; box-sizing: border-box; }
</style>
<h1>Oatmeal / Refresh token</h1>
<p>Below is the request and response data from your request token result. If the response contains a refresh_token field, you can restart the <a href=/refresh_token>Refresh token flow</a> again and again and again and again!</p>
<p>Below is the request and response data from your refresh token result. If the response contains a refresh_token field, you can restart the <a href=/refresh_token>Refresh token flow</a> again and again and again and again!</p>
<div>
<label>
<span>Token URI (POST):</span>