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 5 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
39 changes: 39 additions & 0 deletions docs/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,45 @@
}
}
},
"/authentication/authenticated": {
"get": {
"tags": [
"Authentication"
],
"summary": "Checks the authentication status of the user.",
"description": "Checks if the user is authenticated based on cookie.. The response is a JSON object with the authentication status and an optional username + userId of the authenticated user",
"responses": {
"200": {
"description": "OK",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"authenticated": {
"type": "boolean",
"description": "True if authenticated, false if unauthenticated."
},
"username": {
"type": "string",
"description": "The username of the authenticated user. It's empty if the user is unauthenticated."
},
"userId": {
"type": "integer",
"description": "The ID of the authenticated user. It's empty if the user is unauthenticated."
},
"isAdmin": {
"type": "boolean",
"description": "True if the user is an admin, False if the user isn't."
}
}
}
}
}
}
}
}
},
"/authentication/token": {
"post": {
"tags": [
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
11 changes: 8 additions & 3 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 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
{
$apiToken = $request->getHeaders()['X-Auth-Token'] ?? null;
if ($apiToken === null) {
return Response::createBadRequest(
Json::encode([
'error' => 'MissingAuthToken',
'message' => 'Authentication token header is missing'
]),
[Header::createContentTypeJson()],
);
}

if ($this->authenticationService->isValidToken($apiToken) === false) {
return Response::createForbidden();
}

$user = $this->userApi->findByToken($apiToken);
if ($user === null) {
return Response::createForbidden();
}

return Response::createJson(
Json::encode([
'user' => [
'id' => $user->getId(),
'name' => $user->getName(),
'isAdmin' => $user->isAdmin(),
]
]),
);
}
}
43 changes: 40 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,21 @@ 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 = 403
client.assert(response.status === expected, "Expected status code: " + expected);
});
%}

###

DELETE http://127.0.0.1/api/authentication/token
Accept: */*
Cache-Control: no-cache
Expand All @@ -89,7 +126,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