diff --git a/public/index.php b/public/index.php
index 8309b26..44cc8b2 100644
--- a/public/index.php
+++ b/public/index.php
@@ -5,6 +5,18 @@ use Exception;
require_once __DIR__ . '/../startup.php';
+if(!function_exists('base64uri_encode')) {
+ function base64uri_encode(string $string): string {
+ return rtrim(strtr(base64_encode($string), '+/', '-_'), '=');
+ }
+}
+
+if(!function_exists('base64uri_decode')) {
+ function base64uri_decode(string $string, bool $strict = false): string|false {
+ return base64_decode(str_pad(strtr($string, '-_', '+/'), strlen($string) % 4, '=', STR_PAD_RIGHT));
+ }
+}
+
function page_url(string $path, array $params = []): string {
if(isset($params['p']))
unset($params['p']);
@@ -55,14 +67,10 @@ function html_header(array $vars = []): string {
['text' => 'Settings', 'link' => page_url('/settings')],
];
- if(Config::get('user.invite_only', Config::TYPE_BOOL))
- $userMenu[] = ['text' => 'Invites', 'link' => page_url('/settings/invites')];
-
$userMenu[] = ['text' => 'Log out', 'link' => page_url('/auth/logout', ['s' => UserSession::instance()->getSmallToken()])];
} else {
$userMenu = [
['text' => 'Log in', 'link' => page_url('/auth/login')],
- ['text' => 'Register', 'link' => page_url('/auth/register')],
];
}
@@ -861,7 +869,7 @@ if(preg_match('#^/uploads/([a-zA-Z0-9-_]{16}).json$#', $reqPath, $matches)) {
if($reqPath === '/settings') {
if(!UserSession::hasInstance()) {
- http_response_code(404);
+ http_response_code(403);
echo html_information('You must be logged in to access this page.');
return;
}
@@ -872,62 +880,6 @@ if($reqPath === '/settings') {
return;
}
-if($reqPath === '/settings/invites') {
- if(!UserSession::hasInstance()) {
- http_response_code(404);
- echo html_information('You must be logged in to access this page.');
- return;
- }
-
- $currentUser = UserSession::instance()->getUser();
- $createdInvites = UserInvite::fromCreator($currentUser);
- $createToken = $currentUser->getId() !== 1 /*&& count($createdInvites) >= 5*/ ? '' : UserSession::instance()->getSmallToken(11);
-
- if($reqMethod === 'POST') {
- if(empty($createToken)) {
- $inviteError = 'You\'ve reached the maximum amount of invites you can generate.';
- } elseif($createToken !== filter_input(INPUT_POST, 'invite_token')) {
- $inviteError = 'Cannot create invite.';
- } else {
- try {
- $createdInvites[] = $createdInvite = UserInvite::create($currentUser);
- $inviteError = $createdInvite->getToken();
- } catch(UserInviteCreationFailedException $ex) {
- $inviteError = 'Invite creation failed.';
- }
- }
- }
-
- if(isset($inviteError))
- $inviteError = Template::renderRaw('error', [
- 'error_text' => $inviteError,
- ]);
-
- if(!empty($createToken))
- $inviteCreate = Template::renderRaw('settings/invites-create', [
- 'create_token' => $createToken,
- ]);
-
- $invitesItems = [];
- foreach($createdInvites as $inviteItem) {
- $invitesItems[] = [
- 'invite_created' => date('c', $inviteItem->getCreated()),
- 'invite_used' => (($_used = $inviteItem->getUsed()) === null ? 'Unused' : date('c', $_used)),
- 'invite_user' => (($_user = $inviteItem->getUser()) === null ? 'Unused' : sprintf('%1$s', $_user->getUsername())),
- 'invite_token' => $inviteItem->getToken(),
- ];
- }
-
- echo html_header(['title' => 'Invites - YTKNS']);
- Template::render('settings/invites', [
- 'invite_error' => $inviteError ?? '',
- 'invite_create' => $inviteCreate ?? '',
- 'invite_list' => Template::renderSet('settings/invites-item', $invitesItems),
- ]);
- echo html_footer();
- return;
-}
-
if($reqPath === '/auth/login') {
if(YTKNS_MAINTENANCE) {
http_response_code(503);
@@ -941,161 +893,130 @@ if($reqPath === '/auth/login') {
return;
}
- if($reqMethod === 'POST') {
- $loginUsername = filter_input(INPUT_POST, 'username');
- $loginPassword = filter_input(INPUT_POST, 'password');
- $loginRemember = !empty(filter_input(INPUT_POST, 'remember'));
+ if(filter_has_var(INPUT_GET, 'state')) {
+ $state = base64uri_decode((string)filter_input(INPUT_GET, 'state'));
+ if($state === false || strlen($state) !== 60) {
+ http_response_code(400);
+ echo html_information('Provided state is invalid.');
+ return;
+ }
- if(empty($loginUsername) || empty($loginPassword)) {
- $authError = 'Username or password missing.';
- } else {
+ $signature = hash_hmac('sha256', substr($state, 32), YTKNS_OA2_STATE_SECRET, true);
+ if(!hash_equals($signature, substr($state, 0, 32))) {
+ http_response_code(403);
+ echo html_information('Request verification failed.');
+ return;
+ }
+
+ $state = unpack('a32hash/Jtime/a20verifier', $state);
+ if($state === false) {
+ http_response_code(500);
+ echo html_information('State unpack failed.');
+ return;
+ }
+
+ if($state['time'] < strtotime('-15 minutes') || $state['time'] > strtotime('+15 minutes')) {
+ http_response_code(403);
+ echo html_information('Authorisation request timestamp is no longer valid.');
+ return;
+ }
+
+ if(filter_has_var(INPUT_GET, 'error')) {
+ $error = (string)filter_input(INPUT_GET, 'error');
+ $text = (string)filter_input(INPUT_GET, 'error_description');
+ if($text === '')
+ $text = match($error) {
+ 'access_denied' => 'You rejected the authorisation request.',
+ 'invalid_request' => 'YTKNS sent an invalid request to the authorisation server, please report this!',
+ 'invalid_scope' => 'YTKNS sent an invalid request to the authorisation server, please report this!',
+ 'server_error' => 'Something went wrong on the authorisation server, please try again later.',
+ default => sprintf('An unexpected error occurred: %s', $error),
+ };
+
+ http_response_code(400);
+ echo html_information(htmlspecialchars($text));
+ return;
+ }
+
+ if(filter_has_var(INPUT_GET, 'code')) {
try {
- $loginUser = User::forLogin($loginUsername);
- } catch(\Exception $ex) {}
+ $postData = sprintf(
+ 'grant_type=authorization_code&code=%s&code_verifier=%s',
+ rawurlencode((string)filter_input(INPUT_GET, 'code')),
+ rawurldecode($state['verifier'])
+ );
+ $authz = sprintf('Basic %s', base64_encode(sprintf('%s:%s', YTKNS_OA2_CLIENT_ID, YTKNS_OA2_CLIENT_SECRET)));
- if(empty($loginUser) || empty($loginUser->password) || !password_verify($loginPassword, $loginUser->password)) {
- $authError = 'Username or password was invalid.';
- } else {
- $session = UserSession::create($loginUser, $loginRemember);
- $session->setInstance();
- setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
- echo html_information('You are now logged in!', 'Welcome', '/');
- return;
- }
- }
- }
+ $tokenInfo = json_decode(file_get_contents('https://api.flashii.net/oauth2/token', false, stream_context_create([
+ 'http' => [
+ 'method' => 'POST',
+ 'header' => implode("\r\n", [
+ sprintf('Authorization: %s', $authz),
+ 'Content-Type: application/x-www-form-urlencoded',
+ sprintf('Content-Length: %d', strlen($postData)),
+ 'User-Agent: YTKNS',
+ ]),
+ 'content' => $postData,
+ ],
+ ])));
- if(isset($authError))
- $authError = Template::renderRaw('error', [
- 'error_text' => $authError,
- ]);
+ if(isset($tokenInfo->access_token)) {
+ $fUserInfo = json_decode(file_get_contents('https://api.flashii.net/v1/me', false, stream_context_create([
+ 'http' => [
+ 'method' => 'GET',
+ 'header' => implode("\r\n", [
+ sprintf('Authorization: Bearer %s', $tokenInfo->access_token),
+ 'User-Agent: YTKNS',
+ ]),
+ ],
+ ])));
- $authFields = [
- [
- 'field_title' => 'Username or E-Mail Address',
- 'field_type' => 'text',
- 'field_name' => 'username',
- 'field_value' => ($loginUsername ?? ''),
- ],
- [
- 'field_title' => 'Password',
- 'field_type' => 'password',
- 'field_name' => 'password',
- 'field_value' => '',
- ],
- ];
-
- echo html_header(['title' => 'Log in - YTKNS']);
- Template::render('auth/login' . (isset($_GET['new']) ? '2' : ''), [
- 'auth_error' => $authError ?? '',
- 'auth_fields' => Template::renderSet('auth/field', $authFields),
- 'auth_remember' => ($loginRemember ?? false) ? ' checked' : '',
- ]);
- echo html_footer();
- return;
-}
-
-if($reqPath === '/auth/register') {
- if(YTKNS_MAINTENANCE) {
- http_response_code(503);
- echo html_information('You cannot register during maintenance.');
- return;
- }
-
- if(UserSession::hasInstance()) {
- http_response_code(404);
- echo html_information('You are logged in already.');
- return;
- }
-
- $inviteOnly = Config::get('user.invite_only', Config::TYPE_BOOL);
-
- if($reqMethod === 'POST') {
- $registerUsername = filter_input(INPUT_POST, 'username');
- $registerPassword = filter_input(INPUT_POST, 'password');
- $registerEMail = filter_input(INPUT_POST, 'email');
- $registerInvite = filter_input(INPUT_POST, 'invite');
-
- if(empty($registerUsername) || empty($registerPassword) || empty($registerEMail) || ($inviteOnly && empty($registerInvite))) {
- $authError = 'You must fill in all fields.';
- } else {
- if($inviteOnly) {
- try {
- $userInvite = UserInvite::byToken($registerInvite);
-
- if($userInvite->isUsed()) {
- $authError = 'Invalid invite token.';
+ if(empty($fUserInfo->id)) {
+ http_response_code(500);
+ echo html_information('Authentication failed.');
+ return;
}
- } catch(UserInviteNotFoundException $ex) {
- $authError = 'Invalid invite token.';
- }
- }
- if(!isset($authError)) {
- try {
- $createdUser = User::create(
- $registerUsername,
- $registerPassword,
- $registerEMail
- );
+ try {
+ $userInfo = User::byRemoteId($fUserInfo->id);
+ $loginMessage = 'You are now logged in!';
+ } catch(UserNotFoundException) {
+ try {
+ $userInfo = User::create($fUserInfo->id, $fUserInfo->name);
+ } catch(\PDOException) {
+ $userInfo = User::create($fUserInfo->id, sprintf('%s_%04d', $fUserInfo->name, random_int(0, 9999)));
+ }
- if(isset($userInvite))
- $userInvite->markUsed($createdUser);
- } catch(UserCreationInvalidNameException $ex) {
- $authError = 'Your username contains invalid characters or is too short or long.
Must be between 1 and 20 characters and may only contains alphanumeric characters as well as - and _.';
- } catch(UserCreationInvalidPasswordException $ex) {
- $authError = 'Your password must have at least 6 unique characters.';
- } catch(UserCreationInvalidMailException $ex) {
- $authError = 'Your e-mail address isn\'t real.';
- } catch(UserCreationFailedException $ex) {
- $authError = 'Failed to create user.';
+ $loginMessage = 'Your account been created!';
+ }
+
+ // leaving session bumping off for now, the implementation needs to be better for that
+ $session = UserSession::create($userInfo, false);
+ $session->setInstance();
+ setcookie('ytkns_login', $session->getToken(), $session->getExpires(), '/', '.' . Config::get('domain.main'), false, true);
+ echo html_information($loginMessage, 'Welcome', '/');
+ return;
}
+ } catch(\Exception $ex) {
+ http_response_code(500);
+ echo html_information('Authorisation request failed, please try again.');
}
+ return;
}
- } elseif($reqMethod === 'GET') {
- $registerInvite = filter_input(INPUT_GET, 'inv', FILTER_SANITIZE_STRING);
}
- $authFields = [
- [
- 'field_title' => 'Username',
- 'field_type' => 'text',
- 'field_name' => 'username',
- 'field_value' => ($registerUsername ?? ''),
- ],
- [
- 'field_title' => 'Password',
- 'field_type' => 'password',
- 'field_name' => 'password',
- 'field_value' => '',
- ],
- [
- 'field_title' => 'E-Mail Address',
- 'field_type' => 'email',
- 'field_name' => 'email',
- 'field_value' => ($registerEMail ?? ''),
- ],
- ];
+ $verifier = random_bytes(20);
+ $time = pack('J', time());
+ $signature = hash_hmac('sha256', $time . $verifier, YTKNS_OA2_STATE_SECRET, true);
+ $state = base64uri_encode($signature . $time . $verifier);
- if($inviteOnly)
- $authFields[] = [
- 'field_title' => 'Invitation',
- 'field_type' => 'password',
- 'field_name' => 'invite',
- 'field_value' => ($registerInvite ?? ''),
- ];
-
- if(isset($authError))
- $authError = Template::renderRaw('error', [
- 'error_text' => $authError,
- ]);
-
- echo html_header(['title' => 'Register - YTKNS']);
- Template::render('auth/register', [
- 'auth_error' => $authError ?? '',
- 'auth_fields' => Template::renderSet('auth/field', $authFields),
- ]);
- echo html_footer();
+ header(sprintf(
+ 'Location: https://id.flashii.net/oauth2/authorise?response_type=code&scope=identify&code_challenge_method=S256&client_id=%s&state=%s&code_challenge=%s&redirect_uri=%s',
+ rawurlencode(YTKNS_OA2_CLIENT_ID),
+ rawurlencode($state),
+ rawurlencode(base64uri_encode(hash('sha256', $verifier, true))),
+ rawurlencode('https://ytkns.com/auth/login'),
+ ));
return;
}
diff --git a/src/User.php b/src/User.php
index c23ec13..94d8088 100644
--- a/src/User.php
+++ b/src/User.php
@@ -5,9 +5,6 @@ use Exception;
class UserNotFoundException extends Exception {}
class UserCreationFailedException extends Exception {}
-class UserCreationInvalidNameException extends Exception {}
-class UserCreationInvalidPasswordException extends Exception {}
-class UserCreationInvalidMailException extends Exception {}
#[\AllowDynamicProperties]
class User {
@@ -21,13 +18,17 @@ class User {
$this->user_id = $userId;
}
+ public function getRemoteId(): string {
+ return (string)($this->remote_id ?? '');
+ }
+
public function getUsername(): string {
return $this->username ?? '';
}
public static function byId(int $userId): self {
$getUser = DB::prepare('
- SELECT `user_id`, `username`, `email`, `password`, `user_created`
+ SELECT `user_id`, `remote_id`, `username`, `user_created`
FROM `ytkns_users`
WHERE `user_id` = :user
');
@@ -41,15 +42,13 @@ class User {
return $user;
}
- public static function forLogin(string $usernameOrEmail): self {
+ public static function byRemoteId(string $remoteId): self {
$getUser = DB::prepare('
- SELECT `user_id`, `username`, `email`, `password`, `user_created`
+ SELECT `user_id`, `remote_id`, `username`, `user_created`
FROM `ytkns_users`
- WHERE LOWER(`username`) = LOWER(:username)
- OR LOWER(`email`) = LOWER(:email)
+ WHERE `remote_id` = :remote
');
- $getUser->bindValue('username', $usernameOrEmail);
- $getUser->bindValue('email', $usernameOrEmail);
+ $getUser->bindValue('remote', $remoteId);
$getUser->execute();
$user = $getUser->fetchObject(self::class);
@@ -61,7 +60,7 @@ class User {
public static function forProfile(string $username): self {
$getUser = DB::prepare('
- SELECT `user_id`, `username`, `email`, `password`, `user_created`
+ SELECT `user_id`, `remote_id`, `username`, `user_created`
FROM `ytkns_users`
WHERE LOWER(`username`) = LOWER(:username)
');
@@ -75,44 +74,16 @@ class User {
return $user;
}
- public static function validatePassword(string $password): bool {
- $chars = [];
- $length = mb_strlen($password);
-
- for($i = 0; $i < $length; $i++) {
- $current = mb_substr($password, $i, 1);
-
- if(!in_array($current, $chars, true))
- $chars[] = $current;
- }
-
- return count($chars) >= 6;
- }
-
- public static function hashPassword(string $password): string {
- return password_hash($password, PASSWORD_ARGON2ID);
- }
-
- public static function create(string $userName, string $password, string $email): self {
- if(!preg_match('#^([a-zA-Z0-9-_]{1,20})$#', $userName))
- throw new UserCreationInvalidNameException;
- if(!filter_var($email, FILTER_VALIDATE_EMAIL))
- throw new UserCreationInvalidMailException;
- if(!self::validatePassword($password))
- throw new UserCreationInvalidPasswordException;
-
- $password = self::hashPassword($password);
-
+ public static function create(string $remoteId, string $userName): self {
$createUser = DB::prepare('
INSERT INTO `ytkns_users` (
- `username`, `email`, `password`
+ `remote_id`, `username`
) VALUES (
- :username, :email, :password
+ :remote, :username
)
');
+ $createUser->bindValue('remote', $remoteId);
$createUser->bindValue('username', $userName);
- $createUser->bindValue('email', $email);
- $createUser->bindValue('password', $password);
$userId = $createUser->execute() ? (int)DB::lastInsertId() : 0;
try {
diff --git a/src/UserInvite.php b/src/UserInvite.php
deleted file mode 100644
index 7d1dc71..0000000
--- a/src/UserInvite.php
+++ /dev/null
@@ -1,123 +0,0 @@
-invite_token;
- }
-
- public function getCreatorId(): int {
- return $this->created_by ?? 0;
- }
- public function getCreator(): User {
- return User::byId($this->getCreatedById());
- }
-
- public function getUserId(): ?int {
- return $this->used_by ?? null;
- }
- public function getUser(): ?User {
- $userId = $this->getUserId();
-
- if($userId === null)
- return null;
-
- try {
- return User::byId($this->getUserId());
- } catch(UserNotFoundException $ex) {
- return null;
- }
- }
-
- public function getCreated(): int {
- return $this->invite_created ?? 0;
- }
-
- public function getUsed(): ?int {
- return $this->invite_used ?? null;
- }
-
- public function isUsed(): bool {
- return !empty($this->used_by) || !empty($this->invite_used);
- }
-
- public function markUsed(User $user): void {
- $markUsed = DB::prepare('
- UPDATE `ytkns_users_invites`
- SET `used_by` = :user
- WHERE `invite_token` = UNHEX(:token)
- ');
- $markUsed->bindValue('user', $user->getId());
- $markUsed->bindValue('token', $this->getToken());
- $markUsed->execute();
- }
-
- public static function fromCreator(User $creator): array {
- $getInvites = DB::prepare('
- SELECT `created_by`, `used_by`,
- LOWER(HEX(`invite_token`)) AS `invite_token`,
- UNIX_TIMESTAMP(`invite_created`) AS `invite_created`,
- UNIX_TIMESTAMP(`invite_used`) AS `invite_used`
- FROM `ytkns_users_invites`
- WHERE `created_by` = :creator
- ');
- $getInvites->bindValue('creator', $creator->getId());
- $getInvites->execute();
- $invites = [];
-
- while($invite = $getInvites->fetchObject(self::class))
- $invites[] = $invite;
-
- return $invites;
- }
-
- public static function byToken(string $token, bool $isHex = true): self {
- $getInvite = DB::prepare(sprintf('
- SELECT `created_by`, `used_by`,
- LOWER(HEX(`invite_token`)) AS `invite_token`,
- UNIX_TIMESTAMP(`invite_created`) AS `invite_created`,
- UNIX_TIMESTAMP(`invite_used`) AS `invite_used`
- FROM `ytkns_users_invites`
- WHERE `invite_token` = %s(:token)
- ', $isHex ? 'UNHEX' : ''));
- $getInvite->bindValue('token', $token);
- $invite = $getInvite->execute() ? $getInvite->fetchObject(self::class) : false;
-
- if(!$invite)
- throw new UserInviteNotFoundException;
-
- return $invite;
- }
-
- public static function generateToken(): string {
- return bin2hex(random_bytes(self::TOKEN_LENGTH));
- }
-
- public static function create(User $creator): self {
- $inviteToken = self::generateToken();
- $insertInvite = DB::prepare('
- INSERT INTO `ytkns_users_invites` (
- `invite_token`, `created_by`
- ) VALUES (
- UNHEX(:token), :creator
- )
- ');
- $insertInvite->bindValue('token', $inviteToken);
- $insertInvite->bindValue('creator', $creator->getId());
- $insertInvite->execute();
-
- try {
- return self::byToken($inviteToken);
- } catch(UserInviteNotFoundException $ex) {
- throw new UserInviteCreationFailedException;
- }
- }
-}
diff --git a/templates/auth/field.html b/templates/auth/field.html
deleted file mode 100644
index de57e19..0000000
--- a/templates/auth/field.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
\ No newline at end of file
diff --git a/templates/auth/login.html b/templates/auth/login.html
deleted file mode 100644
index a6f2e95..0000000
--- a/templates/auth/login.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
:invite_token
-