This commit is contained in:
Kilian Hofmann 2024-07-29 00:41:10 +02:00
commit 7d92daeccd
11 changed files with 217 additions and 27 deletions

View File

@ -33,7 +33,7 @@ class Login extends Api
case "NotFound":
throw ApiError::notFound("user");
case "Invalid":
throw ApiError::unauthorized("Invalid username or password");
throw ApiError::notAllowed("Invalid username or password");
default:
throw $err;
}

View File

@ -60,7 +60,7 @@ class Posts extends Api
$post = Post::getByID($id);
// Throw 400 if we aren't admin but trying to edit another users post.
if (!$self->getIsAdmin() && $post->getUser()->getID() !== $self->getID()) throw ApiError::unauthorized("Not allowed");
if (!$self->getIsAdmin() && $post->getUser()->getID() !== $self->getID()) throw ApiError::notAllowed("Not allowed");
// Try update.
Response::json($post->update($content));

View File

@ -0,0 +1,38 @@
<?php
namespace Api\Refresh;
use Exception;
use Khofmann\Api\Api;
use Khofmann\ApiError\ApiError;
use Khofmann\Input\Input;
use Khofmann\Response\Response;
use Khofmann\Models\User\User;
use Khofmann\Request\Request;
class Refresh extends Api
{
public function post(): void
{
// Fetch all required inputs.
// Throw 400 error if a required one is missing.
$token = Request::token();
$refreshToken = Input::post("refreshToken");
if (empty($refreshToken)) throw ApiError::missingField(["refreshToken"]);
// Try and log in user.
// Throw errors according to situation.
try {
Response::json(User::refresh($token, $refreshToken));
} catch (Exception $err) {
switch ($err->getMessage()) {
case "Failed":
throw ApiError::failed("Refresh failed");
case "NotFound":
throw ApiError::unauthorized("Not authorized");
default:
throw $err;
}
}
}
}

View File

@ -310,6 +310,45 @@ paths:
value: { "code": "NotFound", "entity": "post" }
tags:
- Post
/refresh:
post:
summary: Refresh
description: Token refresh.
security:
- BasicAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/RefreshRequest"
responses:
200:
description: Success.
content:
application/json:
schema:
$ref: "#/components/schemas/LoginResponse"
400:
description: Missing fields.
content:
application/json:
schema:
$ref: "#/components/schemas/MissingFieldResponse"
examples:
Missing fields:
value: { "code": "MissingField", "fields": ["refreshToken"] }
500:
description: Failed.
content:
application/json:
schema:
$ref: "#/components/schemas/FailedResponse"
examples:
Failed:
value: { "code": "Failed", "message": "Refresh failed" }
tags:
- Refresh
/register:
post:
summary: Register
@ -678,8 +717,7 @@ components:
UnauthorizedResponse:
type: object
properties:
code:
type: Unauthorized
code: type:NotAllowed
message:
type: string
FailedResponse:
@ -731,6 +769,8 @@ components:
$ref: "#/components/schemas/UserResponse"
token:
type: string
refreshToken:
type: string
UserResponse:
type: object
properties:
@ -855,6 +895,14 @@ components:
properties:
content:
type: string
RefreshRequest:
type: object
required:
- refreshToken
properties:
refreshToken:
type: string
format: uuid4
securitySchemes:
BasicAuth:
type: apiKey
@ -864,4 +912,5 @@ tags:
- name: Login/Logout
- name: Post
- name: Register
- name: Refresh
- name: User

File diff suppressed because one or more lines are too long

View File

