diff --git a/docs/openapi.json b/docs/openapi.json index bb676856..ce1054b4 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1067,10 +1067,70 @@ } }, "/authentication/token": { + "get": { + "tags": [ + "Authentication" + ], + "summary": "Get authentication data", + "description": "Get information about the authenticated user.", + "parameters": [ + { + "in": "header", + "name": "X-Auth-Token", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "user": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the user" + }, + "userId": { + "type": "integer", + "description": "The id of the user" + }, + "isAdmin": { + "type": "boolean", + "description": "True if user is an admin, false if not." + } + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/400" + }, + "401": { + "$ref": "#/components/responses/401" + } + }, + "security": [ + { + "token": [] + } + ] + }, "post": { "tags": [ "Authentication" ], + "summary": "Create authentication token", "description": "Create an authentication token via email, password and optionally TOTP code. Add the token as X-Auth-Token header to further requests. Token lifetime 1d default, 30d with rememberMe.", "parameters": [ { @@ -1131,13 +1191,26 @@ "schema": { "type": "object", "properties": { - "userId": { - "type": "integer", - "description": "The id of the authenticated user." - }, - "token": { + "authToken": { "type": "string", "description": "The authentication token to be used in future requests." + }, + "user": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The username of the user" + }, + "userId": { + "type": "integer", + "description": "The ID of the authenticated user" + }, + "isAdmin": { + "type": "boolean", + "description": "True if the user is an admin, False if the user isn't." + } + } } } } @@ -1156,6 +1229,7 @@ "tags": [ "Authentication" ], + "summary": "Delete authentication token", "description": "Delete the authentication token provided in the X-Auth-Token header value.", "parameters": [ { @@ -1399,11 +1473,6 @@ "type": "apiKey", "name": "X-Auth-Token", "in": "header" - }, - "cookie": { - "type": "apiKey", - "name": "id", - "in": "cookie" } } } diff --git a/settings/phpcs.xml b/settings/phpcs.xml index 558074bc..edcebc3e 100644 --- a/settings/phpcs.xml +++ b/settings/phpcs.xml @@ -18,7 +18,11 @@ - + + + + + diff --git a/settings/routes.php b/settings/routes.php index 90801d6c..9f0d58aa 100644 --- a/settings/routes.php +++ b/settings/routes.php @@ -202,6 +202,7 @@ function addApiRoutes(RouterService $routerService, FastRoute\RouteCollector $ro $routes->add('GET', '/openapi', [Api\OpenApiController::class, 'getSchema']); $routes->add('POST', '/authentication/token', [Api\AuthenticationController::class, 'createToken']); $routes->add('DELETE', '/authentication/token', [Api\AuthenticationController::class, 'destroyToken']); + $routes->add('GET', '/authentication/token', [Api\AuthenticationController::class, 'getTokenData']); $routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies'; $routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]); diff --git a/src/Domain/User/Service/Authentication.php b/src/Domain/User/Service/Authentication.php index 4eb2c3bf..0d03aeb8 100644 --- a/src/Domain/User/Service/Authentication.php +++ b/src/Domain/User/Service/Authentication.php @@ -98,14 +98,19 @@ public function getCurrentUserId() : int return $userId; } - public function getToken() : ?string + public function getToken(Request $request) : ?string { - return $_COOKIE[self::AUTHENTICATION_COOKIE_NAME]; + $tokenInCookie = filter_input(INPUT_COOKIE, self::AUTHENTICATION_COOKIE_NAME); + if ($tokenInCookie !== false && $tokenInCookie !== null) { + return $tokenInCookie; + } + + return $request->getHeaders()['X-Auth-Token'] ?? null; } public function getUserIdByApiToken(Request $request) : ?int { - $apiToken = $request->getHeaders()['X-Auth-Token'] ?? filter_input(INPUT_COOKIE, self::AUTHENTICATION_COOKIE_NAME) ?? null; + $apiToken = $this->getToken($request); if ($apiToken === null) { return null; } @@ -117,7 +122,7 @@ public function isUserAuthenticatedWithCookie() : bool { $token = filter_input(INPUT_COOKIE, self::AUTHENTICATION_COOKIE_NAME); - if (empty($token) === false && $this->isValidToken((string)$token) === true) { + if (empty($token) === false && $this->isValidAuthToken((string)$token) === true) { return true; } @@ -159,7 +164,7 @@ public function isUserPageVisibleForCurrentUser(int $privacyLevel, int $userId) return $this->isUserAuthenticatedWithCookie() === true && $this->getCurrentUserId() === $userId; } - public function isValidToken(string $token) : bool + public function isValidAuthToken(string $token) : bool { $tokenExpirationDate = $this->repository->findAuthTokenExpirationDate($token); diff --git a/src/HttpController/Api/AuthenticationController.php b/src/HttpController/Api/AuthenticationController.php index f1687c0c..f2c76057 100644 --- a/src/HttpController/Api/AuthenticationController.php +++ b/src/HttpController/Api/AuthenticationController.php @@ -16,6 +16,7 @@ class AuthenticationController { public function __construct( private readonly Authentication $authenticationService, + private readonly UserApi $userApi, ) { } @@ -85,8 +86,12 @@ public function createToken(Request $request) : Response return Response::createJson( Json::encode([ - 'userId' => $userAndAuthToken['user']->getId(), - 'authToken' => $userAndAuthToken['token'] + 'authToken' => $userAndAuthToken['token'], + 'user' => [ + 'id' => $userAndAuthToken['user']->getId(), + 'name' => $userAndAuthToken['user']->getName(), + 'isAdmin' => $userAndAuthToken['user']->isAdmin(), + ] ]), ); } @@ -99,12 +104,12 @@ public function destroyToken(Request $request) : Response return Response::CreateNoContent(); } - $apiToken = $request->getHeaders()['X-Auth-Token'] ?? null; + $apiToken = $this->authenticationService->getToken($request); if ($apiToken === null) { return Response::createBadRequest( Json::encode([ 'error' => 'MissingAuthToken', - 'message' => 'Authentication token to delete in headers missing' + 'message' => 'Authentication token header is missing' ]), [Header::createContentTypeJson()], ); @@ -114,4 +119,37 @@ public function destroyToken(Request $request) : Response return Response::CreateNoContent(); } + + public function getTokenData(Request $request) : Response + { + $token = $this->authenticationService->getToken($request); + if ($token === null) { + return Response::createBadRequest( + Json::encode([ + 'error' => 'MissingAuthToken', + 'message' => 'Authentication token header is missing' + ]), + [Header::createContentTypeJson()], + ); + } + + $user = $this->userApi->findByToken($token); + if ($user === null) { + return Response::createUnauthorized(); + } + + if($this->authenticationService->isUserAuthenticatedWithCookie() && $this->authenticationService->isValidAuthToken($token) === false) { + return Response::createUnauthorized(); + } + + return Response::createJson( + Json::encode([ + 'user' => [ + 'id' => $user->getId(), + 'name' => $user->getName(), + 'isAdmin' => $user->isAdmin(), + ] + ]), + ); + } } diff --git a/tests/rest/api/authentication.assert.http b/tests/rest/api/authentication.assert.http index 96c5fcf4..d056dbc7 100644 --- a/tests/rest/api/authentication.assert.http +++ b/tests/rest/api/authentication.assert.http @@ -54,8 +54,10 @@ X-Movary-Client: RestAPI Test client.assert(response.status === expected, "Expected status code: " + expected); }); client.test("Response has correct body", function() { - client.assert(response.body.hasOwnProperty("'userId'") === false, "Response body missing property: userId"); - client.assert(response.body.hasOwnProperty("'authToken'") === false, "Response body missing property: authToken"); + client.assert(response.body.hasOwnProperty("authToken") === true, "Response body missing property: authToken"); + client.assert(response.body.user.hasOwnProperty("id") === true, "Response body missing property: user.id"); + client.assert(response.body.user.hasOwnProperty("name") === true, "Response body missing property: user.name"); + client.assert(response.body.user.hasOwnProperty("isAdmin") === true, "Response body missing property: user.isAdmin"); }); client.global.set("responseAuthToken", response.body.authToken); @@ -63,6 +65,26 @@ X-Movary-Client: RestAPI Test ### +GET http://127.0.0.1/api/authentication/token +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{responseAuthToken}} + +> {% + client.test("Response has correct status code", function() { + let expected = 200 + client.assert(response.status === expected, "Expected status code: " + expected); + }); + client.test("Response has correct body", function() { + client.assert(response.body.user.hasOwnProperty("id") === true, "Response body missing property: user.id"); + client.assert(response.body.user.hasOwnProperty("name") === true, "Response body missing property: user.name"); + client.assert(response.body.user.hasOwnProperty("isAdmin") === true, "Response body missing property: user.isAdmin"); + }); +%} + +### + DELETE http://127.0.0.1/api/authentication/token Accept: */* Cache-Control: no-cache @@ -78,6 +100,38 @@ X-Auth-Token: {{responseAuthToken}} ### +GET http://127.0.0.1/api/authentication/token +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{responseAuthToken}} + +> {% + client.test("Response has correct status code", function() { + let expected = 401 + client.assert(response.status === expected, "Expected status code: " + expected); + }); +%} + +### + +GET http://127.0.0.1/api/authentication/token +Accept: */* +Cache-Control: no-cache +Content-Type: application/json + +> {% + client.test("Response has correct status code", function() { + let expectedStatusCode = 400 + let expectedError = "MissingAuthToken"; + client.assert(response.status === expectedStatusCode, "Expected status code: " + expectedStatusCode); + client.assert(response.body.error === expectedError, "Expected error: " + expectedError); + client.assert(response.body.message === 'Authentication token header is missing'); + }); +%} + +### + DELETE http://127.0.0.1/api/authentication/token Accept: */* Cache-Control: no-cache @@ -89,7 +143,7 @@ Content-Type: application/json client.assert(response.status === expected, "Expected status code: " + expected); }); client.test("Response has correct body", function() { - let expected = '{"error":"MissingAuthToken","message":"Authentication token to delete in headers missing"}'; + let expected = '{"error":"MissingAuthToken","message":"Authentication token header is missing"}'; client.assert(JSON.stringify(response.body) === expected, "Expected response body: " + expected); }); %} diff --git a/tests/rest/api/authentication.http b/tests/rest/api/authentication.http index 9e2d2a5e..324ccbab 100644 --- a/tests/rest/api/authentication.http +++ b/tests/rest/api/authentication.http @@ -8,6 +8,14 @@ X-Movary-Client: RestAPI Test ### +GET http://127.0.0.1/api/authentication/token +Accept: */* +Cache-Control: no-cache +Content-Type: application/json +X-Auth-Token: {{xAuthToken}} + +### + DELETE http://127.0.0.1/api/authentication/token Accept: */* Cache-Control: no-cache