Secciones

Seguridad - Tokens Web JSON (JWT)


Resumen

NOTA: Actualmente, sólo se soportan algoritmos simétricos

Phalcon\Encryption\Security\JWT is a namespace that contains components that allow you to issue, parse and validate JSON Web Tokens as described in RFC 7915. Estos componentes son:

NOTE: For the examples below, we have split the output split into different lines for readability

Un ejemplo de uso del componente es:

<?php

use Phalcon\Encryption\Security\JWT\Builder;
use Phalcon\Encryption\Security\JWT\Signer\Hmac;
use Phalcon\Encryption\Security\JWT\Token\Parser;
use Phalcon\Encryption\Security\JWT\Validator;

// Defaults to 'sha512'
$signer  = new Hmac();

// Builder object
$builder = new Builder($signer);

$now        = new DateTimeImmutable();
$issued     = $now->getTimestamp();
$notBefore  = $now->modify('-1 minute')->getTimestamp();
$expires    = $now->modify('+1 day')->getTimestamp();
$passphrase = 'QcMpZ&b&mo3TPsPk668J6QH8JA$&U&m2';

// Setup
$builder
    ->setAudience('https://target.phalcon.io')  // aud
    ->setContentType('application/json')        // cty - header
    ->setExpirationTime($expires)               // exp 
    ->setId('abcd123456789')                    // JTI id 
    ->setIssuedAt($issued)                      // iat 
    ->setIssuer('https://phalcon.io')           // iss 
    ->setNotBefore($notBefore)                  // nbf
    ->setSubject('my subject for this claim')   // sub
    ->setPassphrase($passphrase)                // password 
;

// Phalcon\Encryption\Security\JWT\Token\Token
$tokenObject = $builder->getToken();

echo $tokenObject->getToken();

// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiIsImN0eSI6ImFwcGxpY2F0aW9uXC9qc29uIn0.
// eyJhdWQiOlsiaHR0cHM6XC9cL3RhcmdldC5waGFsY29uLmlvIl0sImV4cCI6MTYxNDE4NTkxN
// ywianRpIjoiYWJjZDEyMzQ1Njc4OSIsImlhdCI6MTYxNDA5OTUxNywiaXNzIjoiaHR0cHM6XC
// 9cL3BoYWxjb24uaW8iLCJuYmYiOjE2MTQwOTk0NTcsInN1YiI6Im15IHN1YmplY3QgZm9yIHR
// oaXMgY2xhaW0ifQ.
// LdYevRZaQDZ2lul4CCQ5DymeP2ubcapTtgeezOZGIq7Meu7rFF1pv32b-AMWOxCS63CQz_jpm
// BPlPyOeEAkMbg
// #01
$tokenReceived = getMyTokenFromTheApplication();
$audience      = 'https://target.phalcon.io';
$now           = new DateTimeImmutable();
$issued        = $now->getTimestamp();
$notBefore     = $now->modify('-1 minute')->getTimestamp();
$expires       = $now->getTimestamp();
$id            = 'abcd123456789';
$issuer        = 'https://phalcon.io';

// #02
$signer     = new Hmac();
$passphrase = 'QcMpZ&b&mo3TPsPk668J6QH8JA$&U&m2';

// #03
$parser      = new Parser();

// Phalcon\Encryption\Security\JWT\Token\Token
$tokenObject = $parser->parse($tokenReceived);

// Phalcon\Encryption\Security\JWT\Validator
$validator = new Validator($tokenObject, 100); // #04

# 05
$validator
    ->validateAudience($audience)
    ->validateExpiration($expires)
    ->validateId($id)
    ->validateIssuedAt($issued)
    ->validateIssuer($issuer)
    ->validateNotBefore($notBefore)
    ->validateSignature($signer, $passphrase)
;

# 06
var_dump($validator->getErrors())

LEGEND

  1. $tokenReceived is what we received

  2. Defaults to ‘sha512’

  3. Parse the token

  4. Allow for a time shift of 100

  5. Run the validators

  6. Errors printed out (if any)

El ejemplo anterior da una vista general de cómo se puede usar el componente para generar, analizar y validar Tokens Web JSON.

Objetos

There are several utility components that live in the Phalcon\Encryption\Security\JWT\Token namespace, that help with the issuing, parsing and validating JWT tokens

Enum

Phalcon\Encryption\Security\JWT\Token\Enum is a class that contains several constants. Estas constantes son cadenas definidas en RFC 7915. Puede usarlas si desea o en su lugar usar sus cadenas equivalentes.

