diff --git a/.gitignore b/.gitignore
index 5a2d43a..1ab6d14 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
vendor
composer.lock
+/.idea
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..afc883e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,180 @@
+
+# Basic API Token Middleware for Laravel
+
+This middleware provides a simple, secure token-based authentication mechanism for Laravel, designed for internal machine-to-machine communication (e.g., internal apps accessing other internal APIs) without the complexity of OAuth.
+
+> ⚠️ **Security Note**: Always send API tokens via `Authorization: Bearer` headers. Do not use query parameters or POST body fields as they are insecure and unsupported.
+
+---
+
+## Installation
+
+Install the package via Composer:
+
+```bash
+composer require uogsoe/basic-api-token-middleware
+```
+
+Publish the database migration and model:
+
+```bash
+php artisan vendor:publish
+```
+
+Select:`UoGSoE\ApiTokenMiddleware\ApiTokenServiceProvider`
+
+Run the migration to create the `api_tokens` table:
+
+```bash
+php artisan migrate
+```
+
+---
+
+## Usage
+
+### Creating a Token
+
+Generate a token for a service (e.g., `testservice`):
+
+```bash
+php artisan apitoken:create testservice
+```
+
+> The token will only be displayed once. Store it securely.
+
+---
+
+### Protecting Routes
+
+In `routes/api.php`, apply the middleware:
+
+```php
+use Illuminate\Support\Facades\Route;
+
+Route::middleware('apitoken:testservice')->group(function () {
+ Route::get('/hello', fn() => response()->json(['message' => 'Hello, World!']));
+});
+```
+
+Multiple services:
+
+```php
+Route::middleware('apitoken:testservice,anotherservice')->group(function () {
+ Route::get('/hello', fn() => response()->json(['message' => 'Hello, World!']));
+});
+```
+
+---
+
+### Authenticating Requests
+
+Send requests using the Authorization header:
+
+```bash
+curl -H "Authorization: Bearer jT7ryt28gi3YCvgE4WvluO1uVcb0ndVx" https://my-project.test/api/hello
+```
+
+**Successful Response:**
+
+```json
+HTTP/2 200
+{"message": "Hello, World!"}
+```
+
+**Unauthorized Response:**
+
+```json
+HTTP/2 401
+{"message":"Unauthorized"}
+```
+
+**Laravel Example:**
+
+```php
+use Illuminate\Support\Facades\Http;
+Http::withHeaders([
+ 'Authorization' => 'Bearer jT7ryt28gi3YCvgE4WvluO1uVcb0ndVx',
+])->get('https://my-project.test/api/hello');
+```
+
+**AJAX Example:**
+
+```javascript
+fetch('https://my-project.test/api/hello', {
+ method: 'GET',
+ headers: {
+ 'Authorization': 'Bearer jT7ryt28gi3YCvgE4WvluO1uVcb0ndVx',
+ 'Accept': 'application/json'
+ }
+})
+.then(response => response.json())
+.then(data => console.log(data))
+.catch(error => console.error('Error:', error));
+```
+
+> ❗ Avoid sending tokens via query strings or POST bodies - will result in a 401 Unauthorized response.
+
+---
+
+## Managing Tokens
+
+- **List all tokens:**
+
+```bash
+php artisan apitoken:list
+```
+
+- **Regenerate a token:**
+
+```bash
+php artisan apitoken:regenerate testservice
+```
+
+- **Delete a token:**
+
+```bash
+php artisan apitoken:delete testservice
+```
+
+---
+
+## Security Best Practices
+
+- **Use HTTPS**: Encrypt all API traffic.
+- **Secure Token Storage**: Use environment variables, secret vaults, or HTTP-only secure cookies. Avoid client-side exposure.
+- **Token Expiry**: Use `apitoken:regenerate` periodically.
+- **CORS Configuration** (in `config/cors.php`):
+
+```php
+'allowed_origins' => ['https://your-frontend.com'],
+'supports_credentials' => true,
+```
+
+- **Rate Limiting**:
+
+```php
+Route::middleware('throttle:60,1')->get('/hello', fn() => response()->json(['message' => 'Hello, World!']));
+```
+
+- **XSS Protection**: Use Content Security Policy (CSP) and sanitize inputs.
+
+---
+
+## Upgrading from Previous Versions
+
+If you were using `?api_token=` in URLs or POST bodies, **update clients** to use `Authorization: Bearer` headers immediately. These methods are no longer supported.
+
+---
+
+## Contributing
+
+Contributions are welcome!
+Submit PRs at: [https://github.com/uogsoe/basic-api-token-middleware](https://github.com/uogsoe/basic-api-token-middleware)
+Ensure tests and security practices are followed.
+
+---
+
+## License
+
+This project is licensed under the **MIT License**.
diff --git a/Readme.md b/Readme.md
deleted file mode 100644
index 8039896..0000000
--- a/Readme.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# Basic API key middleware for Laravel
-
-This is a simple key-based middleware for Laravel. It suited our common use-case of internal apps which need access to other internal apps (machine-to-machine) without the hassles of oauth etc.
-
-## Installation
-
-You should be able to pull it in using composer :
-
-```
-composer require uogsoe/basic-api-token-middleware
-```
-
-Then you have to publish the database migration and ApiKey model :
-```
-php artisan vendor:publish
-```
-And pick `UoGSoE\ApiTokenMiddleware\ApiTokenServiceProvider` from the list. Then run the migration :
-```
-php artisan migrate
-```
-
-## Usage
-
-First of all you create a token for the consuming 'service' (eg, the remote client) :
-```
-php artisan apitoken:create testservice
-```
-That will create the token and show it to you. You need to take note of the token as your client will have to use it to access the routes.
-
-Now in your `routes/api.php` file you can use the middleware to wrap endpoints :
-```
-Route::group(['middleware' => 'apitoken:testservice'], function () {
- Route::get('/hello', function () {
- return 'hello';
- });
-});
-```
-
-If you try and access that route without passing the token you will get a 401 response :
-```
-curl -kv https://my-project.test/api/hello
-...
-HTTP/2 401
-{"message":"Unauthorized"}
-```
-So pass the token you created above and it should let you through :
-```
-curl -kv https://my-project.test/api/hello?api_token=jT7ryt28gi3YCvgE4WvluO1uVcb0ndVx
-...
-HTTP/2 200
-hello
-```
-
-You can pass the token in various ways, like a GET param as above, a bearer token header or as part of the JSON body. Eg:
-```
-$this->withHeaders([
- 'Authorization' => 'Bearer '.$tokenString,
-])->get('https://my-project.test/api/hello');
-
-$this->json('POST', 'https://my-project.test/api/hello', ['api_token' => $token]);
-
-$this->call('POST', 'https://my-project.test/api/hello', ['api_token' => $token]);
-```
-
-You can use multiple service token names with a route if you want to seperate your api controls too :
-```
-Route::group(['middleware' => 'apitoken:testservice,anotherservice'], function () {
- Route::get('/hello', function () {
- return 'hello';
- });
-});
-```
-
-There are a few other artisan commands available to help manage the tokens :
-```
-php artisan apitoken:list -- lists all current tokens
-php artisan apitoken:regenerate -- create a new token for a given service
-php artisan apitoken:delete -- deletes a given service token
-```
diff --git a/composer.json b/composer.json
index d346f35..825e76e 100644
--- a/composer.json
+++ b/composer.json
@@ -9,21 +9,20 @@
"email": "william.allan@glasgow.ac.uk"
}
],
+ "require": {},
+ "require-dev": {
+ "orchestra/testbench": "^10.0"
+ },
"autoload": {
"psr-4": {
- "UoGSoE\\ApiTokenMiddleware\\": "src"
+ "UoGSoE\\ApiTokenMiddleware\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
- "Tests\\": "tests",
- "App\\": "src"
+ "Tests\\": "tests/"
}
},
- "require": {},
- "require-dev": {
- "orchestra/testbench": "~3.0"
- },
"extra": {
"laravel": {
"providers": [
@@ -31,4 +30,4 @@
]
}
}
-}
+}
\ No newline at end of file
diff --git a/migrations/2018_04_18_090739_create_api_tokens_table.php b/migrations/2018_04_18_090739_create_api_tokens_table.php
index aa90e83..ab49627 100644
--- a/migrations/2018_04_18_090739_create_api_tokens_table.php
+++ b/migrations/2018_04_18_090739_create_api_tokens_table.php
@@ -14,7 +14,7 @@ class CreateApiTokensTable extends Migration
public function up()
{
Schema::create('api_tokens', function (Blueprint $table) {
- $table->increments('id');
+ $table->id();
$table->string('service');
$table->string('token');
$table->timestamps();
diff --git a/phpunit.xml b/phpunit.xml
index 08800d5..22afabf 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -1,5 +1,7 @@
-
+ stopOnFailure="false"
+ beStrictAboutTestsThatDoNotTestAnything="true">
+
./tests
-
-
+
+
+
+
./app
-
-
+ ./vendor/uogsoe/apitokenmiddleware/src
+
+
+
+
+
+
+
+
-
+
-
+
-
+
\ No newline at end of file
diff --git a/src/ApiToken.php b/src/ApiToken.php
index 0e15c68..abf8c00 100644
--- a/src/ApiToken.php
+++ b/src/ApiToken.php
@@ -1,32 +1,87 @@
+ */
+ protected $fillable = ['service', 'token'];
- public static function createNew($service)
+ /**
+ * The attributes that should be hidden for arrays and JSON responses.
+ *
+ * @var array
+ */
+ protected $hidden = ['token'];
+
+ /**
+ * Create a new API token for the given service.
+ *
+ * @param string $service The service name for the token.
+ * @return string The generated raw token (not hashed).
+ * @throws \InvalidArgumentException If the service name is empty.
+ */
+ public static function createNew(string $service): string
{
- $newToken = str_random(32);
+ // Validate the service name
+ if (empty($service)) {
+ throw new \InvalidArgumentException('Service name cannot be empty');
+ }
+
+ // Generate a random 32-character token
+ $newToken = Str::random(32);
+
+ // Create and save the token record with a hashed token
static::create([
'service' => $service,
- 'token' => bcrypt($newToken)
+ 'token' => Hash::make($newToken),
]);
+
+ // Return the raw (unhashed) token
return $newToken;
}
- public static function regenerate($service)
+ /**
+ * Regenerate an API token for the given service.
+ *
+ * @param string $service The service name for the token.
+ * @return string The regenerated raw token (not hashed).
+ * @throws \InvalidArgumentException If the service name is empty or no token exists for the service.
+ */
+ public static function regenerate(string $service): string
{
- $token = static::where('service', '=', $service)->first();
- if (! $token) {
- throw new \InvalidArgumentException('No such service');
+ // Validate the service name
+ if (empty($service)) {
+ throw new \InvalidArgumentException('Service name cannot be empty');
}
- $newToken = str_random(32);
- $token->token = bcrypt($newToken);
+
+ // Find the token for the given service
+ $token = static::where('service', $service)->first();
+
+ // Throw an exception if no token is found
+ if (!$token) {
+ throw new \InvalidArgumentException('No token found for service: ' . $service);
+ }
+
+ // Generate a new random 32-character token
+ $newToken = Str::random(32);
+
+ // Update the token with the new hashed value
+ $token->token = Hash::make($newToken);
$token->save();
+
+ // Return the raw (unhashed) token
return $newToken;
}
}
diff --git a/src/ApiTokenServiceProvider.php b/src/ApiTokenServiceProvider.php
index 9442d90..90ee6fc 100644
--- a/src/ApiTokenServiceProvider.php
+++ b/src/ApiTokenServiceProvider.php
@@ -3,24 +3,57 @@
namespace UoGSoE\ApiTokenMiddleware;
use Illuminate\Support\ServiceProvider;
+use Illuminate\Routing\Router;
+use UoGSoE\ApiTokenMiddleware\Commands\ListTokens;
+use UoGSoE\ApiTokenMiddleware\Commands\CreateToken;
+use UoGSoE\ApiTokenMiddleware\Commands\DeleteToken;
+use UoGSoE\ApiTokenMiddleware\Commands\RegenerateToken;
+/**
+ * Service provider for the ApiTokenMiddleware package.
+ */
class ApiTokenServiceProvider extends ServiceProvider
{
- public function boot(\Illuminate\Routing\Router $router)
+ /**
+ * Bootstrap any application services.
+ *
+ * @param Router $router The Laravel router instance for registering middleware.
+ * @return void
+ */
+ public function boot(Router $router): void
{
- $this->loadMigrationsFrom(__DIR__.'/../migrations');
- $router->aliasMiddleware('apitoken', 'UoGSoE\ApiTokenMiddleware\BasicApiTokenMiddleware');
+ // Publish the ApiToken model to the app/Models directory to align with Laravel 12's default model namespace
+ $this->publishes([
+ __DIR__ . '/ApiToken.php' => app_path('Models/ApiToken.php'),
+ ]);
+
+ // Publish the migration file to create the api_tokens table
+ $this->publishes([
+ __DIR__ . '/../migrations/2018_04_18_090739_create_api_tokens_table.php' =>
+ database_path('migrations/2018_04_18_090739_create_api_tokens_table.php'),
+ ]);
+
+ // Register the 'apitoken' middleware alias for use in routes
+ $router->aliasMiddleware('apitoken', BasicApiTokenMiddleware::class);
+
+ // Register console commands, but only if running in the console environment
if ($this->app->runningInConsole()) {
$this->commands([
- Commands\ListTokens::class,
- Commands\CreateToken::class,
- Commands\DeleteToken::class,
- Commands\RegenerateToken::class,
+ ListTokens::class,
+ CreateToken::class,
+ DeleteToken::class,
+ RegenerateToken::class,
]);
}
}
- public function register()
+ /**
+ * Register any application services.
+ *
+ * @return void
+ */
+ public function register(): void
{
+ // Empty for now, but can be used to bind services or configurations in the future
}
}
diff --git a/src/BasicApiTokenMiddleware.php b/src/BasicApiTokenMiddleware.php
index 2dda966..4db9861 100644
--- a/src/BasicApiTokenMiddleware.php
+++ b/src/BasicApiTokenMiddleware.php
@@ -2,68 +2,73 @@
namespace UoGSoE\ApiTokenMiddleware;
+use App\Models\ApiToken;
use Closure;
-use UoGSoE\ApiTokenMiddleware\ApiToken;
+use Illuminate\Http\JsonResponse;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Hash;
+/**
+ * Middleware to authenticate API requests using Bearer tokens.
+ */
class BasicApiTokenMiddleware
{
- const CODE = 401;
- const MESSAGE = 'Unauthorized';
+ public const CODE = 401;
+ public const MESSAGE = 'Unauthorized';
/**
* Handle an incoming request.
*
- * @param \Illuminate\Http\Request $request
- * @param \Closure $next
+ * @param Request $request
+ * @param Closure $next
+ * @param string ...$services
* @return mixed
*/
- public function handle($request, Closure $next)
+ public function handle(Request $request, Closure $next, string ...$services): mixed
{
- $services = array_except(func_get_args(), [0,1]);
if (!$this->authorized($request, $services)) {
return response()->json(['message' => self::MESSAGE], self::CODE);
}
+
return $next($request);
}
/**
- * Checks an incoming token against one in the database
+ * Check if the request is authorized based on the provided token and services.
*
- * @param \Illuminate\Http\Request $request
- * @param array $service
+ * @param Request $request
+ * @param array $services
+ * @return bool
*/
- public function authorized($request, $services)
+ protected function authorized(Request $request, array $services): bool
{
$passedToken = $this->extractToken($request);
- if (! $passedToken) {
+
+ if (!$passedToken || empty($services)) {
return false;
}
- $apiTokens = ApiToken::whereIn('service', $services)->get();
- if ($apiTokens->isEmpty()) {
- return false;
+ foreach ($services as $service) {
+ $apiToken = ApiToken::where('service', $service)
+ ->whereNotNull('token')
+ ->first();
+
+ if ($apiToken && Hash::check($passedToken, $apiToken->token)) {
+ return true;
+ }
}
- return !is_null($apiTokens->first(function ($apiToken) use ($passedToken) {
- return \Hash::check($passedToken, $apiToken->token);
- }));
+ return false;
}
/**
- * Try to find the api token in the request
+ * Extract the API token from the request's Authorization header.
*
- * @param \Illuminate\Http\Request $request
+ * @param Request $request
+ * @return string|null
*/
- public function extractToken($request)
+ protected function extractToken(Request $request): ?string
{
- if ($request->bearerToken()) {
- return $request->bearerToken();
- }
-
- if ($request->input('api_token')) {
- return $request->input('api_token');
- }
-
- return null;
+ return $request->bearerToken();
}
-}
+}
\ No newline at end of file
diff --git a/src/Commands/CreateToken.php b/src/Commands/CreateToken.php
index 117226c..2c453aa 100644
--- a/src/Commands/CreateToken.php
+++ b/src/Commands/CreateToken.php
@@ -2,9 +2,12 @@
namespace UoGSoE\ApiTokenMiddleware\Commands;
+use App\Models\ApiToken;
use Illuminate\Console\Command;
-use UoGSoE\ApiTokenMiddleware\ApiToken;
+/**
+ * Console command to create a new API token for a specified service.
+ */
class CreateToken extends Command
{
/**
@@ -12,19 +15,17 @@ class CreateToken extends Command
*
* @var string
*/
- protected $signature = 'apitoken:create {service}';
+ protected $signature = 'apitoken:create {service : The service name for the API token}';
/**
* The console command description.
*
* @var string
*/
- protected $description = 'Create a new API token';
+ protected $description = 'Create a new API token for a specified service';
/**
* Create a new command instance.
- *
- * @return void
*/
public function __construct()
{
@@ -34,19 +35,42 @@ public function __construct()
/**
* Execute the console command.
*
- * @return mixed
+ * @return int Exit code (0 for success, 1 for failure).
*/
- public function handle()
+ public function handle(): int
{
+ // Get the service name from the command argument
$service = $this->argument('service');
- $token = ApiToken::where('service', '=', $service)->first();
- if ($token) {
- $this->error('That service name is already used');
- exit;
+
+ // Check if a token already exists for the service
+ if (ApiToken::where('service', $service)->exists()) {
+ $this->error("A token for service '$service' already exists.");
+ return 1;
}
- $token = ApiToken::createNew($service);
- $this->info("Token created :");
- $this->table(['Service', 'Token'], [['service' => $service, 'token' => $token]]);
+ try {
+ // Create a new token using the ApiToken model's createNew method
+ $token = ApiToken::createNew($service);
+
+ // Display the created token in a table
+ $this->info('Token created successfully:');
+ $this->table(
+ ['Service', 'Token'],
+ [[$service, $token]]
+ );
+
+ // Provide additional usage instructions
+ $this->comment('Use this token in API requests via Authorization: Bearer .');
+
+ return 0; // Success
+ } catch (\InvalidArgumentException $e) {
+ // Handle validation errors from createNew (e.g., empty service)
+ $this->error('Failed to create token: ' . $e->getMessage());
+ return 1;
+ } catch (\Exception $e) {
+ // Handle unexpected errors (e.g., database issues)
+ $this->error('An unexpected error occurred: ' . $e->getMessage());
+ return 1;
+ }
}
}
diff --git a/src/Commands/DeleteToken.php b/src/Commands/DeleteToken.php
index 0eca6b5..a0a88f7 100644
--- a/src/Commands/DeleteToken.php
+++ b/src/Commands/DeleteToken.php
@@ -2,9 +2,12 @@
namespace UoGSoE\ApiTokenMiddleware\Commands;
+use App\Models\ApiToken;
use Illuminate\Console\Command;
-use UoGSoE\ApiTokenMiddleware\ApiToken;
+/**
+ * Console command to delete an API token for a specified service.
+ */
class DeleteToken extends Command
{
/**
@@ -12,19 +15,17 @@ class DeleteToken extends Command
*
* @var string
*/
- protected $signature = 'apitoken:delete {service}';
+ protected $signature = 'apitoken:delete {service : The service name of the API token to delete}';
/**
* The console command description.
*
* @var string
*/
- protected $description = 'Remove an API token';
+ protected $description = 'Delete an API token for a specified service';
/**
* Create a new command instance.
- *
- * @return void
*/
public function __construct()
{
@@ -34,17 +35,36 @@ public function __construct()
/**
* Execute the console command.
*
- * @return mixed
+ * @return int Exit code (0 for success, 1 for failure).
*/
- public function handle()
+ public function handle(): int
{
+ // Get the service name from the command argument
$service = $this->argument('service');
- $token = ApiToken::where('service', '=', $service)->first();
- if (! $token) {
- $this->error('No such service');
- exit;
+
+ // Validate the service name
+ if (empty($service)) {
+ $this->error('Service name cannot be empty.');
+ return 1;
+ }
+
+ try {
+ // Attempt to find and delete the token for the given service
+ $deleted = ApiToken::where('service', $service)->delete();
+
+ // Check if a token was deleted
+ if ($deleted === 0) {
+ $this->error("No token found for service '$service'.");
+ return 1;
+ }
+
+ // Confirm successful deletion
+ $this->info("Token for service '$service' deleted successfully.");
+ return 0; // Success
+ } catch (\Exception $e) {
+ // Handle unexpected errors (e.g., database issues)
+ $this->error('Failed to delete token: ' . $e->getMessage());
+ return 1;
}
- $token->delete();
- $this->info("Token for {$service} removed");
}
}
diff --git a/src/Commands/ListTokens.php b/src/Commands/ListTokens.php
index 2c9e9d5..07aa52f 100644
--- a/src/Commands/ListTokens.php
+++ b/src/Commands/ListTokens.php
@@ -2,9 +2,12 @@
namespace UoGSoE\ApiTokenMiddleware\Commands;
-use UoGSoE\ApiTokenMiddleware\ApiToken;
+use App\Models\ApiToken;
use Illuminate\Console\Command;
+/**
+ * Console command to list all API tokens.
+ */
class ListTokens extends Command
{
/**
@@ -19,12 +22,10 @@ class ListTokens extends Command
*
* @var string
*/
- protected $description = 'List current API tokens';
+ protected $description = 'List all current API tokens';
/**
* Create a new command instance.
- *
- * @return void
*/
public function __construct()
{
@@ -34,10 +35,37 @@ public function __construct()
/**
* Execute the console command.
*
- * @return mixed
+ * @return int Exit code (0 for success, 1 for failure).
*/
- public function handle()
+ public function handle(): int
{
- $this->table(['Service', 'Hashed Token'], ApiToken::all(['service', 'token']));
+ try {
+ // Retrieve all API tokens, selecting only the service and created_at fields
+ $tokens = ApiToken::select('service', 'created_at')->get();
+
+ // Check if any tokens exist
+ if ($tokens->isEmpty()) {
+ $this->info('No API tokens found.');
+ return 0; // Success, but no data
+ }
+
+ // Display the tokens in a table (excluding token field due to $hidden)
+ $this->info('Current API tokens:');
+ $this->table(
+ ['Service', 'Created At'],
+ $tokens->map(function ($token) {
+ return [
+ $token->service,
+ $token->created_at->toDateTimeString(),
+ ];
+ })
+ );
+
+ return 0; // Success
+ } catch (\Exception $e) {
+ // Handle unexpected errors (e.g., database issues)
+ $this->error('Failed to list tokens: ' . $e->getMessage());
+ return 1; // Failure
+ }
}
}
diff --git a/src/Commands/RegenerateToken.php b/src/Commands/RegenerateToken.php
index c77fac3..f77d22d 100644
--- a/src/Commands/RegenerateToken.php
+++ b/src/Commands/RegenerateToken.php
@@ -2,9 +2,12 @@
namespace UoGSoE\ApiTokenMiddleware\Commands;
+use App\Models\ApiToken;
use Illuminate\Console\Command;
-use UoGSoE\ApiTokenMiddleware\ApiToken;
+/**
+ * Console command to regenerate an API token for a specified service.
+ */
class RegenerateToken extends Command
{
/**
@@ -12,19 +15,17 @@ class RegenerateToken extends Command
*
* @var string
*/
- protected $signature = 'apitoken:regenerate {service}';
+ protected $signature = 'apitoken:regenerate {service : The service name of the API token to regenerate}';
/**
* The console command description.
*
* @var string
*/
- protected $description = 'Regenerate an API token';
+ protected $description = 'Regenerate an API token for a specified service';
/**
* Create a new command instance.
- *
- * @return void
*/
public function __construct()
{
@@ -34,19 +35,36 @@ public function __construct()
/**
* Execute the console command.
*
- * @return mixed
+ * @return int Exit code (0 for success, 1 for failure).
*/
- public function handle()
+ public function handle(): int
{
+ // Get the service name from the command argument
$service = $this->argument('service');
- $token = ApiToken::where('service', '=', $service)->first();
- if (!$token) {
- $this->error('No such service');
- exit;
- }
- $token = ApiToken::regenerate($service);
- $this->info("Token regenerated :");
- $this->table(['Service', 'Token'], [['service' => $service, 'token' => $token]]);
+ try {
+ // Regenerate the token using the ApiToken model's regenerate method
+ $token = ApiToken::regenerate($service);
+
+ // Display the regenerated token in a table
+ $this->info('Token regenerated successfully:');
+ $this->table(
+ ['Service', 'Token'],
+ [[$service, $token]]
+ );
+
+ // Provide additional usage instructions
+ $this->comment('Use this token in API requests via Authorization: Bearer .');
+
+ return 0; // Success
+ } catch (\InvalidArgumentException $e) {
+ // Handle validation errors from regenerate (e.g., empty or non-existent service)
+ $this->error('Failed to regenerate token: ' . $e->getMessage());
+ return 1;
+ } catch (\Exception $e) {
+ // Handle unexpected errors (e.g., database issues)
+ $this->error('An unexpected error occurred: ' . $e->getMessage());
+ return 1;
+ }
}
}
diff --git a/tests/ArtisanTest.php b/tests/ArtisanTest.php
index 740a006..f971fb9 100644
--- a/tests/ArtisanTest.php
+++ b/tests/ArtisanTest.php
@@ -2,79 +2,137 @@
namespace Tests;
-use App\ApiToken;
-
+use App\Models\ApiToken;
+use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\Hash;
+use Tests\TestCase;
+
+/**
+ * Test case for API token Artisan commands and model functionality.
+ */
class ArtisanTest extends TestCase
{
- /** @test */
- public function creating_a_new_token_stores_a_hashed_token_in_the_db()
+ /**
+ * Test that creating a new token stores a hashed token in the database.
+ *
+ * @return void
+ */
+ public function test_creating_a_new_token_stores_a_hashed_token_in_the_db(): void
{
+ // Create a new token for the 'test' service
$token = ApiToken::createNew('test');
+ // Retrieve the first token from the database
$dbToken = ApiToken::first();
+ // Assert that the raw token is not stored directly (it's hashed)
$this->assertNotEquals($token, $dbToken->token);
- $this->assertTrue(\Hash::check($token, $dbToken->token));
+ // Assert that the raw token matches the hashed token in the database
+ $this->assertTrue(Hash::check($token, $dbToken->token));
}
- /** @test */
- public function we_can_generate_a_new_hashed_token_for_an_existing_token()
+ /**
+ * Test that regenerating a token creates a new hashed token.
+ *
+ * @return void
+ */
+ public function test_we_can_generate_a_new_hashed_token_for_an_existing_token(): void
{
+ // Create an initial token
$token = ApiToken::createNew('test');
+ // Regenerate the token for the same service
$newToken = ApiToken::regenerate('test');
+ // Retrieve the updated token from the database
$dbToken = ApiToken::first();
+
+ // Assert that the new token is different from the original
$this->assertNotEquals($token, $newToken);
- $this->assertTrue(\Hash::check($newToken, $dbToken->token));
+ // Assert that the new token matches the hashed token in the database
+ $this->assertTrue(Hash::check($newToken, $dbToken->token));
}
- /** @test */
- public function we_can_call_artisan_to_create_a_new_token()
+ /**
+ * Test that the Artisan command creates a new token.
+ *
+ * @return void
+ */
+ public function test_we_can_call_artisan_to_create_a_new_token(): void
{
+ // Assert that no tokens exist initially
$this->assertCount(0, ApiToken::all());
+ // Run the Artisan command to create a token
$this->artisan('apitoken:create', ['service' => 'test']);
+ // Assert that one token now exists
$this->assertCount(1, ApiToken::all());
+ // Assert that the database has the token for the 'test' service
$this->assertDatabaseHas('api_tokens', ['service' => 'test']);
}
- /** @test */
- public function we_can_call_artisan_to_delete_a_token()
+ /**
+ * Test that the Artisan command deletes a token.
+ *
+ * @return void
+ */
+ public function test_we_can_call_artisan_to_delete_a_token(): void
{
- $token1 = ApiToken::createNew('test1');
- $token2 = ApiToken::createNew('test2');
+ // Create two tokens
+ ApiToken::createNew('test1');
+ ApiToken::createNew('test2');
+ // Run the Artisan command to delete one token
$this->artisan('apitoken:delete', ['service' => 'test1']);
+ // Assert that only one token remains
$this->assertCount(1, ApiToken::all());
+ // Assert that the remaining token is for 'test2'
$this->assertDatabaseHas('api_tokens', ['service' => 'test2']);
}
- /** @test */
- public function we_can_call_artisan_to_list_all_tokens()
+ /**
+ * Test that the Artisan command lists all tokens.
+ *
+ * @return void
+ */
+ public function test_we_can_call_artisan_to_list_all_tokens(): void
{
- $token1 = ApiToken::createNew('test1');
- $token2 = ApiToken::createNew('test2');
+ // Create two tokens
+ ApiToken::createNew('test1');
+ ApiToken::createNew('test2');
+ // Run the Artisan command to list tokens
$this->artisan('apitoken:list');
- $output = \Artisan::output();
- $this->assertContains('test1', $output);
- $this->assertContains('test2', $output);
+ // Get the command output
+ $output = Artisan::output();
+
+ // Assert that the output contains the service names
+ $this->assertStringContainsString('test1', $output);
+ $this->assertStringContainsString('test2', $output);
}
- /** @test */
- public function we_can_call_artisan_to_regenerate_a_token()
+ /**
+ * Test that the Artisan command regenerates a token.
+ *
+ * @return void
+ */
+ public function test_we_can_call_artisan_to_regenerate_a_token(): void
{
+ // Create an initial token
$token = ApiToken::createNew('test');
$dbToken = ApiToken::first();
- $this->assertTrue(\Hash::check($token, $dbToken->token));
+ // Verify the initial token is valid
+ $this->assertTrue(Hash::check($token, $dbToken->token));
+ // Run the Artisan command to regenerate the token
$this->artisan('apitoken:regenerate', ['service' => 'test']);
+ // Retrieve the updated token from the database
$dbToken = ApiToken::first();
- $this->assertFalse(\Hash::check($token, $dbToken->token));
+ // Assert that the original token is no longer valid
+ $this->assertFalse(Hash::check($token, $dbToken->token));
}
-}
+}
\ No newline at end of file
diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php
index 29510df..25f41b8 100644
--- a/tests/MiddlewareTest.php
+++ b/tests/MiddlewareTest.php
@@ -2,128 +2,191 @@
namespace Tests;
+use App\Models\ApiToken;
+use Illuminate\Support\Facades\Route;
+use Tests\TestCase;
+
+/**
+ * Test case for API token middleware functionality.
+ */
class MiddlewareTest extends TestCase
{
- /** @test */
- public function using_an_invalid_token_returns_unauthorised()
+ /**
+ * Test that an invalid token returns a 401 Unauthorized response.
+ *
+ * @return void
+ */
+ public function test_using_an_invalid_token_returns_unauthorised(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken:test')->any('/_test/', function () {
- return 'OK';
- });
+ // Create a valid token for the 'test' service
+ $token = ApiToken::createNew('test');
+ // Register a test route with the apitoken middleware
+ Route::middleware('apitoken:test')->any('/_test', fn() => 'OK');
- $response = $this->call('GET', '_test', ['api_token' => 'invalidtoken']);
+ // Send a request with an invalid token
+ $response = $this->call('GET', '/_test', ['api_token' => 'invalidtoken']);
+ // Assert that the response is 401 Unauthorized
$response->assertStatus(401);
}
- /** @test */
- public function using_no_token_returns_unauthorised()
+ /**
+ * Test that no token returns a 401 Unauthorized response.
+ *
+ * @return void
+ */
+ public function test_using_no_token_returns_unauthorised(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken:test')->any('/_test/', function () {
- return 'OK';
- });
+ // Create a token (not used in the request)
+ $token = ApiToken::createNew('test');
+ // Register a test route with the apitoken middleware
+ Route::middleware('apitoken:test')->any('/_test', fn() => 'OK');
- $response = $this->call('GET', '_test');
+ // Send a request without a token
+ $response = $this->call('GET', '/_test');
+ // Assert that the response is 401 Unauthorized
$response->assertStatus(401);
}
- /** @test */
- public function using_a_valid_token_as_a_url_param_returns_ok()
+ /**
+ * Test that a valid token in a URL parameter returns a 200 OK response.
+ *
+ * @return void
+ */
+ public function test_using_a_valid_token_as_a_url_param_returns_ok(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken:test')->any('/_test/', function () {
- return 'OK';
- });
+ // Create a valid token for the 'test' service
+ $token = ApiToken::createNew('test');
+ // Register a test route with the apitoken middleware
+ Route::middleware('apitoken:test')->any('/_test', fn() => 'OK');
- $response = $this->call('GET', '_test', ['api_token' => $token]);
+ // Send a request with the valid token as a URL parameter
+ $response = $this->call('GET', '/_test', ['api_token' => $token]);
+ // Assert that the response is 200 OK
$response->assertStatus(200);
}
- /** @test */
- public function using_a_valid_token_as_a_json_field_returns_ok()
+ /**
+ * Test that a valid token in a JSON payload returns a 200 OK response.
+ *
+ * @return void
+ */
+ public function test_using_a_valid_token_as_a_json_field_returns_ok(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken:test')->any('/_test/', function () {
- return 'OK';
- });
- $response = $this->json('GET', '_test', ['api_token' => $token]);
+ // Create a valid token for the 'test' service
+ $token = ApiToken::createNew('test');
+ // Register a test route with the apitoken middleware
+ Route::middleware('apitoken:test')->any('/_test', fn() => 'OK');
+
+ // Send a JSON request with the valid token
+ $response = $this->json('GET', '/_test', ['api_token' => $token]);
+ // Assert that the response is 200 OK
$response->assertStatus(200);
}
- /** @test */
- public function using_a_valid_token_as_a_form_field_returns_ok()
+ /**
+ * Test that a valid token in a form field returns a 200 OK response.
+ *
+ * @return void
+ */
+ public function test_using_a_valid_token_as_a_form_field_returns_ok(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken:test')->any('/_test/', function () {
- return 'OK';
- });
- $response = $this->call('POST', '_test', ['api_token' => $token]);
+ // Create a valid token for the 'test' service
+ $token = ApiToken::createNew('test');
+ // Register a test route with the apitoken middleware
+ Route::middleware('apitoken:test')->any('/_test', fn() => 'OK');
+
+ // Send a POST request with the valid token as a form field
+ $response = $this->call('POST', '/_test', ['api_token' => $token]);
+ // Assert that the response is 200 OK
$response->assertStatus(200);
}
- /** @test */
- public function using_a_valid_token_as_a_bearer_token_returns_ok()
+ /**
+ * Test that a valid token as a Bearer token returns a 200 OK response.
+ *
+ * @return void
+ */
+ public function test_using_a_valid_token_as_a_bearer_token_returns_ok(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken:test')->any('/_test/', function () {
- return 'OK';
- });
+ // Create a valid token for the 'test' service
+ $token = ApiToken::createNew('test');
+ // Register a test route with the apitoken middleware
+ Route::middleware('apitoken:test')->any('/_test', fn() => 'OK');
- $response = $this->withHeaders(['Authorization' => 'Bearer '.$token])->get('_test');
+ // Send a request with the valid token as a Bearer token
+ $response = $this->withHeaders(['Authorization' => "Bearer {$token}"])->get('/_test');
+ // Assert that the response is 200 OK
$response->assertStatus(200);
}
- /** @test */
- public function we_can_use_multiple_api_service_tokens()
+ /**
+ * Test that multiple service tokens can be used, and invalid ones are rejected.
+ *
+ * @return void
+ */
+ public function test_we_can_use_multiple_api_service_tokens(): void
{
- $this->withoutExceptionHandling();
- $token1 = \App\ApiToken::createNew('test1');
- $token2 = \App\ApiToken::createNew('test2');
- $token3 = \App\ApiToken::createNew('test3');
- \Route::middleware('apitoken:test1,test2')->any('/_test/', function () {
- return 'OK';
- });
-
- $response = $this->call('GET', '_test', ['api_token' => $token1]);
+ // Create tokens for multiple services
+ $token1 = ApiToken::createNew('test1');
+ $token2 = ApiToken::createNew('test2');
+ $token3 = ApiToken::createNew('test3');
+ // Register a test route allowing test1 and test2 services
+ Route::middleware('apitoken:test1,test2')->any('/_test', fn() => 'OK');
+
+ // Test with token1 (valid)
+ $response = $this->call('GET', '/_test', ['api_token' => $token1]);
$response->assertStatus(200);
- $response = $this->call('GET', '_test', ['api_token' => $token2]);
+ // Test with token2 (valid)
+ $response = $this->call('GET', '/_test', ['api_token' => $token2]);
$response->assertStatus(200);
- $response = $this->call('GET', '_test', ['api_token' => $token3]);
+ // Test with token3 (invalid for this route)
+ $response = $this->call('GET', '/_test', ['api_token' => $token3]);
$response->assertStatus(401);
}
- /** @test */
- public function using_a_non_existant_service_name_always_returns_unauthorised()
+ /**
+ * Test that a non-existent service name returns a 401 Unauthorized response.
+ *
+ * @return void
+ */
+ public function test_using_a_non_existant_service_name_always_returns_unauthorised(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken:nottest')->any('/_test/', function () {
- return 'OK';
- });
+ // Create a token for a valid service
+ $token = ApiToken::createNew('test');
+ // Register a test route with a non-existent service
+ Route::middleware('apitoken:nottest')->any('/_test', fn() => 'OK');
- $response = $this->call('GET', '_test', ['api_token' => $token]);
+ // Send a request with the token
+ $response = $this->call('GET', '/_test', ['api_token' => $token]);
+ // Assert that the response is 401 Unauthorized
$response->assertStatus(401);
}
- /** @test */
- public function using_no_service_name_always_returns_unauthorised()
+ /**
+ * Test that no service name returns a 401 Unauthorized response.
+ *
+ * @return void
+ */
+ public function test_using_no_service_name_always_returns_unauthorised(): void
{
- $token = \App\ApiToken::createNew('test');
- \Route::middleware('apitoken')->any('/_test/', function () {
- return 'OK';
- });
+ // Create a token for a valid service
+ $token = ApiToken::createNew('test');
+ // Register a test route with no service specified
+ Route::middleware('apitoken')->any('/_test', fn() => 'OK');
- $response = $this->call('GET', '_test', ['api_token' => $token]);
+ // Send a request with the token
+ $response = $this->call('GET', '/_test', ['api_token' => $token]);
+ // Assert that the response is 401 Unauthorized
$response->assertStatus(401);
}
-}
+}
\ No newline at end of file
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 7672dc8..a4f87f3 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -2,24 +2,60 @@
namespace Tests;
-class TestCase extends \Orchestra\Testbench\TestCase
+use Orchestra\Testbench\TestCase as OrchestraTestCase;
+
+/**
+ * Base test case for package testing, setting up migrations and service providers.
+ */
+class TestCase extends OrchestraTestCase
{
+ /**
+ * Set up the test environment.
+ *
+ * @return void
+ */
protected function setUp(): void
{
parent::setUp();
- $this->loadMigrationsFrom(realpath(__DIR__.'/../migrations'));
- $this->artisan('migrate', ['--database' => 'testing']);
+
+ // Load migrations from the package's migrations directory
+ $migrationPath = realpath(__DIR__ . '/../migrations');
+ if ($migrationPath === false) {
+ $this->fail('Migration directory not found at ' . __DIR__ . '/../migrations');
+ }
+ $this->loadMigrationsFrom($migrationPath);
+
+ // Run migrations for the testing database
+ $this->artisan('migrate', ['--database' => 'testing'])->run();
}
- protected function getPackageProviders($app)
+ /**
+ * Define package service providers for testing.
+ *
+ * @param \Illuminate\Foundation\Application $app
+ * @return array
+ */
+ protected function getPackageProviders($app): array
{
return [
- 'UoGSoE\ApiTokenMiddleware\ApiTokenServiceProvider',
+ \UoGSoE\ApiTokenMiddleware\ApiTokenServiceProvider::class,
];
}
- // protected function resolveApplicationConsoleKernel($app)
- // {
- // $app->singleton('Illuminate\Contracts\Console\Kernel', 'Acme\Testbench\Console\Kernel');
- // }
-}
+ /**
+ * Define environment setup.
+ *
+ * @param \Illuminate\Foundation\Application $app
+ * @return void
+ */
+ protected function getEnvironmentSetUp($app): void
+ {
+ // Configure an in-memory SQLite database for testing
+ $app['config']->set('database.default', 'testing');
+ $app['config']->set('database.connections.testing', [
+ 'driver' => 'sqlite',
+ 'database' => ':memory:',
+ 'prefix' => '',
+ ]);
+ }
+}
\ No newline at end of file