penicylline/skype-bot-sdk

View on GitHub
src/Listener/Security.php

Summary

Maintainability
A
3 hrs
Test Coverage
<?php

namespace SkypeBot\Listener;

use SkypeBot\DataProvider\OpenIdKeysProvider;
use SkypeBot\Entity\Jwk\JsonWebKey;
use SkypeBot\Entity\Jwk\JwkInfo;
use SkypeBot\Entity\Jwk\JwkPayload;
use SkypeBot\Exception\SecurityException;
use SkypeBot\SkypeBot;

class Security
{
    const SUPPORTED_ALGORITHM = 'RS256';

    const TIME_MARGIN = 120; //margin 2 minutes

    private $keyProvider;

    /**
     * @return OpenIdKeysProvider
     */
    private function getKeyProvider()
    {
        if ($this->keyProvider === null) {
            $this->keyProvider = SkypeBot::getInstance()->getOpenIdKeysProvider();
        }
        return $this->keyProvider;
    }

    public function validateBearerHeader($header)
    {
        $bearerData = $this->extractBearerData($header);
        $tokenParts = explode('.', $bearerData);
        if (count($tokenParts) !== 3) {
            throw new SecurityException('Authenticate header is not valid format');
        }
        list($keyInfo, $payload, $signature) = $tokenParts;
        $keyInfoObj = json_decode(base64_decode($keyInfo));
        $payloadObj = json_decode(base64_decode($payload));

        if (!isset($keyInfoObj, $payloadObj)) {
            throw new SecurityException('Authenticate header key info part is not valid format');
        }
        $jwkInfo = new JwkInfo($keyInfoObj);
        $jwkPayload = new JwkPayload($payloadObj);

        //checking key valid time
        $now = time();
        if ($jwkPayload->getExpired() < $now - static::TIME_MARGIN) {
            throw new SecurityException('Token expired at: ' . date('Y-m-d H:i:s', $jwkPayload->getExpired()));
        }
        if ($jwkPayload->getNotBefore() > $now + static::TIME_MARGIN) {
            throw new SecurityException('Token cannot use before: ' . date('Y-m-d H:i:s', $jwkPayload->getNotBefore()));
        }

        //checking supported encrypt algorithm
        if ($jwkInfo->getAlgorithm() != static::SUPPORTED_ALGORITHM) {
            throw new SecurityException('Unsupported key type: ' . $jwkInfo->getAlgorithm());
        }

        //get correct public key from the list
        $signingKey = $this->getKeyProvider()->getKeyById($jwkInfo->getKeyId());
        if ($signingKey === null) {
            $this->getKeyProvider()->resyncKeys();
            $signingKey = $this->getKeyProvider()->getKeyById($jwkInfo->getKeyId());
            if ($signingKey === null) {
                throw new SecurityException('Cannot find signing key of the header');
            }
        }

        // checking public key usage - only support signature
        if ($signingKey->getPublicKeyUse() != JsonWebKey::PUBLIC_KEY_SIGNATURE) {
            throw new SecurityException('Unsupported public key usage: ' . $signingKey->getPublicKeyUse());
        }
        $pKey = $this->getPublicKey($signingKey->getCertificateChain()[0]);
        return $this->verify(
            $keyInfo,
            $payload,
            $signature,
            $pKey,
            'SHA256'
        );
    }

    private function extractBearerData($header)
    {
        if (substr($header, 0, 7) != 'Bearer ') {
            throw new SecurityException('Authorization header should start with Bearer');
        }
        return substr($header, 7);
    }

    private function base64UrlDecode($input)
    {
        return base64_decode(strtr($input, '-_', '+/'));
    }

    private function verify($keyInfo, $payload, $signature, $key, $algorithm)
    {
        $msg = $keyInfo . '.' . $payload;
        $signature = $this->base64UrlDecode($signature);
        return openssl_verify($msg, $signature, $key, $algorithm);
    }

    private function getPublicKey($cert)
    {
        $certObj = openssl_x509_read(
            sprintf(
                "-----BEGIN CERTIFICATE-----\n%s-----END CERTIFICATE-----\n",
                chunk_split($cert, 64)
            )
        );
        $pkeyObj = openssl_pkey_get_public($certObj);
        $pkeyArr = openssl_pkey_get_details($pkeyObj);
        return $pkeyArr['key'];
    }

}