Added styling to the authorisation prompt etc.

This commit is contained in:
flash 2024-07-21 00:47:00 +00:00
parent 86b32bb8eb
commit f6346e3f25
26 changed files with 730 additions and 69 deletions

View file

@ -0,0 +1,49 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
position: relative;
}
html, body {
width: 100%;
height: 100%;
}
[hidden],
.hidden {
display: none !important;
}
:root {
--font-regular: Verdana, Geneva, 'Dejavu Sans', Arial, Helvetica, sans-serif;
--font-monospace: Consolas, 'Liberation Mono', Menlo, Courier, monospace;
}
body {
background-color: #111;
color: #fff;
font-size: 16px;
line-height: 25px;
font-family: var(--font-regular);
overflow-y: scroll;
position: static;
display: flex;
flex-direction: column;
}
pre, code {
font-family: var(--font-monospace);
}
a {
color: #1e90ff;
text-decoration: none;
}
a:visited {
color: #6B4F80;
}
a:hover,
a:focus {
text-decoration: underline;
}

View file

@ -0,0 +1,43 @@
.oauth2-appinfo {}
.oauth2-appinfo-name {
font-size: 2em;
line-height: 1.5em;
}
.oauth2-appinfo-links {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.oauth2-appinfo-link {
display: flex;
color: inherit;
gap: 5px;
align-items: center;
background: #333;
padding: 2px 6px;
border-radius: 4px;
}
.oauth2-appinfo-link-icon {
flex: 0 0 auto;
background-color: #fff;
width: 12px;
height: 12px;
}
.oauth2-appinfo-link-icon-globe {
mask: url('/images/globe-solid.svg') no-repeat center;
}
.oauth2-appinfo-link-text {
font-size: .8em;
line-height: 1.4em;
}
.oauth2-appinfo-summary {
font-size: .9em;
line-height: 1.4em;
}
.oauth2-appinfo-summary p {
margin: .5em 0;
}

View file

@ -0,0 +1,57 @@
.oauth2-authorise-requesting {
font-size: .8em;
line-height: 1.4em;
border-bottom: 1px solid #333;
}
.oauth2-authorise-requesting p {
margin: .5em 0;
}
.oauth2-authorise-buttons {
margin-top: 10px;
display: flex;
justify-content: center;
gap: 10px;
}
.oauth2-authorise-button {
background-color: #191919;
font-family: var(--font-regular);
font-size: 1.2em;
line-height: 1.4em;
padding: 5px 10px;
min-width: 140px;
text-align: center;
cursor: pointer;
transition: color .2s, background-color .2s, opacity .2s;
border: 1px solid;
border-radius: 2px;
display: inline-flex;
align-items: center;
justify-content: center;
text-decoration: none;
}
.oauth2-authorise-button[disabled] {
opacity: .5;
}
.oauth2-authorise-button-accept {
color: #080;
border-color: #0a0;
}
.oauth2-authorise-button-accept:hover,
.oauth2-authorise-button-accept:focus {
color: #191919;
background-color: #0a0;
}
.oauth2-authorise-button-deny {
color: #c00;
border-color: #a00;
}
.oauth2-authorise-button-deny:hover,
.oauth2-authorise-button-deny:focus {
color: #191919;
background-color: #a00;
}

View file

@ -0,0 +1,21 @@
.oauth2-banner {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.oauth2-banner-text {
font-size: .9em;
line-height: 1.4em;
flex: 1 1 auto;
}
.oauth2-banner-logo {
flex: 0 0 auto;
background-color: #fff;
mask: url('/images/flashii.svg') no-repeat center;
width: 30px;
height: 30px;
font-size: 0;
}

View file

@ -0,0 +1,22 @@
.oauth2-errorhead {
display: flex;
align-items: center;
}
.oauth2-errorhead-icon {
flex: 0 0 auto;
background-color: #fff;
mask: url('/images/circle-exclamation-solid.svg') no-repeat center;
width: 40px;
height: 40px;
margin: 10px;
}
.oauth2-errorhead-text {
font-size: 1.8em;
line-height: 1.4em;
}
.oauth2-errorbody p {
margin: .5em 1em;
}

View file

@ -0,0 +1,18 @@
.oauth2-loginhead {
display: flex;
align-items: center;
}
.oauth2-loginhead-icon {
flex: 0 0 auto;
background-color: #fff;
mask: url('/images/user-lock-solid.svg') no-repeat center;
width: 40px;
height: 40px;
margin: 10px;
}
.oauth2-loginhead-text {
font-size: 1.8em;
line-height: 1.4em;
}

View file

@ -1,29 +1,44 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
position: relative;
.oauth2-wrapper {
display: flex;
flex-direction: column;
flex: 1 0 auto;
margin-bottom: 10px;
}
html, body {
.oauth2-dialog {
display: flex;
flex: 1 0 auto;
padding: 10px;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
}
body {
background-color: #111;
color: #fff;
font: 12px/20px Verdana, Geneva, Arial, Helvetica, sans-serif;
.oauth2-dialog-body {
max-width: 500px;
width: 100%;
background: #191919;
box-shadow: 0 1px 2px #0009;
display: flex;
flex-direction: column;
}
@media (prefers-color-scheme: light) {
body {
background-color: #ddd;
color: #000;
}
.oauth2-header {
background-image: url('/images/clouds.png');
background-blend-mode: multiply;
background-color: #8559a5;
width: 100%;
min-height: 4px;
}
[hidden],
.hidden {
display: none !important;
.oauth2-body {
margin: 10px;
}
@include banner.css;
@include error.css;
@include login.css;
@include userhead.css;
@include appinfo.css;
@include scope.css;
@include authorise.css;

View file

@ -0,0 +1,34 @@
.oauth2-scope {
background: #292929;
border-radius: 4px;
padding: 4px 8px;
}
.oauth2-scope-header {
border-bottom: 1px solid #494949;
}
.oauth2-scope-perms {
display: flex;
flex-direction: column;
gap: 4px;
margin: 4px 0;
}
.oauth2-scope-perm {
display: flex;
align-items: center;
gap: 4px;
}
.oauth2-scope-perm-icon {
width: 16px;
height: 16px;
mask: url('/images/circle-check-regular.svg') no-repeat center;
flex: 0 0 auto;
background-color: #0a0;
margin: 2px;
}
.oauth2-scope-perm-text {
font-size: .8em;
line-height: 1.4em;
}

View file

@ -0,0 +1,64 @@
.oauth2-userhead {
display: flex;
flex-direction: column;
background-color: #0005;
}
.oauth2-userhead-main {
display: flex;
align-items: center;
}
.oauth2-userhead-main-avatar {
flex: 0 0 auto;
margin: 10px;
}
.oauth2-userhead-main-avatar-image {
width: 60px;
height: 60px;
overflow: hidden;
border-radius: 4px;
}
.oauth2-userhead-main-avatar-image img {
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
}
.oauth2-userhead-main-name {
font-size: 1.8em;
line-height: 1.4em;
}
.oauth2-userhead-main-name a {
color: inherit;
}
.oauth2-userhead-guise {
display: flex;
align-items: center;
background-image: repeating-linear-gradient(-45deg, #8559a57f, #8559a57f 10px, #1111117f 10px, #1111117f 20px);
}
.oauth2-userhead-guise-avatar {
flex: 0 0 auto;
margin: 10px;
}
.oauth2-userhead-guise-avatar-image {
width: 30px;
height: 30px;
overflow: hidden;
border-radius: 4px;
}
.oauth2-userhead-guise-avatar-image img {
width: 100%;
height: 100%;
object-fit: cover;
border: 0;
}
.oauth2-userhead-guise-text {
font-size: .8em;
line-height: 1.5em;
overflow: hidden;
padding: 2px 0;
}
.oauth2-userhead-guise-text p {
margin: 1px 0;
}

View file

@ -1 +1,32 @@
/* beans */
(() => {
const authoriseButtons = document.querySelectorAll('.js-authorise-action');
for(const button of authoriseButtons) {
button.disabled = false;
button.onclick = () => {
for(const other of authoriseButtons)
other.disabled = true;
const body = [];
for(const name in button.dataset)
body.push(encodeURIComponent(name) + '=' + encodeURIComponent(button.dataset[name]));
const xhr = new XMLHttpRequest;
xhr.responseType = 'json';
xhr.open('POST', '/oauth2/authorise');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = () => {
if(xhr.response.redirect)
location.assign(xhr.response.redirect);
else
location.assign('/oauth2/error?error=invalid_request');
};
xhr.onerror = () => {
for(const other of authoriseButtons)
other.disabled = false;
};
xhr.send(body.join('&'));
};
}
})();

View file

@ -21,6 +21,7 @@ const fs = require('fs');
],
css: [
{ source: 'errors.css', target: '/', name: 'errors.css', },
{ source: 'hanyuu.css', target: '/assets', name: 'hanyuu.{hash}.css', },
{ source: 'oauth2.css', target: '/assets', name: 'oauth2.{hash}.css', },
],
twig: [

View file

@ -9,3 +9,5 @@ misuzu:secret beans
oauth2:device:verification_uri https://hau.local/oauth2/device
oauth2:device:verification_uri_complete https://hau.local/oauth2/device?code=%s
csrfp:secret change this please

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"/></svg>

After

Width:  |  Height:  |  Size: 471 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zm0-384c13.3 0 24 10.7 24 24l0 112c0 13.3-10.7 24-24 24s-24-10.7-24-24l0-112c0-13.3 10.7-24 24-24zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>

After

Width:  |  Height:  |  Size: 421 B

BIN
public/images/clouds.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View file

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1000 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g id="g285"><path id="rect18" d="M912.855,608.974l-86.145,321.498l-57.813,-15.491l86.145,-321.498l57.813,15.491Z"/><path id="rect16" d="M854.364,442.816l-86.145,321.498l-57.813,-15.49l86.145,-321.499l57.813,15.491Z"/><path id="rect14" d="M772.856,362.559l-86.145,321.498l-57.813,-15.491l86.145,-321.498l57.813,15.491Z"/><path id="rect12" d="M676.004,339.569l-86.145,321.498l-57.813,-15.491l86.145,-321.498l57.813,15.491Z"/><path id="rect10" d="M571.479,345.212l-86.145,321.498l-57.813,-15.491l86.145,-321.498l57.813,15.491Z"/><path id="rect8" d="M466.954,350.855l-86.145,321.498l-57.813,-15.491l86.146,-321.498l57.812,15.491Z"/><path id="rect6" d="M370.102,327.865l-86.145,321.498l-57.813,-15.491l86.145,-321.498l57.813,15.491Z"/><path id="rect4" d="M288.594,247.608l-86.145,321.498l-57.813,-15.491l86.145,-321.498l57.813,15.491Z"/><path id="rect2" d="M230.103,81.45l-86.145,321.498l-57.813,-15.49l86.145,-321.499l57.813,15.491Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M352 256c0 22.2-1.2 43.6-3.3 64l-185.3 0c-2.2-20.4-3.3-41.8-3.3-64s1.2-43.6 3.3-64l185.3 0c2.2 20.4 3.3 41.8 3.3 64zm28.8-64l123.1 0c5.3 20.5 8.1 41.9 8.1 64s-2.8 43.5-8.1 64l-123.1 0c2.1-20.6 3.2-42 3.2-64s-1.1-43.4-3.2-64zm112.6-32l-116.7 0c-10-63.9-29.8-117.4-55.3-151.6c78.3 20.7 142 77.5 171.9 151.6zm-149.1 0l-176.6 0c6.1-36.4 15.5-68.6 27-94.7c10.5-23.6 22.2-40.7 33.5-51.5C239.4 3.2 248.7 0 256 0s16.6 3.2 27.8 13.8c11.3 10.8 23 27.9 33.5 51.5c11.6 26 20.9 58.2 27 94.7zm-209 0L18.6 160C48.6 85.9 112.2 29.1 190.6 8.4C165.1 42.6 145.3 96.1 135.3 160zM8.1 192l123.1 0c-2.1 20.6-3.2 42-3.2 64s1.1 43.4 3.2 64L8.1 320C2.8 299.5 0 278.1 0 256s2.8-43.5 8.1-64zM194.7 446.6c-11.6-26-20.9-58.2-27-94.6l176.6 0c-6.1 36.4-15.5 68.6-27 94.6c-10.5 23.6-22.2 40.7-33.5 51.5C272.6 508.8 263.3 512 256 512s-16.6-3.2-27.8-13.8c-11.3-10.8-23-27.9-33.5-51.5zM135.3 352c10 63.9 29.8 117.4 55.3 151.6C112.2 482.9 48.6 426.1 18.6 352l116.7 0zm358.1 0c-30 74.1-93.6 130.9-171.9 151.6c25.5-34.2 45.2-87.7 55.3-151.6l116.7 0z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M224 256A128 128 0 1 0 224 0a128 128 0 1 0 0 256zm-45.7 48C79.8 304 0 383.8 0 482.3C0 498.7 13.3 512 29.7 512l362.8 0c-5.4-9.4-8.6-20.3-8.6-32l0-128c0-2.1 .1-4.2 .3-6.3c-31-26-71-41.7-114.6-41.7l-91.4 0zM528 240c17.7 0 32 14.3 32 32l0 48-64 0 0-48c0-17.7 14.3-32 32-32zm-80 32l0 48c-17.7 0-32 14.3-32 32l0 128c0 17.7 14.3 32 32 32l160 0c17.7 0 32-14.3 32-32l0-128c0-17.7-14.3-32-32-32l0-48c0-44.2-35.8-80-80-80s-80 35.8-80 80z"/></svg>

After

Width:  |  Height:  |  Size: 657 B

View file

@ -49,6 +49,13 @@ class AppInfo {
public function getWebsite(): string {
return $this->website;
}
public function getWebsiteDisplay(): string {
$website = $this->website;
if(str_starts_with($website, 'https://'))
$website = substr($website, 8);
return rtrim($website, '/');
}
public function isTrusted(): bool {
return $this->trusted;

View file

@ -68,6 +68,10 @@ class HanyuuContext {
return $this->authInfo;
}
public function getCSRFPSecret(): string {
return $this->config->getString('csrfp:secret', 'beans');
}
public function getWebAssetInfo(): ?object {
$path = HAU_DIR_ASSETS . '/current.json';
return is_file($path) ? json_decode(file_get_contents($path)) : null;
@ -81,7 +85,12 @@ class HanyuuContext {
$routingCtx = new RoutingContext($this->getTemplating());
$routingCtx->getRouter()->use('/', function($response, $request) {
$this->authInfo = $this->misuzuInterop->authCheck('Misuzu', (string)$request->getCookie('msz_auth'), $_SERVER['REMOTE_ADDR']);
$this->authInfo = $this->misuzuInterop->authCheck(
'Misuzu',
(string)$request->getCookie('msz_auth'),
$_SERVER['REMOTE_ADDR'],
[60]
);
});
$routingCtx->getRouter()->get('/', function($response, $request) {
@ -93,7 +102,8 @@ class HanyuuContext {
$this->oauth2Ctx,
$this->appsCtx,
$this->templating,
$this->getAuthInfo(...)
$this->getAuthInfo(...),
$this->getCSRFPSecret(...)
));
return $routingCtx;

View file

@ -66,9 +66,13 @@ class OAuth2AuthoriseData {
string $state,
string $challengeCode,
string $challengeMethod,
string $scope
string $scope,
?string $code = null,
bool $preapprove = false
): OAuth2AuthoriseInfo {
$stmt = $this->cache->get('INSERT INTO hau_oauth2_authorise (app_id, user_id, uri_id, auth_state, auth_challenge_code, auth_challenge_method, auth_scope, auth_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
$code ??= XString::random(60);
$stmt = $this->cache->get('INSERT INTO hau_oauth2_authorise (app_id, user_id, uri_id, auth_state, auth_challenge_code, auth_challenge_method, auth_scope, auth_code, auth_approval) VALUES (?, ?, ?, ?, ?, ?, ?, ?, IF(?, "approved", DEFAULT(auth_approval)))');
$stmt->addParameter(1, $appInfo instanceof AppInfo ? $appInfo->getId() : $appInfo);
$stmt->addParameter(2, $userId);
$stmt->addParameter(3, $appUriInfo instanceof AppUriInfo ? $appUriInfo->getId() : $appUriInfo);
@ -76,7 +80,8 @@ class OAuth2AuthoriseData {
$stmt->addParameter(5, $challengeCode);
$stmt->addParameter(6, $challengeMethod);
$stmt->addParameter(7, $scope);
$stmt->addParameter(8, XString::random(60));
$stmt->addParameter(8, $code);
$stmt->addParameter(9, $preapprove ? 1 : 0);
$stmt->execute();
return $this->getAuthoriseInfo(authoriseId: (string)$this->dbConn->getLastInsertId());

View file

@ -5,6 +5,7 @@ use InvalidArgumentException;
use RuntimeException;
use Index\XString;
use Index\Http\Routing\{HttpGet,HttpOptions,HttpPost,RouteHandler};
use Index\Security\CSRFP;
use Sasae\SasaeEnvironment;
use Syokuhou\IConfig;
use Hanyuu\Apps\AppsContext;
@ -15,7 +16,8 @@ final class OAuth2Routes extends RouteHandler {
private OAuth2Context $oauth2Ctx,
private AppsContext $appsCtx,
private SasaeEnvironment $templating,
private $getAuthInfo
private $getAuthInfo,
private $getCSRFPSecret
) {
if(!is_callable($getAuthInfo))
throw new InvalidArgumentException('$getAuthInfo must be callable');
@ -45,9 +47,55 @@ final class OAuth2Routes extends RouteHandler {
return $scopes;
}
private const AUTHORISE_ERRORS = [
'invalid_request' => [
'description' => 'The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed.',
'allow_description' => true,
],
'unauthorized_client' => [
'description' => 'The client is not authorised to request an authorisation code using this method.',
],
'access_denied' => [
'description' => 'The resource owner or authorization server denied the request.',
],
'unsupported_response_type' => [
'description' => 'The authorisation server does not support obtaining an authorisation code using this method.',
],
'invalid_scope' => [
'description' => 'The requested scope is invalid, unknown, or malformed.',
],
'server_error' => [
'status' => 500,
'description' => 'The authorisation server encountered an unexpected condition that prevented it from fulfilling the request.',
],
'temporarily_unavailable' => [
'status' => 503,
'description' => 'The authorization server is currently unable to handle the request due to a temporary overloading or maintenance of the server.',
],
];
#[HttpGet('/oauth2/error')]
public function getError($response, $request) {
//
$error = (string)$request->getParam('error');
if(!array_key_exists($error, self::AUTHORISE_ERRORS))
return 404;
$info = self::AUTHORISE_ERRORS[$error];
$description = $info['description'];
if($request->hasParam('error_description') && array_key_exists('allow_description', $info) && $info['allow_description'])
$description = $request->getParam('error_description');
$statusCode = 400;
if(array_key_exists('status', $info))
$statusCode = $info['status'];
$response->setStatusCode($statusCode);
return $this->templating->render('oauth2/error', [
'error_code' => $error,
'error_description' => $description,
]);
}
#[HttpGet('/oauth2/authorise')]
@ -172,7 +220,8 @@ final class OAuth2Routes extends RouteHandler {
$state,
$codeChallenge,
$codeChallengeMethod,
$scope
$scope,
preapprove: $appInfo->isTrusted()
);
} catch(RuntimeException $ex) {
return $response->redirect(self::buildCallbackUri($redirectUri, [
@ -181,28 +230,72 @@ final class OAuth2Routes extends RouteHandler {
]));
}
if($authoriseInfo->isApproved()) {
$response->redirect(self::buildCallbackUri($redirectUri, [
'code' => $authoriseInfo->getCode(),
'state' => $authoriseInfo->getState(),
]));
return;
}
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
return $this->templating->render('oauth2/authorise', [
'app' => $appInfo,
'req' => $authoriseInfo,
'auth' => $authInfo,
'csrfp_token' => $csrfp->createToken(),
'redirect_uri' => $redirectUri,
]);
}
#[HttpPost('/oauth2/authorise')]
public function postAuthorise($response, $request) {
if(!$request->isFormContent())
return 400;
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user))
return 401;
// TODO: CSRFP!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
$content = $request->getContent();
$redirectUri = (string)$content->getParam('redirect');
if(filter_var($redirectUri, FILTER_VALIDATE_URL) === false) {
$response->setStatusCode(400);
return [
'redirect' => self::buildCallbackUri('', [
'error' => 'invalid_request',
]),
];
}
$authInfo = ($this->getAuthInfo)();
if(!isset($authInfo->user)) {
$response->setStatusCode(403);
return [
'redirect' => self::buildCallbackUri($redirectUri, [
'error' => 'access_denied',
]),
];
}
$csrfp = new CSRFP(($this->getCSRFPSecret)(), $authInfo->session->token);
if(!$csrfp->verifyToken((string)$content->getParam('csrfp'))) {
$response->setStatusCode(403);
return [
'redirect' => self::buildCallbackUri($redirectUri, [
'error' => 'invalid_request',
'error_description' => 'Request verification failed.',
]),
];
}
$approve = (string)$content->getParam('approve');
if(!in_array($approve, ['yes', 'no']))
return 400;
if(!in_array($approve, ['yes', 'no'])) {
$response->setStatusCode(400);
return [
'redirect' => self::buildCallbackUri($redirectUri, [
'error' => 'invalid_request',
]),
];
}
$authoriseData = $this->oauth2Ctx->getAuthoriseData();
try {
@ -211,32 +304,68 @@ final class OAuth2Routes extends RouteHandler {
code: (string)$content->getParam('code'),
);
} catch(RuntimeException $ex) {
return 404;
$response->setStatusCode(404);
return [
'redirect' => self::buildCallbackUri($redirectUri, [
'error' => 'invalid_request',
'error_description' => 'Could not find authorisation request.',
]),
];
}
if(!$authoriseInfo->isPending())
return 410;
if(!$authoriseInfo->isPending()) {
$response->setStatusCode(410);
return [
'redirect' => self::buildCallbackUri($redirectUri, [
'error' => 'invalid_request',
'error_description' => 'This authorisation request has already been handled.',
'state' => $authoriseInfo->getState(),
]),
];
}
$appsData = $this->appsCtx->getData();
try {
$uriInfo = $appsData->getAppUriInfo($authoriseInfo->getUriId());
} catch(RuntimeException $ex) {
return 400;
$response->setStatusCode(400);
return [
'redirect' => self::buildCallbackUri($redirectUri, [
'error' => 'invalid_request',
'error_description' => 'This authorisation request was made with a redirect URI that is no longer registered with this application.',
'state' => $authoriseInfo->getState(),
]),
];
}
if($uriInfo->getString() !== $redirectUri) {
$response->setStatusCode(400);
return [
'redirect' => self::buildCallbackUri($redirectUri, [
'error' => 'invalid_request',
'error_description' => 'Attempt at request forgery detected.',
'state' => $authoriseInfo->getState(),
]),
];
}
$approved = $approve === 'yes';
$authoriseData->setAuthoriseApproval($authoriseInfo, $approved);
if($approved)
$response->redirect(self::buildCallbackUri($uriInfo->getString(), [
'code' => $authoriseInfo->getCode(),
'state' => $authoriseInfo->getState(),
]));
else
$response->redirect(self::buildCallbackUri($uriInfo->getString(), [
return [
'redirect' => self::buildCallbackUri($uriInfo->getString(), [
'code' => $authoriseInfo->getCode(),
'state' => $authoriseInfo->getState(),
]),
];
return [
'redirect' => self::buildCallbackUri($uriInfo->getString(), [
'error' => 'access_denied',
'state' => $authoriseInfo->getState(),
]));
]),
];
}
private static function error(string $code, string $message = '', string $url = ''): array {

View file

@ -1,23 +1,87 @@
{% extends 'oauth2/master.twig' %}
{% block body %}
<h1><a href="{{ app.website }}" target="_blank">{{ app.name }}</a></h1>
<p>{{ app.summary }}</p>
<p>{% if app.isTrusted %}This is an official application. Things should probably just implicitly authorise if we hit this.{% else %}This is a third-party application.{% endif %}</p>
<header class="oauth2-header"{% if auth.user.colour != 'inherit' %} style="background-color: {{ auth.user.colour }}"{% endif %}>
<div class="oauth2-userhead">
<div class="oauth2-userhead-main">
<div class="oauth2-userhead-main-avatar">
<div class="oauth2-userhead-main-avatar-image">
<img src="{{ auth.user.avatars.original }}" alt="">
</div>
</div>
<div class="oauth2-userhead-main-name">
<a href="{{ auth.user.profile_url }}" target="_blank">{{ auth.user.name }}</a>
</div>
</div>
{% if auth.guise is defined %}
<div class="oauth2-userhead-guise">
<div class="oauth2-userhead-guise-avatar">
<div class="oauth2-userhead-guise-avatar-image">
<img src="{{ auth.guise.avatars.x60 }}" alt="">
</div>
</div>
<div class="oauth2-userhead-guise-text">
<p>Are you <a href="{{ auth.guise.profile_url }}" target="_blank" style="color: {{ auth.guise.colour }}">{{ auth.guise.name }}</a> and did you mean to use your own account?</p>
<p><a href="{{ auth.guise.revert_url }}" target="_blank">Click here</a> and reload this page.</p>
</div>
</div>
{% endif %}
</div>
</header>
<div class="oauth2-body">
<div class="oauth2-banner">
<div class="oauth2-banner-text">
Authorisation Request
</div>
<div class="oauth2-banner-logo">
Flashii
</div>
</div>
<p>You are logged in as <a href="{{ auth.user.profile_url }}" target="_blank" style="color: {{ auth.user.colour }}">{{ auth.user.name }}</a>.</p>
{% if auth.guise is defined %}
<p>Are you <a href="{{ auth.guise.profile_url }}" target="_blank" style="color: {{ auth.guise.colour }}">{{ auth.guise.name }}</a> and did you mean to use your own account? <a href="{{ auth.guise.revert_url }}" target="_blank">Click here</a> and reload this page.</p>
{% endif %}
<div class="oauth2-authorise-requesting">
<p>A third-party application is requesting permission to access your account.</p>
</div>
<form method="post" action="/oauth2/authorise">
<input type="hidden" name="code" value="{{ req.code }}">
<input type="hidden" name="approve" value="yes">
<button>Authorise</button>
</form>
<form method="post" action="/oauth2/authorise">
<input type="hidden" name="code" value="{{ req.code }}">
<input type="hidden" name="approve" value="no">
<button>Cancel</button>
</form>
<div class="oauth2-appinfo">
<div class="oauth2-appinfo-name">
{{ app.name }}
</div>{# TODO: author should be listed #}
<div class="oauth2-appinfo-links">
<a href="{{ app.website }}" target="_blank" rel="noopener noreferrer" class="oauth2-appinfo-link" title="Website">
<div class="oauth2-appinfo-link-icon oauth2-appinfo-link-icon-globe"></div>
<div class="oauth2-appinfo-link-text">{{ app.websiteDisplay }}</div>
</a>
</div>
<div class="oauth2-appinfo-summary">
<p>{{ app.summary }}</p>
</div>
</div>
<div class="oauth2-scope">
<div class="oauth2-scope-header">This application will be able to:</div>
<div class="oauth2-scope-perms">
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">Do anything because I have not made up scopes yet.</div>
</div>
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">Eat soup.</div>
</div>
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">These are placeholders.</div>
</div>
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">This one is really long because I want to test wrapping and how the chevron icon thing will handle it so there will be a lot of text here, the app will not be gaining anything from it but yeah sometimes you just need to explode seventy times.</div>
</div>
</div>
</div>
<div class="oauth2-authorise-buttons">
<button type="button" class="oauth2-authorise-button oauth2-authorise-button-accept js-authorise-action" data-redirect="{{ redirect_uri }}" data-csrfp="{{ csrfp_token }}" data-approve="yes" data-code="{{ req.code }}" disabled>Authorise</button>
<button type="button" class="oauth2-authorise-button oauth2-authorise-button-deny js-authorise-action" data-redirect="{{ redirect_uri }}" data-csrfp="{{ csrfp_token }}" data-approve="no" data-code="{{ req.code }}" disabled>Cancel</button>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,15 @@
{% extends 'oauth2/master.twig' %}
{% block body %}
<header class="oauth2-header">
<div class="oauth2-errorhead">
<div class="oauth2-errorhead-icon"></div>
<div class="oauth2-errorhead-text">
An error occurred!
</div>
</div>
</header>
<div class="oauth2-errorbody">
<p>{{ error_description }}</p>
</div>
{% endblock %}

View file

@ -1,9 +1,71 @@
{% extends 'oauth2/master.twig' %}
{% block body %}
<h1><a href="{{ app.website }}" target="_blank">{{ app.name }}</a></h1>
<p>{{ app.summary }}</p>
<p>{% if app.isTrusted %}This is an official application. Things should probably just implicitly authorise if we hit this.{% else %}This is a third-party application.{% endif %}</p>
<header class="oauth2-header">
<div class="oauth2-loginhead">
<div class="oauth2-loginhead-icon"></div>
<div class="oauth2-loginhead-text">
Not logged in!
</div>
</div>
</header>
<div class="oauth2-body">
<div class="oauth2-banner">
<div class="oauth2-banner-text">
Authorisation Request
</div>
<div class="oauth2-banner-logo">
Flashii
</div>
</div>
<p>You must be logged in to authorise applications. <a href="{{ auth.login_url }}" target="_blank">Log in</a> or <a href="{{ auth.register_url }}" target="_blank">create an account</a> and reload this page to try again.</p>
<div class="oauth2-authorise-requesting">
{% if not app.isTrusted %}
<p>A third-party application is requesting permission to access your account.</p>
{% endif %}
<p>You must be logged in to authorise applications. <a href="{{ auth.login_url }}" target="_blank">Log in</a> or <a href="{{ auth.register_url }}" target="_blank">create an account</a> and reload this page to try again.</p>
{% if app.isTrusted %}
<p>You will be redirected to the following application after logging in.</p>
{% endif %}
</div>
<div class="oauth2-appinfo">
<div class="oauth2-appinfo-name">
{{ app.name }}
</div>{# TODO: author should be listed #}
<div class="oauth2-appinfo-links">
<a href="{{ app.website }}" target="_blank" rel="noopener noreferrer" class="oauth2-appinfo-link" title="Website">
<div class="oauth2-appinfo-link-icon oauth2-appinfo-link-icon-globe"></div>
<div class="oauth2-appinfo-link-text">{{ app.websiteDisplay }}</div>
</a>
</div>
<div class="oauth2-appinfo-summary">
<p>{{ app.summary }}</p>
</div>
</div>
<div class="oauth2-scope">
<div class="oauth2-scope-header">This application will be able to:</div>
<div class="oauth2-scope-perms">
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">Do anything because I have not made up scopes yet.</div>
</div>
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">Eat soup.</div>
</div>
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">These are placeholders.</div>
</div>
<div class="oauth2-scope-perm">
<div class="oauth2-scope-perm-icon"></div>
<div class="oauth2-scope-perm-text">This one is really long because I want to test wrapping and how the chevron icon thing will handle it so there will be a lot of text here, the app will not be gaining anything from it but yeah sometimes you just need to explode seventy times.</div>
</div>
</div>
</div>
</div>
{% endblock %}

View file

@ -4,10 +4,17 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>{% if title is defined %}{{ title }} :: {% endif %}{{ globals.siteInfo.name }}</title>
<link href="{{ asset('hanyuu.css') }}" rel="stylesheet">
<link href="{{ asset('oauth2.css') }}" rel="stylesheet">
</head>
<body>
{% block body %}{% endblock %}
<div class="oauth2-wrapper">
<div class="oauth2-dialog">
<div class="oauth2-dialog-body">
{% block body %}{% endblock %}
</div>
</div>
</div>
<script src="{{ asset('oauth2.js') }}"></script>
</body>
</html>