Important
This package is a replacement for tuupola/slim-jwt-auth
with the updated
version of firebase/php-jwt to resolve
CVE-2021-46743
in the meantime. I plan to maintain compatibility with v1, and then in v2 I
plan to diverge, adding new features and dropping support for older PHP
versions.
This middleware implements JSON Web Token Authentication. It was originally developed for Slim but can be used with any framework using PSR-7 and PSR-15 style middleware. It has been tested with Slim Framework and Zend Expressive.
Heads up! You are reading the documentation for 3.x branch which is PHP 7.4 and up only. If you are using an older version of PHP see the 2.x branch. These two branches are not backwards compatible, see UPGRADING for instructions on how to upgrade.
Middleware does not implement an OAuth 2.0 authorization server nor does it provide ways to generate, issue or store authentication tokens. It only parses and authenticates a token when passed via header or cookie. This is useful for example when you want to use JSON Web Tokens as API keys.
For example implementation see Slim API Skeleton.
The default algorithm
has changed from ['HS256', 'HS512', 'HS384']
to
['HS256']
in most cases this will not be a problem, unless you are using
multiple JWT with different encoding
The way the secrets
and algorithm
are passed has changed, It now requires a
unique key to match the secret and algorithm together.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => ["acme" => "supersecretkeyyoushouldnotcommittogithub"],
"algorithm" => ["amce" => "HS256"]
]));
If your application is using multiple JWTs with different algorithms you will
need to change how the JWT is created. Each token now must include the kid
in
the header, this must match the corresponding algorithm/secret key as the
middleware uses this to decode the JWT. if you using firebase/php-jwt
to
create your tokens here's how to do this.
$hs256token = JWT::encode([...], 'tooManySecrets', 'HS256', 'acme');
$hs512token = JWT::encode([...], 'tooManySecrets', 'HS512', 'beta');
Switch over the package by using the following commands, for now, the namespace is the same.
composer require -W jimtools/jwt-auth
Update the JwtAuthentication
config to have keys for the secret
and
algorithm
to have a unique index.
Before
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub",
"algorithm" => ["HS256"]
]));
After
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => ["acme" => "supersecretkeyyoushouldnotcommittogithub"],
"algorithm" => ["acme" => "HS256"],
]));
(Maybe) If you're using multiple encryption algorithms you will need to add the
kid
to the JWT header.
firebase JWT Docs
Install the latest version using composer.
composer require jimtools/jwt-auth
If using Apache add the following to the .htaccess
file. Otherwise, PHP won't
have access to the Authorization: Bearer
header.
RewriteRule .* - [env=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
Configuration options are passed as an array. The only mandatory parameter is
secret
which is used for verifying the token signature. Note again that
secret
is not the token. It is the secret you use to sign the token.
For simplicity's sake examples show secret
hardcoded in code. In real life,
you should store it somewhere else. A good option is the environment variables.
You can use dotenv or something similar
for development. Examples assume you are using Slim Framework.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
An example where your secret is stored as an environment variable:
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => getenv("JWT_SECRET")
]));
When a request is made, the middleware tries to validate and decode the token.
If a token is not found or there is an error when validating and decoding it,
the server will respond with 401 Unauthorized
.
Validation errors are triggered when the token has been tampered with or the token has expired. For all possible validation errors, see JWT library source.
The optional path
parameter allows you to specify the protected part of your
website. It can be either a string or an array. You do not need to specify each
URL. Instead, think of path
setting as a folder. In the example below
everything starting with /api
will be authenticated. If you do not define
path
all routes will be protected.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"path" => "/api", /* or ["/api", "/admin"] */
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
With the optional ignore
parameter you can make exceptions to path
parameter. In the example below everything starting with /api
and /admin
will be authenticated except /api/token
and /admin/ping
which will not be
authenticated.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"path" => ["/api", "/admin"],
"ignore" => ["/api/token", "/admin/ping"],
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
By default, middleware tries to find the token from the Authorization
header.
You can change the header name using the header
parameter.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"header" => "X-Token",
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
By default, the middleware assumes the value of the header is in
Bearer <token>
format. You can change this behaviour with the regexp
parameter. For example, if you have a custom header such as X-Token: <token>
you should pass both header and regexp parameters.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"header" => "X-Token",
"regexp" => "/(.*)/",
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
If the token is not found from the header, the middleware tries to find it via a
cookie named token
. You can change the cookie name using the cookie
parameter.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"cookie" => "nekot",
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
You can set supported algorithms via the algorithm
parameter. This can be
either a string or an array of strings. The default value is ["HS256"]
.
Supported algorithms are HS256
, HS384
, HS512
and RS256
. Note that
enabling both HS256
and RS256
is a security risk.
When passing multiple algorithms it must be a key array, with the key matching
the kid
of the JWT.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => [
"acme" => "supersecretkeyyoushouldnotcommittogithub",
"beta" => "supersecretkeyyoushouldnotcommittogithub",
"algorithm" => [
"amce" => "HS256",
"beta" => "HS384"
]
]));
⚠️ Warning: Because of changes infirebase/php-jwt
thekid
is now checked when multiple algorithms are set, if you do not specify a key the algorithm will be used as the key. this also means thekid
will now need to be present in the JWT header as well.
When the token is decoded successfully and authentication succeeds the contents
of the decoded token are saved as a token
attribute to the $request
object.
You can change this with. attribute
parameter. Set to null
or false
to
disable this behaviour
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"attribute" => "jwt",
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
/* ... */
$decoded = $request->getAttribute("jwt");
The optional logger
parameter allows you to pass in a PSR-3 compatible logger
to help with debugging or other application logging needs.
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
$app = new Slim\App;
$logger = new Logger("slim");
$rotating = new RotatingFileHandler(__DIR__ . "/logs/slim.log", 0, Logger::DEBUG);
$logger->pushHandler($rotating);
$app->add(new Tuupola\Middleware\JwtAuthentication([
"path" => "/api",
"logger" => $logger,
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
The before function is called only when authentication succeeds but before the
next incoming middleware is called. You can use this to alter the request before
passing it to the next incoming middleware in the stack. If it returns anything
else than Psr\Http\Message\ServerRequestInterface
the return value will be
ignored.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub",
"before" => function ($request, $arguments) {
return $request->withAttribute("test", "test");
}
]));
The after function is called only when authentication succeeds and after the
incoming middleware stack has been called. You can use this to alter the
response before passing it next outgoing middleware in the stack. If it returns
anything else than Psr\Http\Message\ResponseInterface
the return value will be
ignored.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub",
"after" => function ($response, $arguments) {
return $response->withHeader("X-Brawndo", "plants crave");
}
]));
Note that both the after and before callback functions receive the raw token string as well as the decoded claims through the
$arguments
argument.
Error is called when authentication fails. It receives the last error message in arguments. You can use this for example to return JSON formatted error responses.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub",
"error" => function ($response, $arguments) {
$data["status"] = "error";
$data["message"] = $arguments["message"];
$response->getBody()->write(
json_encode($data, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT)
);
return $response->withHeader("Content-Type", "application/json")
}
]));
The optional rules
parameter allows you to pass in rules which define whether
the request should be authenticated or not. A rule is a callable which receives
the request as a parameter. If any of the rules returns boolean false
the
request will not be authenticated.
By default, the middleware configuration looks like this. All paths are
authenticated with all request methods except OPTIONS
.
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"rules" => [
new Tuupola\Middleware\JwtAuthentication\RequestPathRule([
"path" => "/",
"ignore" => []
]),
new Tuupola\Middleware\JwtAuthentication\RequestMethodRule([
"ignore" => ["OPTIONS"]
])
]
]));
RequestPathRule contains both a path
parameter and a ignore
parameter. Later
contains paths which should not be authenticated. RequestMethodRule contains the
ignore
parameter of request methods which also should not be authenticated.
Think of ignore
as a whitelist.
In 99% of the cases, you do not need to use the rules
parameter. It is only
provided for special cases when defaults do not suffice.
JSON Web Tokens are essentially passwords. You should treat them as such and you
should always use HTTPS. If the middleware detects insecure usage over HTTP it
will throw a RuntimeException
. By default, this rule is relaxed for requests
to the server running on localhost
. To allow insecure usage you must enable it
manually by setting secure
to false
.
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secure" => false,
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
Alternatively, you could list multiple development servers to have relaxed
security. With the below settings both localhost
and dev.example.com
allow
incoming unencrypted requests.
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secure" => true,
"relaxed" => ["localhost", "dev.example.com"],
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
By default middleware only authenticates. This is not very interesting. The beauty of JWT is you can pass extra data in the token. This data can include for example scope which can be used for authorization.
It is up to you to implement how token data is stored or possible authorization implemented.
Let's assume you have a token which includes data for scope. By default,
middleware saves the contents of the token to the token
attribute of the
request.
[
"iat" => "1428819941",
"exp" => "1744352741",
"scope" => ["read", "write", "delete"]
]
$app = new Slim\App;
$app->add(new Tuupola\Middleware\JwtAuthentication([
"secret" => "supersecretkeyyoushouldnotcommittogithub"
]));
$app->delete("/item/{id}", function ($request, $response, $arguments) {
$token = $request->getAttribute("token");
if (in_array("delete", $token["scope"])) {
/* Code for deleting item */
} else {
/* No scope so respond with 401 Unauthorized */
return $response->withStatus(401);
}
});
You can run tests either manually or automatically on every code change. Automatic tests require entr to work.
make test
brew install entr
make watch
Please see CONTRIBUTING for details.
If you discover any security-related issues, please email james.read.18@gmail.com instead of using the issue tracker.
The MIT License (MIT). Please see License File for more information.