Skip to content

Commit

Permalink
Merge pull request #582 from leepeuker/add-authentication-check-endpoint
Browse files Browse the repository at this point in the history
Add authentication check endpoint
  • Loading branch information
JVT038 authored Feb 27, 2024
2 parents 6c8f3dc + b9abb81 commit 4f5a2c6
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 23 deletions.
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>
<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']);

$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
{
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' => [
'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

0 comments on commit 4f5a2c6

Please sign in to comment.