JWT Authentication
Note
Shield now supports only JWS (Singed JWT). JWE (Encrypted JWT) is not supported.
What is JWT?
JWT or JSON Web Token is a compact and self-contained way of securely transmitting information between parties as a JSON object. It is commonly used for authentication and authorization purposes in web applications.
For example, when a user logs in to a web application, the server generates a JWT token and sends it to the client. The client then includes this token in the header of subsequent requests to the server. The server verifies the authenticity of the token and grants access to protected resources accordingly.
If you are not familiar with JWT, we recommend that you check out Introduction to JSON Web Tokens before continuing.
Setup
To use JWT Authentication, you need additional setup and configuration.
Manual Setup
-
Install "firebase/php-jwt" via Composer.
composer require firebase/php-jwt:^6.4
-
Copy the AuthJWT.php from vendor/codeigniter4/shield/src/Config/ into your project's config folder and update the namespace to
Config
. You will also need to have these classes extend the original classes. See the example below.<?php // app/Config/AuthJWT.php declare(strict_types=1); namespace Config; use CodeIgniter\Shield\Config\AuthJWT as ShieldAuthJWT; /** * JWT Authenticator Configuration */ class AuthJWT extends ShieldAuthJWT { // ... }
-
If your app/Config/Auth.php is not up-to-date, you also need to update it. Check vendor/codeigniter4/shield/src/Config/Auth.php and apply the differences.
You need to add the following constants:
public const RECORD_LOGIN_ATTEMPT_NONE = 0; // Do not record at all public const RECORD_LOGIN_ATTEMPT_FAILURE = 1; // Record only failures public const RECORD_LOGIN_ATTEMPT_ALL = 2; // Record all login attempts
You need to add JWT Authenticator:
use CodeIgniter\Shield\Authentication\Authenticators\JWT; // ... public array $authenticators = [ 'tokens' => AccessTokens::class, 'session' => Session::class, 'jwt' => JWT::class, ];
If you want to use JWT Authenticator in Authentication Chain, add
jwt
:public array $authenticationChain = [ 'session', 'tokens', 'jwt' ];
Configuration
Configure app/Config/AuthJWT.php for your needs.
Set the Default Claims
Note
A payload contains the actual data being transmitted, such as user ID, role, or expiration time. Items in a payload is called claims.
Set the default payload items to the property $defaultClaims
.
E.g.:
public array $defaultClaims = [
'iss' => 'https://codeigniter.com/',
];
The default claims will be included in all tokens issued by Shield.
Set Secret Key
Set your secret key in the $keys
property, or set it in your .env
file.
E.g.:
authjwt.keys.default.0.secret = 8XBFsF6HThIa7OV/bSynahEch+WbKrGcuiJVYPiwqPE=
It needs at least 256 bits random string. The length of the secret depends on the
algorithm we use. The default one is HS256
, so to ensure that the hash value is
secure and not easily guessable, the secret key should be at least as long as the
hash function's output - 256 bits (32 bytes). You can get a secure random string
with the following command:
php -r 'echo base64_encode(random_bytes(32));'
Note
The secret key is used for signing and validating tokens.
Login Attempt Logging
By default, only failed login attempts are recorded in the auth_token_logins
table.
public int $recordLoginAttempt = Auth::RECORD_LOGIN_ATTEMPT_FAILURE;
If you don't want any logs, set it to Auth::RECORD_LOGIN_ATTEMPT_NONE
.
If you want to log all login attempts, set it to Auth::RECORD_LOGIN_ATTEMPT_ALL
.
It means you log all requests.
Issuing JWTs
To use JWT Authentication, you need a controller that issues JWTs.
Here is a sample controller. When a client posts valid credentials (email/password), it returns a new JWT.
// app/Config/Routes.php
$routes->post('auth/jwt', '\App\Controllers\Auth\LoginController::jwtLogin');
<?php
// app/Controllers/Auth/LoginController.php
declare(strict_types=1);
namespace App\Controllers\Auth;
use App\Controllers\BaseController;
use CodeIgniter\API\ResponseTrait;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Shield\Authentication\Authenticators\Session;
use CodeIgniter\Shield\Authentication\JWTManager;
use CodeIgniter\Shield\Validation\ValidationRules;
class LoginController extends BaseController
{
use ResponseTrait;
/**
* Authenticate Existing User and Issue JWT.
*/
public function jwtLogin(): ResponseInterface
{
// Get the validation rules
$rules = $this->getValidationRules();
// Validate credentials
if (! $this->validateData($this->request->getJSON(true), $rules, [], config('Auth')->DBGroup)) {
return $this->fail(
['errors' => $this->validator->getErrors()],
$this->codes['unauthorized']
);
}
// Get the credentials for login
$credentials = $this->request->getJsonVar(setting('Auth.validFields'));
$credentials = array_filter($credentials);
$credentials['password'] = $this->request->getJsonVar('password');
/** @var Session $authenticator */
$authenticator = auth('session')->getAuthenticator();
// Check the credentials
$result = $authenticator->check($credentials);
// Credentials mismatch.
if (! $result->isOK()) {
// @TODO Record a failed login attempt
return $this->failUnauthorized($result->reason());
}
// Credentials match.
// @TODO Record a successful login attempt
$user = $result->extraInfo();
/** @var JWTManager $manager */
$manager = service('jwtmanager');
// Generate JWT and return to client
$jwt = $manager->generateToken($user);
return $this->respond([
'access_token' => $jwt,
]);
}
/**
* Returns the rules that should be used for validation.
*
* @return array<string, array<string, array<string>|string>>
* @phpstan-return array<string, array<string, string|list<string>>>
*/
protected function getValidationRules(): array
{
$rules = new ValidationRules();
return $rules->getLoginRules();
}
}
You could send a request with the existing user's credentials by curl like this:
curl --location 'http://localhost:8080/auth/jwt' \
--header 'Content-Type: application/json' \
--data-raw '{"email" : "[email protected]" , "password" : "passw0rd!"}'
When making all future requests to the API, the client should send the JWT in
the Authorization
header as a Bearer
token.
You could send a request with the Authorization
header by curl like this:
curl --location --request GET 'http://localhost:8080/api/users' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTaGllbGQgVGVzdCBBcHAiLCJzdWIiOiIxIiwiaWF0IjoxNjgxODA1OTMwLCJleHAiOjE2ODE4MDk1MzB9.DGpOmRPOBe45whVtEOSt53qJTw_CpH0V8oMoI_gm2XI'
Protecting Routes
The first way to specify which routes are protected is to use the jwt
controller
filter.
For example, to ensure it protects all routes under the /api
route group, you
would use the $filters
setting on app/Config/Filters.php.
public $filters = [
'jwt' => ['before' => ['api', 'api/*']],
];
You can also specify the filter should run on one or more routes within the routes file itself:
$routes->group('api', ['filter' => 'jwt'], static function ($routes) {
// ...
});
$routes->get('users', 'UserController::list', ['filter' => 'jwt']);
When the filter runs, it checks the Authorization
header for a Bearer
value
that has the JWT. It then validates the token. If the token is valid, it can
determine the correct user, which will then be available through an auth()->user()
call.
Method References
Generating Signed JWTs
JWT to a Specific User
JWTs are created through the JWTManager::generateToken()
method.
This takes a User object to give to the token as the first argument.
It can also take optional additional claims array, time to live in seconds,
a key group (an array key) in the Config\AuthJWT::$keys
, and additional header
array:
public function generateToken(
User $user,
array $claims = [],
?int $ttl = null,
$keyset = 'default',
?array $headers = null
): string
The following code generates a JWT to the user.
use CodeIgniter\Shield\Authentication\JWTManager;
/** @var JWTManager $manager */
$manager = service('jwtmanager');
$user = auth()->user();
$claims = [
'email' => $user->email,
];
$jwt = $manager->generateToken($user, $claims);
It sets the Config\AuthJWT::$defaultClaims
to the token, and adds
the 'email'
claim and the user ID in the "sub"
(subject) claim.
It also sets "iat"
(Issued At) and "exp"
(Expiration Time) claims automatically
if you don't specify.
Arbitrary JWT
You can generate arbitrary JWT with the JWTManager::issue()
method.
It takes a JWT claims array, and can take time to live in seconds, a key group
(an array key) in the Config\AuthJWT::$keys
, and additional header array:
public function issue(
array $claims,
?int $ttl = null,
$keyset = 'default',
?array $headers = null
): string
The following code generates a JWT.
use CodeIgniter\Shield\Authentication\JWTManager;
/** @var JWTManager $manager */
$manager = service('jwtmanager');
$payload = [
'user_id' => '1',
'email' => '[email protected]',
];
$jwt = $manager->issue($payload, DAY);
It uses the secret
and alg
in the Config\AuthJWT::$keys['default']
.
It sets the Config\AuthJWT::$defaultClaims
to the token, and sets
"iat"
(Issued At) and "exp"
(Expiration Time) claims automatically even if
you don't pass them.
Logging
Login attempts are recorded in the auth_token_logins
table, according to the
configuration above.
When a failed login attempt is logged, the raw token value sent is saved in
the identifier
column.
When a successful login attempt is logged, the SHA256 hash value of the token
sent is saved in the identifier
column.