AuthSodium is a Laravel package for authenticating API requests with LibSodium's public-key (ED25519) signatures. Sodium is available natively in PHP since version 7.2 without an extension. Some objectives of the package:
- use modern security standards
- provide stateless RESTful API asymmetric authentication
- remove complexity of clients having to request/manage/renew tokens
- remove the need for sending sensitive credentials (such as passwords) to the server
- offload memory/cpu intensive work (such as slow password hashing) from the server to the client
- manage nonces transparently and automatically
- throttle and block malicious users
- highly customizable
- non-invasive
- no dependencies (other than Laravel itself)
- thoroughly tested
- fast and lightweight
- support Laravel 7+
The traditional model is for the client to send the user's password to 'log in', which is then hashed on the server (which is computationally expensive) and compared with a previously-saved hash. If they match, then a token (with an expiration date) is stored on the server and issued to the client. For future requests, the client provides the token. The issue with this is that the client must then store the token, send it with each request, renew it if it has expired, or request a new one if it is rejected. Furthermore, tokens are susceptible to misuse if they fall into the hands of an attacker, as they may be used to authenticate any number of requests.
Put simply, each request must contain all of the information necessary to be understood by the server, and to authenticate the user making the request, rather than being dependent on the server 'remembering' previously successful authentications from the user.
The package works by verifying the signature sent with each authenticated request. Your user model should contain a public key for each user.
How you choose to manage the user's public and private keys is up to you, however, the following is potential workflow for user registration:
- choose appropriate LibSodium bindings for your client
- user provides email and chooses a password
- input password into a PBKDF (several options are provided by LibSodium) to produce cryptographically secure pseudo-random bytes
- input bytes into LibSodium's crypto_sign_seed_keypair function to deterministically produce the user's public and private keys
- send the user's email address and public key to the server to be registered, being sure to sign the request with the user's private key, to ensure that the public key is valid
Each request must contain headers with the following metadata:
- a unique identifier for the user to be authenticated (for example: an email address)
- a nonce
- a timestamp
- a signature
The signature, which is generated by the client, is calculated by signing data related to each request. This data is comprised of:
- the HTTP request method (
get
,put
,post
, ordelete
) - the fully qualified URL of the request (with the
http://
orhttps://www
), but without any query strings - query data (sorted alphabetically and json-encoded), or an empty string in the absense of query data
- post data (json-encoded), or an empty string in the absense of post data
- user identifier (such as the user's email address)
- timestamp (in milliseconds, unless using a 32-bit version of PHP, in which case it should be in seconds) since midnight January 1st 1970 (UTC)
- nonce (a number or string which should not be repeated)
This data is then concatenated to make a single string, which is then signed with the user's private key. The signature is then sent as a header.
On the server side, this string is reconstructed. Once the auth user has been retrieved, their public key is used to verify the signature. If the signature fails then the appropriate error response is returned, otherwise, the request continues as per normal.
- Require this package with composer.
composer require rotgp/auth-sodium
- Publish the AuthSodium config file to your project with the following command:
php artisan vendor:publish --provider="ROTGP\AuthSodium\AuthSodiumServiceProvider" --tag="config"
-
Configure your user model
-
Review the database schema options
-
Run
php artisan migrate
Assuming you've performed the above installation, the
config file can then be found at
config/authsodium.php
.
The only thing you're required to tell AuthSodium
about is the class of your user model. This must extend
Illuminate\Database\Eloquent\Model
, and implement
Illuminate\Contracts\Auth\Authenticatable
. For
convenience, the model may simply extend
ROTGP\AuthSodium\Models\AuthSodiumUser
which already
meets these requirements.
'user.model' => App\Models\User::class
AuthSodium needs to know how to uniquely identify your
auth user. By default, this will be with the user's
'email'
attribute, but you may choose anything, such
as username or even an id (assuming the user knows
their own id).
'user.unique_identifier' => 'username'
You should also note that AuthSodium will look for the
user's public key using the 'public_key'
attribute of
the user model. If you wish to call it something else,
then you may do so as follows:
'user.public_key_identifier' => 'pub_key'
Customizing the package's config values should be enough
for most use-cases, but if you require more fine-grained
control of the AuthSodium's logic, you may specify a
custom delegate. By default the delegate points to
ROTGP\AuthSodium\AuthSodiumDelegate
,
but you can extend this class and override any
functionality you like. Simply update the config value
to point to your custom class as follows:
'delegate' => 'My\Custom\AuthDelegate::class'
Before running AuthSodium's migration, you should consider the following options.
The length of the database column for nonce. By default it's 44, which is 32 base64-encoded bytes. For hex encoded nonces, the length should be 64. Note that this is just a plain string (or integer as a string). It is convenient to generate random bytes with a CSPRNG and encode them as hex or base64, but in the end it's just a string.
'schema.nonce.length' => 44
Whether or not the nonce should be unique per user/timestamp.
If true, then a unique constraint for
user/nonce/timestamp will be created at database level,
meaning that a nonce can be reused if it has a different
timestamp. A request with a repeating
user/nonce/timestamp will still be rejected if the
timestamp does not fall within leeway
of the system
time. This allows for more margin or error (random
nonces being repeated), as the nonces must only be
unique within leeway
of the system time.
If false (the default), then the unique constraint will be for the user/nonce, regardless of the timestamp. So, if user/nonce is repeated (even if days apart), an exception will occur. This means that in order to avoid conflicts, nonces should be cleared regularly (the default).
In either case, using 256-bit nonces generated by a CSPRNG should be more than sufficient to ensure no accidental collisions occur. More discussion here: here.
'schema.nonce.unique_per_timestamp' => false
By default, AuthSodium provides middleware called
'authsodium'
. To protect a route, simply add the
middleware in the same way you'd normally add middleware
Route::resource('foos', FooController::class)->middleware('authsodium');
The
name of the middleware can be customized as follows:
'middleware.name' => 'custom_middleware_name'
If you want to protect all incoming requests
automatically, then set 'middleware.global'
to true:
'middleware.global' => true
If you want to add AuthSodium to a particular middleware group (such as 'web', or 'api'), then you may do so as follows:
'middleware.group' => 'api'
By default, AuthSodium will abort requests with invalid signatures automatically, with the appropriate status and error codes (which are customizable). There may however be situations where you wish to proceed with the request, without establishing an authenticated user. To achieve this - adjust the following value:
'middleware.abort_on_invalid_signature' => false
The leeway
(in milliseconds, unless you're using a
32-bit version of PHP, in which case it is in seconds),
on either side of the timestamp, in which to allow valid
requests. A leeway of 300000 milliseconds (the
default) equates to a request timestamp within 5 minutes
(before or after) the current system timestamp being
accepted. The larger the value, the more forgiving the
service, but this will also result in more nonces being
stored at any given time. This, however, should not be a
concern, as nonce deletion is managed automatically.
The value may be defined as desired, however please note that for security reasons - it is not recommended to use a value that exceeds one hour.
300000 milliseconds = 300 seconds = 5 minutes
'leeway' => 300000
For security reasons, AuthSodium must keep a record (in
the database) of all the nonces used for a particular
user (and possibly also timestamp, according to
authsodium.schema.nonce.unique_per_timestamp
), where
the nonce is not older than the value of
authsodium.leeway
. Nonces that are older than this
value can be safely (and automatically) deleted on a
periodic basis. Below are a few of the options
available, please check the config file for more.
'after_request' => true
'on_terminate' => false
'daily_at' => '23.45'
Failed authenticated requests may be throttled to limit malicious behaviour. If a request's signature is invalid (or missing), and throttling is enabled, then the client must wait for a config-defined number of seconds before attempting another request.
'throttle.enabled' => true
The invervals (in milliseconds, unless you're using a 32-bit version of PHP, in which case it is in seconds) after which a new authentication attempt can be made, after having made an initial failed one. Zero indicates that an attempt can be made immediately. Intervals are relative to the preceding one, so the default would allow three consecutive immediate attempts, then an attempt in 1 second, then 3 seconds following that, etc. After the last attempt fails, the user is considered to be blocked.
'throttle.decay' => [0, 0, 0, 1000, 3000]
Provide a route name such as 'auth/validate' which will
point to the validate
method of
ROTGP\AuthSodium\Http\Controllers\AuthSodiumController
.
The request should be a simple signed GET request to the
route name provided, with no query or post data. The
user is then authenticated and returned. If the
authentication should fail, then the appropriate codes
will be returned.
'routes.validate' => 'auth/validate'
Options for enforcing secure HTTPS/TLS connections. While it's ideal to ensure this with a web-server configuration (such as Nginx) - sometimes that is not possible.
'secure.environments' => 'auth/validate'
'secure.environments' => ['production']
The schemes which are acceptable in secure environments. This should only ever really be https, however, other schemes do exist, such as 'wss' (secure web sockets).
'secure.schemes' => ['https']
AuthSodium is provided under the MIT License.