Here is an example of using this package to secure your API.
This package is a wrapper of the lcobucci/jwt so for the details of using it I recommend reading its documentation.
I'm using here ECDSA asymmetric key with elliptic curve size 256 - the code of this key is ES256
.
On Linux run
openssl ecparam -genkey -name prime256v1 -noout -out private.pem
This creates a file with your private key named private.pem
. Next run
openssl ec -in private.pem -pubout -out public.pem
You will get a file with your public key named public.pem
. Private and public keys are different hence the type -
asymmetric. You can name the files differently if you want.
Place the files somewhere where they cannot be accessed through the web (IMPORTANT!) but are still readable by your
application. You can place them in your application structure but usually above the web
or public
folder - again,
it all depends on your server configuration.
For other OS please refer to the online guides about generating SSH keys.
Add jwt
component to your configuration file:
[
'components' => [
'jwt' => [
'class' => \bizley\jwt\Jwt::class,
'signer' => \bizley\jwt\Jwt::ES256,
'signingKey' => [
'key' => '...', // path to your PRIVATE key, you can start the path with @ to indicate this is a Yii alias
'method' => \bizley\jwt\Jwt::METHOD_FILE,
],
'verifyingKey' => [ // required for asymmetric keys
'key' => '...', // path to your PUBLIC key, you can start the path with @ to indicate this is a Yii alias
'method' => \bizley\jwt\Jwt::METHOD_FILE,
],
'validationConstraints' => static fn (\bizley\jwt\Jwt $jwt) {
$config = $jwt->getConfiguration();
return [
new \Lcobucci\JWT\Validation\Constraint\SignedWith($config->signer(), $config->verificationKey()),
new \Lcobucci\JWT\Validation\Constraint\LooseValidAt(
new \Lcobucci\Clock\SystemClock(new \DateTimeZone(\Yii::$app->timeZone)),
new \DateInterval('PT10S')
),
];
}
],
],
],
Validation constraints used here are:
SignedWith
- this will make sure that received token is indeed signed with the chosen signer and can be verified with the provided verification key,LooseValidAt
- this will make sure that token is not expired yet allowing 10 seconds leeway (in case of some delays between the server and the client), we are here also setting the same time zone that is used in the application.
NOTE: The above implementation requires to install lcobucci/clock
library first (run composer req lcobucci/clock
).
If you prefer other PSR-20 clock implementation you must change the above \Lcobucci\Clock\SystemClock()
usage.
You can also add here any other constraint that you find necessary. The available list is at
https://github.com/lcobucci/jwt/tree/5.5.x/src/Validation/Constraint, and you can always write your own constraint as
long as it implements Lcobucci\JWT\Validation\Constraint
.
For other ways to add constraints please refer to the README file.
New access token should be given to the API client after successful authentication, which usually is done through
providing valid username and password. I'm assuming you have prepared a controller (or similar) to handle the user input
and validation - the next step, after we know that the user is indeed someone that can access our API, is to generate
the token and return it to the user.
We can generate new token with:
$now = new \DateTimeImmutable('now', new \DateTimeZone(\Yii::$app->timeZone));
$token = \Yii::$app->jwt->getBuilder()
// Configures the time that the token was issued
->issuedAt($now)
// Configures the time that the token can be used
->canOnlyBeUsedAfter($now)
// Configures the expiration time of the token
->expiresAt($now->modify('+1 hour'))
// Configures a new claim, called "uid", with user ID, assuming $user is the authenticated user object
->withClaim('uid', $user->id)
// Builds a new token
->getToken(
\Yii::$app->jwt->getConfiguration()->signer(),
\Yii::$app->jwt->getConfiguration()->signingKey()
);
$tokenString = $token->toString();
Now it's a matter of returning this value back to the client, for example:
return ['token' => $tokenString];
API client should use the given token and send it with the API requests to authorize the user.
In order to do that client must send Authorization
header with value Bearer xxx
, where xxx
is the token string
itself.
In the API controller we can add authorization filter:
class ExampleController extends Controller
{
public function behaviors()
{
$behaviors = parent::behaviors();
$behaviors['authenticator'] = [
'class' => \bizley\jwt\JwtHttpBearerAuth::class,
];
return $behaviors;
}
}
The first thing JwtHttpBearerAuth
does is to validate the given token, so it must be properly signed and not expired.
The second thing is to find the user, the token was issued for. To do that let's modify the User
class, the one
configured in user
component (the one implementing yii\web\IdentityInterface
).
This class must have findIdentityByAccessToken
static method I will use.
public static function findIdentityByAccessToken($token, $type = null)
{
$claims = \Yii::$app->jwt->parse($token)->claims();
$uid = $claims->get('uid');
if (!is_numeric($uid)) {
throw new ForbiddenHttpException('Invalid token provided');
}
return static::findOne(['id' => $uid]);
}
If the method above returns User object, user
component uses it (Yii::$app->user->identity
) so the application
further on can rely on this information and act accordingly.