<?php

class Enum
{
    /**
     * Headers
     */
    const TYPE         = "typ";
    const ALGO         = "alg";
    const CONTENT_TYPE = "cty";

    /**
     * Claims
     */
    const AUDIENCE        = "aud";
    const EXPIRATION_TIME = "exp";
    const ID              = "jti";
    const ISSUED_AT       = "iat";
    const ISSUER          = "iss";
    const NOT_BEFORE      = "nbf";
    const SUBJECT         = "sub";
}

Item

Phalcon\Encryption\Security\JWT\Token\Item is used internally to store a payload as well as its encoded state. Such payload can be the claims data or the headers’ data. Al usar este componente, podemos extraer fácilmente la información necesaria de cada Token.

Signature

Phalcon\Encryption\Security\JWT\Token\Signature is similar to the Phalcon\Encryption\Security\JWT\Token\Item, but it only holds the signature hash as well as its encoded value.

Token

Phalcon\Encryption\Security\JWT\Token\Token is the component responsible for storing and calculating the JWT token. It accepts the headers, claims (as Phalcon\Encryption\Security\JWT\Token\Item objects) and signature objects in its constructor and exposes:

public function getClaims(): Item

Return the claims collection

public function getHeaders(): Item

Return the headers collection

public function getPayload(): string

Return the payload. For a token abcd.efgh.ijkl, it will return abcd.efgh

public function getSignature(): Signature

Return the signature

public function getToken(): string

Return the token as a string. For a token abcd.efgh.ijkl it will return abcd.efgh.ijkl.

public function validate(Validator $validator): array

Run all validators against the token data. Return the errors array from the validator

public function verify(SignerInterface $signer, string $key): bool

Verify the signature of the token

Signer

Para crear un token JWT, necesitamos suministrar un algoritmo de Firma. By default, the builder uses “none” (Phalcon\Encryption\Security\JWT\Signer\None). You can however use the HMAC signer (Phalcon\Encryption\Security\JWT\Signer\Hmac). Also, for further customization, you can utilize the supplied Phalcon\Encryption\Security\JWT\Signer\SignerInterface interface.

<?php

use Phalcon\Encryption\Security\JWT\Signer\Hmac;

$signer  = new Hmac();

None

El firmante se proporciona principalmente para propósitos de desarrollo. Siempre debería firmar sus tokens JWT.

HMAC

El firmante HMAC soporta los algoritmos sha512, sha384, y sha256. Si no se proporciona ninguno, se seleccionará automáticamente sha512. If you supply a different algorithm, a Phalcon\Encryption\Security\JWT\Exceptions\UnsupportedAlgorithmException will be raised. El algoritmo se establece en el constructor.

<?php

use Phalcon\Encryption\Security\JWT\Signer\Hmac;

$signer  = new Hmac();
$signer  = new Hmac('sha512');
$signer  = new Hmac('sha384');
$signer  = new Hmac('sha256');
$signer  = new Hmac('sha111'); // exception

El componente usa internamente los métodos PHP [hmac_equals][hmac_equals] y [hash_hmac][hash_hmac] para verificar y firmar la carga útil. Expone los siguientes métodos:

public function getAlgHeader(): string

Devuelve una cadena que identifica el algoritmo. For the HMAC algorithms it will return:

Algoritmo getAlgHeader
sha512 HS512
sha384 HS384
sha256 HS256
public function sign(string $payload, string $passphrase): string

Devuelve el hash de la carga útil usando la frase secreta

public function verify(string $source, string $payload, string $passphrase): bool

Verifica que el hash de la cadena original es el mismo que el hash de la carga útil con la frase secreta.

Emisión de Tokens

A Builder component (Phalcon\Encryption\Security\JWT\Builder) is available, utilizing chained methods, and ready to be used to create JWT tokens. Todo lo que tiene que hacer es instanciar el objeto Builder, configurar su token y llamar getToken(). This will return a Phalcon\Encryption\Security\Token\Token object which contains all the necessary information for your token. Al instanciar el componente constructor, debe proporcionar la clase de firmante. In the example below we use the Phalcon\Encryption\Security\JWT\Signer\Hmac signer.

Todos los setters de este componente son encadenables.

<?php

use Phalcon\Encryption\Security\JWT\Builder;
use Phalcon\Encryption\Security\JWT\Signer\Hmac;

// Defaults to 'sha512'
$signer  = new Hmac();

// Builder object
$builder = new Builder($signer);

Métodos

public function __construct(SignerInterface $signer): Builder

Constructor

