Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authentication check endpoint #582

Merged
merged 8 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 79 additions & 10 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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."
}
}
}
}
}
Expand All @@ -1156,6 +1229,7 @@
"tags": [
"Authentication"
],
"summary": "Delete authentication token",
"description": "Delete the authentication token provided in the X-Auth-Token header value.",
"parameters": [
{
Expand Down Expand Up @@ -1399,11 +1473,6 @@
"type": "apiKey",
"name": "X-Auth-Token",
"in": "header"
},
"cookie": {
"type": "apiKey",
"name": "id",
"in": "cookie"
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion settings/phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@
<rule ref="Generic.Commenting.Fixme"/>
<rule ref="Generic.ControlStructures.InlineControlStructure"/>
<rule ref="Generic.Files.ByteOrderMark"/>
<rule ref="Generic.Files.LineEndings"/>
<rule ref="Generic.Files.LineEndings">
<rule ref="Generic.Files.LineEndings">
<exclude name="Generic.Files.LineEndings.InvalidEOLChar"/>
</rule>
</rule>
leepeuker marked this conversation as resolved.
Show resolved Hide resolved
<rule ref="Generic.Formatting.DisallowMultipleStatements"/>
<rule ref="Generic.Formatting.NoSpaceAfterCast"/>
<rule ref="Generic.Functions.FunctionCallArgumentSpacing"/>
Expand Down
1 change: 1 addition & 0 deletions settings/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']);
JVT038 marked this conversation as resolved.
Show resolved Hide resolved

$routeUserHistory = '/users/{username:[a-zA-Z0-9]+}/history/movies';
$routes->add('GET', $routeUserHistory, [Api\HistoryController::class, 'getHistory'], [Api\Middleware\IsAuthorizedToReadUserData::class]);
Expand Down
15 changes: 10 additions & 5 deletions src/Domain/User/Service/Authentication.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,19 @@ public function getCurrentUserId() : int
return $userId;
}

public function getToken() : ?string
public function getToken(Request $request) : ?string
JVT038 marked this conversation as resolved.
Show resolved Hide resolved
{
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;
}
Expand All @@ -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;
}

Expand Down Expand Up @@ -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);

Expand Down
46 changes: 42 additions & 4 deletions src/HttpController/Api/AuthenticationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ class AuthenticationController
{
public function __construct(
private readonly Authentication $authenticationService,
private readonly UserApi $userApi,
) {
}

Expand Down Expand Up @@ -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' => [
JVT038 marked this conversation as resolved.
Show resolved Hide resolved
'id' => $userAndAuthToken['user']->getId(),
'name' => $userAndAuthToken['user']->getName(),
'isAdmin' => $userAndAuthToken['user']->isAdmin(),
]
]),
);
}
Expand All @@ -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()],
);
Expand All @@ -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(),
]
]),
);
}
}
60 changes: 57 additions & 3 deletions tests/rest/api/authentication.assert.http
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,37 @@ 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);
%}

###

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
Expand All @@ -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
Expand All @@ -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);
});
%}
Expand Down
8 changes: 8 additions & 0 deletions tests/rest/api/authentication.http
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading