and finally the device code flow!
This commit is contained in:
parent
05dd302866
commit
0973cd31c3
3 changed files with 359 additions and 6 deletions
|
@ -27,4 +27,4 @@ $oatmeal->register(new HomeRoutes);
|
|||
$oatmeal->register(new AuthzCodeRoutes($oatmeal->getCSRFP()));
|
||||
$oatmeal->register(new RefreshTokenRoutes($oatmeal->getCSRFP()));
|
||||
$oatmeal->register(new ClientCredsRoutes($oatmeal->getCSRFP()));
|
||||
$oatmeal->register(new DeviceCodeRoutes);
|
||||
$oatmeal->register(new DeviceCodeRoutes($oatmeal->getCSRFP()));
|
||||
|
|
|
@ -1,11 +1,364 @@
|
|||
<?php
|
||||
namespace Oatmeal;
|
||||
|
||||
use Index\Http\Routing\{HttpGet,RouteHandler};
|
||||
use Index\Http\Routing\{HttpGet,HttpPost,RouteHandler};
|
||||
use Index\Security\CSRFP;
|
||||
|
||||
final class DeviceCodeRoutes extends RouteHandler {
|
||||
public function __construct(
|
||||
private CSRFP $csrfp
|
||||
) {}
|
||||
|
||||
#[HttpGet('/device_code')]
|
||||
public function getIndex(): string {
|
||||
return 'device code routes go here';
|
||||
public function getIndex($response): void {
|
||||
$response->redirect('/device_code/authorise');
|
||||
}
|
||||
|
||||
#[HttpGet('/device_code/authorise')]
|
||||
public function getAuthorise(): string {
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Oatmeal / Device code</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 / Device code</h1>
|
||||
<p>
|
||||
Device codes allow you to do authentications on device where its impractical or impossible to enter your password.
|
||||
I will also be using it as a way to secure do authentication over plain HTTP!
|
||||
</p>
|
||||
<form method=post>
|
||||
<input type=hidden name=csrfp value="{$this->csrfp->createToken()}">
|
||||
<div>
|
||||
<label>
|
||||
<span>Authorise URI (POST):</span>
|
||||
<input type=url name=device_authorise_uri required>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Client ID:</span>
|
||||
<input type=text name=client_id required>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Client secret (optional):</span>
|
||||
<input type=password name=client_secret>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Scope:</span>
|
||||
<input type=text name=scope required>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button>Submit</button>
|
||||
<button type=reset>Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
<a href=/>Return</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
#[HttpPost('/device_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('device_authorise_uri');
|
||||
if(filter_var($authoriseUri, FILTER_VALIDATE_URL) === false) {
|
||||
$response->setStatusCode(400);
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Oatmeal / Device code</title>
|
||||
<h1>Oatmeal / Device code</h1>
|
||||
<p>Provided Device Authorise 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');
|
||||
$scope = (string)$content->getParam('scope');
|
||||
|
||||
$headers = [];
|
||||
$body = ['scope' => $scope];
|
||||
|
||||
if($clientSecret === '')
|
||||
$body['client_id'] = $clientId;
|
||||
else
|
||||
$headers[] = sprintf('Authorization: Basic %s', base64_encode(sprintf('%s:%s', $clientId, $clientSecret)));
|
||||
|
||||
$body = Tools::shuffleArray($body);
|
||||
$response = Tools::fetch($authoriseUri, headers: $headers, body: $body);
|
||||
|
||||
$authoriseUri = htmlspecialchars($authoriseUri);
|
||||
$headers = htmlspecialchars(json_encode($headers, JSON_PRETTY_PRINT));
|
||||
$body = htmlspecialchars(json_encode($body, JSON_PRETTY_PRINT));
|
||||
|
||||
$form = '<p style="color: red">Could not display form because the request failed.</p>';
|
||||
|
||||
$decoded = json_decode($response);
|
||||
if($decoded !== null) {
|
||||
$response = json_encode($decoded, JSON_PRETTY_PRINT);
|
||||
|
||||
$vericationUri = htmlspecialchars($decoded->verification_uri ?? '');
|
||||
$vericationUriComplete = htmlspecialchars($decoded->verification_uri_complete ?? '');
|
||||
$deviceCode = htmlspecialchars($decoded->device_code ?? '');
|
||||
$userCode = htmlspecialchars($decoded->user_code ?? '');
|
||||
|
||||
if(!is_string($deviceCode))
|
||||
$form = '<p style="color: red">Could not display form because the <code>device_code</code> is missing from the response.</p>';
|
||||
elseif(!is_string($userCode))
|
||||
$form = '<p style="color: red">Could not display form because the <code>user_code</code> is missing from the response.</p>';
|
||||
elseif(!is_string($vericationUri))
|
||||
$form = '<p style="color: red">Could not display form because the <code>verification_uri</code> is missing from the response.</p>';
|
||||
else {
|
||||
$form = <<<HTML
|
||||
<p>Here's the form I promised!</p>
|
||||
<form method=get action=/device_code/token>
|
||||
<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>User code for verification:</span>
|
||||
<input type=text value="{$userCode}" readonly>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Verification URI:</span>
|
||||
<input type=url value="{$vericationUri}" readonly class=js-vu-field>
|
||||
<button type=button class=js-vu-button>Open in new tab</button>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Verification URI (Complete):</span>
|
||||
<input type=url value="{$vericationUriComplete}" readonly class=js-vuc-field>
|
||||
<button type=button class=js-vuc-button>Open in new tab</button>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Client ID:</span>
|
||||
<input type=text name=client_id required>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Client secret (optional):</span>
|
||||
<input type=password name=client_secret>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label>
|
||||
<span>Device code:</span>
|
||||
<input type=text name=device_code value="{$deviceCode}" 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>
|
||||
<span>Grant type:</span>
|
||||
<label>
|
||||
<input type=radio name=type value=std checked>
|
||||
Standard (<code>urn:ietf:params:oauth:grant-type:device_code</code>)
|
||||
</label>
|
||||
<label>
|
||||
<input type=radio name=type value=short>
|
||||
Short, not defined by spec (<code>device_code</code>)
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button>Submit</button>
|
||||
<button type=reset>Reset</button>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
(() => {
|
||||
const makeButtonClickable = (field, button) => {
|
||||
if(field.value === '') {
|
||||
button.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
button.onclick = () => { window.open(field.value, '_blank'); };
|
||||
};
|
||||
|
||||
makeButtonClickable(document.querySelector('.js-vu-field'), document.querySelector('.js-vu-button'));
|
||||
makeButtonClickable(document.querySelector('.js-vuc-field'), document.querySelector('.js-vuc-button'));
|
||||
})();
|
||||
</script>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
$response = htmlspecialchars($response);
|
||||
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Oatmeal / Device code</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; }
|
||||
textarea { min-width: 100%; max-width: 100%; min-height: 200px; box-sizing: border-box; }
|
||||
</style>
|
||||
<h1>Oatmeal / Device code</h1>
|
||||
<p>
|
||||
Below is the request and response data from your device authorisation request.
|
||||
Even further below is the form you can use to actually request an access token!
|
||||
Normally you would open <code>verification_uri</code> on a separate device and enter the <code>user_code</code> or just scan the <code>verification_uri_complete</code> as a QR Code or something like that (if present in the response), but you can also just open it in a new tab.
|
||||
</p>
|
||||
<div>
|
||||
<label>
|
||||
<span>Authorise URI (POST):</span>
|
||||
<input type=text value="{$authoriseUri}">
|
||||
</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>
|
||||
{$form}
|
||||
<a href=/device_code>Return</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
#[HttpGet('/device_code/token')]
|
||||
public function getToken($response, $request) {
|
||||
$csrfp = (string)$request->getParam('csrfp');
|
||||
if(!$this->csrfp->verifyToken($csrfp))
|
||||
return 403;
|
||||
|
||||
$tokenUri = (string)$request->getParam('token_uri');
|
||||
if(filter_var($tokenUri, FILTER_VALIDATE_URL) === false) {
|
||||
$response->setStatusCode(400);
|
||||
return <<<HTML
|
||||
<!doctype html>
|
||||
<meta charset=utf-8>
|
||||
<title>Oatmeal / Device code</title>
|
||||
<h1>Oatmeal / Device code</h1>
|
||||
<p>Provided Token URI was not a valid absolute URI.</p>
|
||||
<a href="javascript:history.back();">Return</a>
|
||||
HTML;
|
||||
}
|
||||
|
||||
$clientId = (string)$request->getParam('client_id');
|
||||
$clientSecret = (string)$request->getParam('client_secret');
|
||||
$deviceCode = (string)$request->getParam('device_code');
|
||||
$grantType = (string)$request->getParam('type');
|
||||
$auth = (string)$request->getParam('auth');
|
||||
|
||||
$headers = [];
|
||||
$body = [
|
||||
'grant_type' => $grantType === 'short' ? 'device_code' : 'urn:ietf:params:oauth:grant-type:device_code',
|
||||
'device_code' => $deviceCode,
|
||||
];
|
||||
|
||||
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)));
|
||||
|
||||
$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 / Device code</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 / Device code</h1>
|
||||
<p>Below is the request and response data from your device code result. If the response contains a refresh_token field, you can move on to the <a href=/refresh_token>Refresh token flow</a>!</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=/device_code>Return</a>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,8 @@ final class HomeRoutes extends RouteHandler {
|
|||
<p>
|
||||
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.
|
||||
It is tailored to my own Hanyuu project for Flashii ID, however it should do for general implementations of OAuth2.1 and the Device Code extension.
|
||||
I'm not going to bother with making this page look nice, the most you'll get is utilitarian Javascript and/or CSS touch ups if really necessary.
|
||||
</p>
|
||||
</header>
|
||||
<h2>Select A Flow™</h2>
|
||||
|
|
Loading…
Reference in a new issue