public function init(): Builder

Inicializa el objeto - útil cuando quiere reutilizar el mismo constructor

public function addClaim(string $name, mixed $value): Builder

Adds a custom claim in the claims collection

public function getAudience(): array|string

Obtiene el contenido de aud

public function getClaims(): array

Obtiene los reclamos como vector

public function getContentType(): ?string

Devuelve el tipo de contenido (cty - cabeceras)

public function getExpirationTime(): ?int

Devuelve el contenido de exp

public function getHeaders(): array

Devuelve las cabeceras como vector

public function getId(): ?string

Devuelve el contenido de jti (ID de este JWT)

public function getIssuedAt(): ?int

Devuelve el contenido de iat

public function getIssuer(): ?string

Devuelve el contenido de iss

public function getNotBefore(): ?int

Devuelve el contenido de nbf

public function getSubject(): ?string

Devuelve el contenido de sub

public function getToken(): Token

Devuelve el token

public function getPassphrase(): string

Devuelve la frase secreta proporcionada

public function setAudience(array|string $audience): Builder

Establece la audiencia (aud). If the parameter passed is not an array or a string, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function setContentType(string $contentType): Builder

Establece el tipo de contenido (cty - cabeceras)

public function setExpirationTime(int $timestamp): Builder

Establece la audiencia (exp). If the $timestamp is less than the current time, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function setId(string $id): Builder

Establece el id (jti).

public function setIssuedAt(int $timestamp): Builder

Establece el emitido a la hora (iat).

public function setIssuer(string $issuer): Builder

Establece el emisor (iss).

public function setNotBefore(int $timestamp): Builder

Sets el tiempo no anterior (nbf). If the $timestamp is greater than the current time, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function setSubject(string $subject): Builder

Establece el asunto (sub).

public function setPassphrase(string $passphrase): Builder

Establece la frase secreta. If the $passphrase is weak, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

private function setClaim(string $name, $value): Builder

Establece un valor de reclamo en la colección interna.

Ejemplo

<?php

use Phalcon\Encryption\Security\JWT\Builder;
use Phalcon\Encryption\Security\JWT\Signer\Hmac;
use Phalcon\Encryption\Security\JWT\Token\Parser;
use Phalcon\Encryption\Security\JWT\Validator;

// 'sha512'
$signer  = new Hmac();

$builder = new Builder($signer);

$now        = new DateTimeImmutable();
$issued     = $now->getTimestamp();
$notBefore  = $now->modify('-1 minute')->getTimestamp();
$expires    = $now->modify('+1 day')->getTimestamp();
$passphrase = 'QcMpZ&b&mo3TPsPk668J6QH8JA$&U&m2';

$builder
    ->setAudience('https://target.phalcon.io')  // aud
    ->setContentType('application/json')        // cty - header
    ->setExpirationTime($expires)               // exp 
    ->setId('abcd123456789')                    // JTI id 
    ->setIssuedAt($issued)                      // iat 
    ->setIssuer('https://phalcon.io')           // iss 
    ->setNotBefore($notBefore)                  // nbf
    ->setSubject('my subject for this claim')   // sub
    ->setPassphrase($passphrase)                // password 
;

// Phalcon\Encryption\Security\JWT\Token\Token 
$tokenObject = $builder->getToken();

echo $tokenObject->getToken();

// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiIsImN0eSI6ImFwcGxpY2F0aW9uXC9qc29uIn0.
// eyJhdWQiOlsiaHR0cHM6XC9cL3RhcmdldC5waGFsY29uLmlvIl0sImV4cCI6MTYxNDE4NTkxN
// ywianRpIjoiYWJjZDEyMzQ1Njc4OSIsImlhdCI6MTYxNDA5OTUxNywiaXNzIjoiaHR0cHM6XC
// 9cL3BoYWxjb24uaW8iLCJuYmYiOjE2MTQwOTk0NTcsInN1YiI6Im15IHN1YmplY3QgZm9yIHR
// oaXMgY2xhaW0ifQ.
// LdYevRZaQDZ2lul4CCQ5DymeP2ubcapTtgeezOZGIq7Meu7rFF1pv32b-AMWOxCS63CQz_jpm
// BPlPyOeEAkMbg

Validación de Tokens

In order to validate a token you will need to create a new Phalcon\Encryption\Security\JWT\Validator object. The object can be constructed using a Phalcon\Encryption\Security\JWT\Token\Token object and an offset in time to handle time/clock shifts of the sending and receiving computers.

