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": case "NotFound":
throw ApiError::notFound("user"); throw ApiError::notFound("user");
case "Invalid": case "Invalid":
throw ApiError::unauthorized("Invalid username or password"); throw ApiError::notAllowed("Invalid username or password");
default: default:
throw $err; throw $err;
} }

View File

@ -60,7 +60,7 @@ class Posts extends Api
$post = Post::getByID($id); $post = Post::getByID($id);
// Throw 400 if we aren't admin but trying to edit another users post. // 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. // Try update.
Response::json($post->update($content)); 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" } value: { "code": "NotFound", "entity": "post" }
tags: tags:
- Post - 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: /register:
post: post:
summary: Register summary: Register
@ -678,8 +717,7 @@ components:
UnauthorizedResponse: UnauthorizedResponse:
type: object type: object
properties: properties:
code: code: type:NotAllowed
type: Unauthorized
message: message:
type: string type: string
FailedResponse: FailedResponse:
@ -731,6 +769,8 @@ components:
$ref: "#/components/schemas/UserResponse" $ref: "#/components/schemas/UserResponse"
token: token:
type: string type: string
refreshToken:
type: string
UserResponse: UserResponse:
type: object type: object
properties: properties:
@ -855,6 +895,14 @@ components:
properties: properties:
content: content:
type: string type: string
RefreshRequest:
type: object
required:
- refreshToken
properties:
refreshToken:
type: string
format: uuid4
securitySchemes: securitySchemes:
BasicAuth: BasicAuth:
type: apiKey type: apiKey
@ -864,4 +912,5 @@ tags:
- name: Login/Logout - name: Login/Logout
- name: Post - name: Post
- name: Register - name: Register
- name: Refresh
- name: User - name: User

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

@ -61,4 +61,14 @@ class Config
{ {
return Config::getInstance()->database; 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 FROM
egb_benutzer AS b egb_benutzer AS b
WHERE WHERE
token = :TOKEN" token = :TOKEN AND
tokenExpiry > NOW()"
); );
$stmt->bindValue(":TOKEN", $token); $stmt->bindValue(":TOKEN", $token);
$stmt->execute(); $stmt->execute();
@ -154,18 +155,29 @@ class User implements JsonSerializable
$stmt->bindValue(":ID", $user->getID()); $stmt->bindValue(":ID", $user->getID());
$stmt->execute(); $stmt->execute();
} }
// Generate token // Generate tokens only if expired or missing
$stmt = $db->prepare("UPDATE egb_benutzer SET token = UUID() WHERE id = :ID"); if (empty($data["token"]) || new DateTime($data["tokenExpiry"]) <= new DateTime()) {
$stmt->bindValue(":ID", $user->getID()); $stmt = $db->prepare(
$stmt->execute(); "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 // 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->bindValue(":ID", $user->getID());
$stmt->execute(); $stmt->execute();
$token = $stmt->fetch(PDO::FETCH_COLUMN, 0); [$token, $refresh] = $stmt->fetch(PDO::FETCH_NUM);
// Return user and token // Return user and tokens
if ($token) { if ($token) {
return ["user" => $user, "token" => $token]; return ["user" => $user, "token" => $token, "refreshToken" => $refresh];
} }
// Token generation failed // Token generation failed
throw new Exception("Failed"); throw new Exception("Failed");
@ -261,6 +273,58 @@ class User implements JsonSerializable
return ["pages" => intdiv($count, $limit + 1), "data" => $list]; 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 * Members
*/ */
@ -268,7 +332,16 @@ class User implements JsonSerializable
public function logOut(): bool public function logOut(): bool
{ {
$db = Database::getInstance(); $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); $stmt->bindValue(":ID", $this->id);
return $stmt->execute(); return $stmt->execute();
} }

View File

@ -4,5 +4,7 @@ return [
"basePath" => "/phpCourse/exam/", "basePath" => "/phpCourse/exam/",
"storagePath" => "/phpCourse/exam/storage/", "storagePath" => "/phpCourse/exam/storage/",
"baseFSPath" => "/home/k/khofmann/public_html/phpCourse/exam/", "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 // Register and confirm
SimpleRouter::post("/register", [Api\Register\Register::class, "post"]); SimpleRouter::post("/register", [Api\Register\Register::class, "post"]);
SimpleRouter::patch("/register", [Api\Register\Register::class, "patch"]); SimpleRouter::patch("/register", [Api\Register\Register::class, "patch"]);
// Refresh (open, but still needs token)
SimpleRouter::post("/refresh", [Api\Refresh\Refresh::class, "post"]);
/* /*
* Optional Auth * Optional Auth