@ -27,7 +27,7 @@ class ApiError extends Exception
]), 400);
}
public static function unauthorized(string $message)
public static function notAllowed(string $message)
{
return new ApiError(json_encode([
"code" => "NotAllowed",
@ -35,6 +35,14 @@ class ApiError extends Exception
]), 401);
}
public static function unauthorized(string $message)
{
return new ApiError(json_encode([
"code" => "Unauthorized",
"message" => $message,
]), 401);
}
public static function notFound(string $entity)
{
return new ApiError(json_encode([

View File

@ -34,7 +34,7 @@ class AdminAuth implements IMiddleware
->header("Access-Control-Allow-Methods: *")
->header("Access-Control-Allow-Headers: *")
->httpCode(401)
->json(["code" => "Unauthorized", "message" => "Not Authorized"]);
->json(["code" => "NotAllowed", "message" => "Not Authorized"]);
}
} catch (Exception $err) {
// No user with this token exists

View File

@ -61,4 +61,14 @@ class Config
{
return Config::getInstance()->database;
}
public static function getTokenExpiry(): string
{
return Config::getInstance()->app["tokenExpiry"];
}
public static function getRefreshTokenExpiry(): string
{
return Config::getInstance()->app["refreshTokenExpiry"];
}
}

View File

@ -114,7 +114,8 @@ class User implements JsonSerializable
FROM
egb_benutzer AS b
WHERE
token = :TOKEN"
token = :TOKEN AND
tokenExpiry > NOW()"
);
$stmt->bindValue(":TOKEN", $token);
$stmt->execute();
@ -154,18 +155,29 @@ class User implements JsonSerializable
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
}
// Generate token
$stmt = $db->prepare("UPDATE egb_benutzer SET token = UUID() WHERE id = :ID");
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
// Generate tokens only if expired or missing
if (empty($data["token"]) || new DateTime($data["tokenExpiry"]) <= new DateTime()) {
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
token = UUID(),
tokenExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getTokenExpiry() . "),
refreshToken = UUID(),
refreshExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getRefreshTokenExpiry() . ")
WHERE id = :ID"
);
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
}
// Get token
$stmt = $db->prepare("SELECT token FROM egb_benutzer WHERE id = :ID");
$stmt = $db->prepare("SELECT token, refreshToken FROM egb_benutzer WHERE id = :ID");
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
$token = $stmt->fetch(PDO::FETCH_COLUMN, 0);
// Return user and token
[$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
// Return user and tokens
if ($token) {
return ["user" => $user, "token" => $token];
return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
}
// Token generation failed
throw new Exception("Failed");
@ -261,6 +273,58 @@ class User implements JsonSerializable
return ["pages" => intdiv($count, $limit + 1), "data" => $list];
}
public static function refresh(string $token, string $refreshToken)
{
$db = Database::getInstance();
$stmt = $db->prepare(
"SELECT
b.id, b.benutzer, b.status, b.email, b.image, b.isadmin, b.zeitstempel, b.tokenExpiry,
(SELECT COUNT(*) FROM egb_gaestebuch WHERE benutzer_id = b.id) as postCount
FROM
egb_benutzer AS b
WHERE
token = :TOKEN AND
refreshToken = :REFRESH AND
refreshExpiry > NOW()"
);
$stmt->bindValue(":TOKEN", $token);
$stmt->bindValue(":REFRESH", $refreshToken);
$stmt->execute();
$data = $stmt->fetch();
if (!$data) throw new Exception("NotFound");
$user = new User($data["id"], $data["benutzer"], $data["status"], $data["email"], $data["zeitstempel"], $data["image"], $data["isadmin"] === 1, $data["postCount"]);
// Update tokens if expired
if (new DateTime($data["tokenExpiry"]) <= new DateTime()) {
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
token = UUID(),
tokenExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getTokenExpiry() . "),
refreshToken = UUID(),
refreshExpiry = DATE_ADD(NOW(), INTERVAL " . Config::getRefreshTokenExpiry() . ")
WHERE id = :ID"
);
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
}
// Get token
$stmt = $db->prepare("SELECT token, refreshToken FROM egb_benutzer WHERE id = :ID");
$stmt->bindValue(":ID", $user->getID());
$stmt->execute();
[$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
// Return user and tokens
if ($token) {
return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
}
// Token generation failed
throw new Exception("Failed");
}
/*
* Members
*/
@ -268,7 +332,16 @@ class User implements JsonSerializable
public function logOut(): bool
{
$db = Database::getInstance();
$stmt = $db->prepare("UPDATE egb_benutzer SET token = NULL WHERE id = :ID");
$stmt = $db->prepare(
"UPDATE
egb_benutzer
SET
token = NULL,
tokenExpiry = NULL,
refreshToken = NULL,
refreshExpiry = NULL
WHERE id = :ID"
);
$stmt->bindValue(":ID", $this->id);
return $stmt->execute();
}

View File

@ -4,5 +4,7 @@ return [
"basePath" => "/phpCourse/exam/",
"storagePath" => "/phpCourse/exam/storage/",
"baseFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/",
"storageFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/storage/"
"storageFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/storage/",
"tokenExpiry" => "5 MINUTE",
"refreshTokenExpiry" => "30 MINUTE"
];

View File

@ -35,6 +35,8 @@ SimpleRouter::post("/login", [Api\Login\Login::class, "post"]);
// Register and confirm
SimpleRouter::post("/register", [Api\Register\Register::class, "post"]);
SimpleRouter::patch("/register", [Api\Register\Register::class, "patch"]);
// Refresh (open, but still needs token)
SimpleRouter::post("/refresh", [Api\Refresh\Refresh::class, "post"]);
/*
* Optional Auth