In order to parse the JWT received and convert it to a Phalcon\Encryption\Security\JWT\Token\Token object, you will need to use a Phalcon\Encryption\Security\JWT\Token\Parser object and parse it.

Validator

$parser = new Parser();

$tokenObject = $parser->parse($tokenReceived);

$validator = new Validator($tokenObject, 100); // allow for a time shift of 100

You can use the Phalcon\Encryption\Security\JWT\Validator object to validate each claim by calling the validate* methods with the necessary parameters (taken from the Phalcon\Encryption\Security\Token\Token). The internal errors array in the Phalcon\Encryption\Security\JWT\Validator will be populated accordingly, returning the results with the getErrors() method.

Métodos

public function __construct(Token $token, int $timeShift = 0)

Constructor

public function get(string $claim): mixed | null

Returns a claim’s value - null if the claim does not exist

public function set(string $claim, mixed $value): Validator

Sets a claim and its value

public function setToken(Token $token): Validator

Establece el objeto token.

public function validateAudience(array|string $audience): Validator

Valida la audiencia. If it is not included in the token’s aud, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function validateExpiration(int $timestamp): Validator

Valida el tiempo de expiración. If the exp value stored in the token is greater than now, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function validateId(string $id): Validator

Valida el id. If it is not the same as the jti value stored in the token, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function validateIssuedAt(int $timestamp): Validator

Valida el emitido a la hora. If the iat value stored in the token is greater than now, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function validateIssuer(string $issuer): Validator

Valida el emisor. If it is not the same as the iss value stored in the token, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function validateNotBefore(int $timestamp): Validator

Valida el tiempo no anterior. If the nbf value stored in the token is greater than now, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

public function validateSignature(SignerInterface $signer, string $passphrase): Validator

Valida al firma del token. If the signature is not valid, a Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException will be thrown.

Ejemplo

<?php

use Phalcon\Encryption\Security\JWT\Signer\Hmac;
use Phalcon\Encryption\Security\JWT\Token\Parser;
use Phalcon\Encryption\Security\JWT\Validator;

// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiIsImN0eSI6ImFwcGxpY2F0aW9uXC9qc29uIn0.
// eyJhdWQiOlsiaHR0cHM6XC9cL3RhcmdldC5waGFsY29uLmlvIl0sImV4cCI6MTYxNDE4NTkxN
// ywianRpIjoiYWJjZDEyMzQ1Njc4OSIsImlhdCI6MTYxNDA5OTUxNywiaXNzIjoiaHR0cHM6XC
// 9cL3BoYWxjb24uaW8iLCJuYmYiOjE2MTQwOTk0NTcsInN1YiI6Im15IHN1YmplY3QgZm9yIHR
// oaXMgY2xhaW0ifQ.
// LdYevRZaQDZ2lul4CCQ5DymeP2ubcapTtgeezOZGIq7Meu7rFF1pv32b-AMWOxCS63CQz_jpm
// BPlPyOeEAkMbg

$tokenReceived = getMyTokenFromTheApplication();
$audience      = 'https://target.phalcon.io';
$now           = new DateTimeImmutable();
$issued        = $now->getTimestamp();
$notBefore     = $now->modify('-1 minute')->getTimestamp();
$expires       = $now->getTimestamp();
$id            = 'abcd123456789';
$issuer        = 'https://phalcon.io';

// 'sha512'
$signer     = new Hmac();
$passphrase = 'QcMpZ&b&mo3TPsPk668J6QH8JA$&U&m2';

$parser      = new Parser();

// Phalcon\Encryption\Security\JWT\Token\Token 
$tokenObject = $parser->parse($tokenReceived);

// Phalcon\Encryption\Security\JWT\Validator 
$validator = new Validator($tokenObject, 100); // allow for a time shift of 100

$validator
    ->validateAudience($audience)
    ->validateExpiration($expires)
    ->validateId($id)
    ->validateIssuedAt($issued)
    ->validateIssuer($issuer)
    ->validateNotBefore($notBefore)
    ->validateSignature($signer, $passphrase)
;

var_dump($validator->getErrors());

Token

As an alternative, you can verify() and validate() your token using the relevant methods in the Phalcon\Encryption\Security\Token\Token object.

Métodos

public function validate(Validator $validator): array

Validate the token claims. The validators that are executed are:

  • validateAudience()
  • validateExpiration()
  • validateId()
  • validateIssuedAt()
  • validateIssuer()
  • validateNotBefore()

You can extend the Phalcon\Encryption\Security\JWT\Validator and Phalcon\Encryption\Security\Token\Token objects to include more validators and execute them (as seen below).

