Modern and highly flexible PSR-15 authentication and authorization middleware.
💿
composer require dakujem/auth-middleware
To use this package, you create two middleware layers:
- a token-decoding middleware that decodes encoded JWT present in the request and verifies its authenticity
- and a middleware that authorizes the request by asserting the presence of the decoded token
Use Dakujem\Middleware\AuthWizard
for convenience:
/* @var Slim\App $app */
$app->add(AuthWizard::assertTokens($app->getResponseFactory()));
$app->add(AuthWizard::decodeTokens(new Secret('a-secret-api-key-never-to-commit', 'HS256')));
The pair of middleware (MW) will look for a JWT
in the Authorization
header or token
cookie.
Then it will decode the JWT and inject the decoded payload to the token
request attribute,
accessible to the application.
If the token is not present or is not valid, the execution pipeline will be terminated
by the assertion middleware
and a 401 Unauthorized
response will be returned.
The token can be accessed via the request attribute:
/* @var Request $request */
$decodedToken = $request->getAttribute('token');
You can choose to apply the assertion to selected routes only instead of every route:
$mwFactory = AuthWizard::factory(new Secret('a-secret-api-key-never-to-commit', 'HS256'), $app->getResponseFactory());
// Decode the token for all routes,
$app->add($mwFactory->decodeTokens());
// but only apply the assertion to selected ones.
$app->group('/foo', ...)->add($mwFactory->assertTokens());
Custom token inspection can be applied too:
$app->group('/admin', ...)->add(AuthWizard::inspectTokens(
$app->getResponseFactory(),
function(MyToken $token, $next, $withError): Response {
return $token->grantsAdminAccess() ? $next() : $withError('Admin privilege required!');
}
));
💡
For highly flexible options to instantiate the middleware,
read the "Compose Your Own Middleware" chapter below.
The examples above use Slim PHP framework, but the same usage applies to any PSR-15 compatible middleware dispatcher.
AuthWizard::decodeTokens(__
// a combination of secret and the encryption algorithm used
new Secret('a-secret-api-key-never-to-commit', 'HS256'),
'token', // what attribute to put the decoded token to
'Authorization', // what header to look for the Bearer token in
'token', // what cookie to look for the raw token in
'token.error' // what attribute to write error messages to
);
The above creates an instance of TokenMiddleware
that uses the default JWT decoder
and injects the decoded token to the token
Request attribute accessible further in the app stack.
If the decoded token appears in the attribute, it is:
- present (obviously)
- authentic (has been created using the key)
- valid (not expired)
The middleware above will only decode the token, if present, authentic and valid, but will NOT terminate the pipeline in any case❗
The authorization must be done by a separate middleware:
AuthWizard::assertTokens(
$responseFactory, // PSR-17 Request factory
'token', // what attribute to look for the decoded token in
'token.error' // what attribute to look for error messages in
);
The above creates a middleware that will assert that the token
attribute of the Request contains a decoded token.
Otherwise, the pipeline will be terminated and 401 (Unauthorized) Response returned.
An error message will be encoded as JSON into the response.
As you can see, the pair of middleware acts as a couple, but is decoupled for flexibility.
The middleware created by AuthWizard::assertTokens
asserts the presence of the decoded token only.
It is possible to create custom inspections, of course:
$inspector = function (object $token, callable $next, callable $withError): Response {
if ($token->sub === 42) { // Implement your inspection logic here.
return $next(); // Invoke the next middleware for valid tokens
} // or
return $withError('Bad token.'); // return an error response for invalid ones.
};
AuthWizard::inspectTokens(
$responseFactory, // PSR-17 Request factory
$inspector,
'token', // what attribute to look for the decoded token in
'token.error' // what attribute to look for error messages in
);
Using AuthWizard::inspectTokens
, the pipeline can be terminated on any conditions, involving the token or not.
Custom error messages or data can be passed to the Response.
If the token is not present, the middleware acts the same as the one created by assertTokens
and the inspector is not called.
You are of course able to cast the token to a custom class,
with methods like MyToken::grantsAdminAccess
to tell if the token authorizes the user for admin access.
AuthWizard::inspectTokens(
$responseFactory,
function(MyToken $token, $next, $withError): Response {
return $token->grantsAdminAccess() ? $next() : $withError('Admin privilege required!');
}
);
The cast can either be done in the decoder or in a separate middleware.
In the examples above, we are using the AuthWizard
helper which provides sensible defaults.
However, it is possible and encouraged to build your own middleware using the components provided by this package.
You have the flexibility to fine-tune the middleware for any use case.
I'm using aliased names instead of full interface names in this documentation for brevity.
Here are the full interface names:
Alias Full interface name Request
Psr\Http\Message\ServerRequestInterface
Response
Psr\Http\Message\ResponseInterface
ResponseFactory
Psr\Http\Message\ResponseFactoryInterface
Handler
Psr\Http\Server\RequestHandlerInterface
Logger
Psr\Log\LoggerInterface
The TokenMiddleware
is responsible for finding and decoding a token,
then making it available to the rest of the app.
The TokenMiddleware
is composed of
- a set of extractors
- an extractor is responsible for finding and extracting a token from a Request, or return
null
- executed in sequence until one returns a string
fn(Request,Logger):?string
- an extractor is responsible for finding and extracting a token from a Request, or return
- a decoder
- the decoder takes the raw token and decodes it
- must only return a valid token object or
null
fn(string,Logger):?object
- an injector
- the injector is responsible for decorating the Request with the decoded token or error messages
- obtains the decoded token by running the callable passed to its first argument, which is
fn():?object
fn(callable,Request,Logger):Request
Any of these callable components can be replaced or extended.
The default components offer customization too.
Here are the defaults provided by AuthWizard::decodeTokens
:
new TokenMiddleware(
// decode JWT tokens
new FirebaseJwtDecoder('a-secret-never-to-commit', ['HS256', 'HS512', 'HS384']),
[
// look for the tokens in the `Authorization` header
TokenManipulators::headerExtractor('Authorization'),
// look for the tokens in the `token` cookie
TokenManipulators::cookieExtractor('token'),
],
// target the `token` and `token.error` attributes for writing the decoded token or error message
TokenManipulators::attributeInjector('token', 'token.error')
);
Usage tips 💡:
- The decoder can be swapped in order to use OAuth tokens or a different JWT implementation.
- Exceptions may be caught and processed by the injector by wrapping the provider callable in a try-catch block
try { $token = $provider(); } catch (RuntimeException $e) { ...
- The decoder may return any object, this is the place to cast the raw payload into your object of choice. Alternatively, a separate middleware can be used for that purpose.
AuthWizard
is a friction reducer that helps quickly instantiate token-decoding and assertion middleware with sensible defaults.
AuthFactory
is a configurable factory with sensible defaults provided for convenience.
AuthWizard
internally instantiates AuthFactory
and acts as a static facade for the factory.
Use AuthFactory::decodeTokens
to create token-decoding middleware.
Use AuthFactory::assertTokens
to create middleware that asserts the presence of a decoded token.
Use AuthFactory::inspectTokens
to create middleware with custom authorization rules against the token.
The GenericMiddleware
is used for assertion of token presence and custom authorization by AuthWizard
/ AuthFactory
.
It can also be used for convenient inline middleware implementation:
$app->add(new GenericMiddleware(function(Request $request, Handler $next): Response {
$request = $request->withAttribute('foo', 42);
$response = $next->handle($request);
return $response->withHeader('foo', 'bar');
}));
The TokenManipulators
static class provides various request/response manipulators
that can be used for token handling.
They are used as components of the middleware.
The FirebaseJwtDecoder
class serves as the default implementation for JWT token decoding.
It is used as a decoder for the TokenMiddleware
.
You can swap it for a different implementation.
You need to install Firebase JWT package in order to use this decoder.
composer require firebase/php-jwt:"^5.5"
The TokenMiddleware
accepts a PSR-3 Logger
instance for debug purposes.
Multiple token-decoding and token-inspecting middleware can be stacked too!
Token decoding will usually be applied to the app-level middleware (every route), but the assertions can be composed and applied to groups or individual routes as needed.
Run unit tests using the following command:
$
composer test
dakujem/auth-middleware |
PHP |
---|---|
1.x |
7.4 - 8.2 |
2.x |
8+ |
In order to use the FirebaseJwtDecoder
decoder, a correct version of firebase/php-jwt
must be installed.
The use of this decoder is not required though.
dakujem/auth-middleware |
firebase/php-jwt |
---|---|
1.0 - 1.2 |
^5 |
1.2 |
^6 when using a single secret+algorithm combination only |
2 |
^5.5 , ^6 and above |
Ideas, feature requests and other contribution is welcome. Please send a PR or create an issue.
If you happen to find a security problem, create an issue without disclosing any relevant details, we'll get in touch and discuss the details privately.