public function verify(SignerInterface $signer, string $key): bool

Verify the signature of the token

Ejemplo

<?php

use Phalcon\Encryption\Security\JWT\Enum;
use Phalcon\Encryption\Security\JWT\Signer\Hmac;
use Phalcon\Encryption\Security\JWT\Token\Parser;
use Phalcon\Encryption\Security\JWT\Validator;

// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiIsImN0eSI6ImFwcGxpY2F0aW9uXC9qc29uIn0.
// eyJhdWQiOlsiaHR0cHM6XC9cL3RhcmdldC5waGFsY29uLmlvIl0sImV4cCI6MTYxNDE4NTkxN
// ywianRpIjoiYWJjZDEyMzQ1Njc4OSIsImlhdCI6MTYxNDA5OTUxNywiaXNzIjoiaHR0cHM6XC
// 9cL3BoYWxjb24uaW8iLCJuYmYiOjE2MTQwOTk0NTcsInN1YiI6Im15IHN1YmplY3QgZm9yIHR
// oaXMgY2xhaW0ifQ.
// LdYevRZaQDZ2lul4CCQ5DymeP2ubcapTtgeezOZGIq7Meu7rFF1pv32b-AMWOxCS63CQz_jpm
// BPlPyOeEAkMbg

$tokenReceived = getMyTokenFromTheApplication();
$subject       = 'Mary had a little lamb';
$audience      = 'https://target.phalcon.io';
$now           = new DateTimeImmutable();
$issued        = $now->getTimestamp();
$notBefore     = $now->modify('-1 minute')->getTimestamp();
$expires       = $now->getTimestamp();
$id            = 'abcd123456789';
$issuer        = 'https://phalcon.io';

// 'sha512'
$signer     = new Hmac();
$passphrase = 'QcMpZ&b&mo3TPsPk668J6QH8JA$&U&m2';

$parser      = new Parser();

// Phalcon\Encryption\Security\JWT\Token\Token 
$tokenObject = $parser->parse($tokenReceived);

// Phalcon\Encryption\Security\JWT\Validator 
$validator = new Validator($tokenObject, 100); // allow for a time shift of 100

$validator
    ->set(Enum::AUDIENCE, $audience)
    ->set(Enum::EXPIRATION_TIME, $expiry)
    ->set(Enum::ISSUER, $issuer)
    ->set(Enum::ISSUED_AT, $issued)
    ->set(Enum::ID, $id)
    ->set(Enum::NOT_BEFORE, $notBefore)
    ->set(Enum::SUBJECT, $subject)
;

$tokenObject->verify($signer, $passphrase);
$errors = $tokenObject->validate($validator);

var_dump($errors);

Excepciones

Any exceptions thrown in the Security component will be of the namespace Phalcon\Encryption\Security\JWT\*. Puede usar esta excepción para capturar selectivamente sólo las excepciones lanzadas desde este componente. Hay dos excepciones lanzadas. First if you supply the wrong algoritm string when instantiating the Phalcon\Encryption\Security\JWT\Signer\Hmac component. This exception is Phalcon\Encryption\Security\JWT\Exceptions\UnsupportedAlgorithmException.

La segunda excepción se lanza cuando se valida un JWT. This exception is Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException.

<?php

use Phalcon\Mvc\Controller;
use Phalcon\Encryption\Security\JWT\Builder;
use Phalcon\Encryption\Security\JWT\Exceptions\ValidatorException;
use Phalcon\Encryption\Security\JWT\Signer\Hmac;
use Phalcon\Encryption\Security\JWT\Validator;

class IndexController extends Controller
{
    public function index()
    {
        try {
            $signer     = new Hmac();
            $builder    = new Builder($signer);
            $expiry     = strtotime('+1 day');
            $issued     = strtotime('now') + 100;
            $notBefore  = strtotime('-1 day');
            $passphrase = '&vsJBETaizP3A3VX&TPMJUqi48fJEgN7';

            return $builder
                ->setAudience('my-audience')
                ->setExpirationTime($expiry)
                ->setIssuer('Phalcon JWT')
                ->setIssuedAt($issued)
                ->setId('PH-JWT')
                ->setNotBefore($notBefore)
                ->setSubject('Mary had a little lamb')
                ->setPassphrase($passphrase)
                ->getToken()
            ;

            $validator = new Validator($token);
            $validator->validateAudience("unknown");
        } catch (Exception $ex) {
            echo $ex->getMessage(); // Validation: audience not allowed
        }
    }
}