Admidio / admidio

Admidio is a free open source user management system for websites of organizations and groups. The system has a flexible role model so that it’s possible to reflect the structure and permissions of your organization.
https://www.admidio.org
GNU General Public License v2.0
328 stars 129 forks source link

JWT to authenticate #443

Open ximex opened 7 years ago

ximex commented 7 years ago

Add JWT (JsonWebToken) as option to authenticate.

Example Code:

Class JWTManager

<?php
/**
 ***********************************************************************************************
 * @copyright 2004-2016 The Admidio Team
 * @see https://www.admidio.org/
 * @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only
 ***********************************************************************************************
 */

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Token;
use Lcobucci\JWT\ValidationData;
use Lcobucci\JWT\Signer\Hmac\Sha256;

/**
 * @class JWTManager
 * @brief Create and validate a JTW.
 */
class JWTManager
{
    /**
     * @param int $orgId
     * @param int $userId
     * @return string
     */
    private static function getSignKey($orgId, $userId)
    {
        global $gPrivateKey, $gDb;

        $orgKey = '';//self::getOrgSignKey($gDb, $orgId);
        $userKey = '';//self::getUserSignKey($gDb, $userId);

        return $gPrivateKey . $orgKey . $userKey;
    }

    /**
     * @param \Database $database
     * @param int       $orgId
     * @return string
     */
    private static function getOrgSignKey(\Database $database, $orgId)
    {
        $sql = 'SELECT org_sign_key
                  FROM ' . TBL_ORGANIZATIONS . '
                 WHERE org_id = ' . $orgId;
        $statement = $database->query($sql);

        return $statement->fetchColumn();
    }

    /**
     * @param \Database $database
     * @param int       $userId
     * @return string
     */
    private static function getUserSignKey(\Database $database, $userId)
    {
        $sql = 'SELECT usr_sign_key
                  FROM ' . TBL_USERS . '
                 WHERE usr_id = ' . $userId;
        $statement = $database->query($sql);

        return $statement->fetchColumn();
    }

    /**
     * @param string $signKey
     * @param string $id
     * @param int    $lifetime
     * @param string $audience
     * @param string $subject
     * @param array  $properties
     * @return string
     */
    private static function build($signKey, $id, $lifetime, $audience, $subject, array $properties = array())
    {
        $tokenBuilder = new Builder();

        foreach ($properties as $key => $value)
        {
            $tokenBuilder->set($key, $value);
        }

        $now = time();
        $tokenBuilder
            ->setId($id)
            ->setIssuer('admidio-jwt-plugin')
            ->setIssuedAt($now)
            ->setExpiration($now + $lifetime)
            ->setAudience($audience)
            ->setSubject($subject)
        ;

        $tokenBuilder->sign(new Sha256(), $signKey);

        return (string) $tokenBuilder->getToken();
    }

    /**
     * @param string $tokenString
     * @return Lcobucci\JWT\Token
     */
    private static function parse($tokenString)
    {
        $tokenParser = new Parser();
        return $tokenParser->parse((string) $tokenString);
    }

    /**
     * @param Token  $token
     * @param string $audience
     * @param string $subject
     * @return bool
     */
    private static function validate(Token $token, $audience, $subject)
    {
        $tokenValData = new ValidationData();
//        $tokenValData->setAudience($audience);
//        $tokenValData->setSubject($subject);

        return $token->validate($tokenValData);
    }

    /**
     * @param Token $token
     * @param int   $orgId
     * @param int   $userId
     * @return bool
     */
    private static function verify(Token $token, $orgId, $userId)
    {
        $signKey = self::getSignKey($orgId, $userId);

        return $token->verify(new Sha256(), $signKey);
    }

    /**
     * @param int   $orgId
     * @param int   $userId
     * @param array $properties
     * @return string
     */
    public static function create($orgId, $userId, array $properties)
    {
        $signKey = self::getSignKey($orgId, $userId);
        $id = '123456'; // TODO: Generate unique ID
        $lifetime = 3600;

        return self::build($signKey, $id, $lifetime, (string) $orgId, (string) $userId, $properties);
    }

    /**
     * @param string $tokenString
     * @return array[]
     */
    public static function properties($tokenString)
    {
        $token = self::parse($tokenString);
        $claims = $token->getClaims();
        $properties = array();

        /**
         * @var Lcobucci\JWT\Claim\Basic $claim
         */
        foreach ($claims as $key => $claim)
        {
            $properties[$key] = $claim->getValue();
        }

        return $properties;
    }

    /**
     * @param string $tokenString
     * @return bool
     */
    public static function check($tokenString)
    {
        $token = self::parse($tokenString);

        $orgId = (int) $token->getClaim('aud');
        $userId = (int) $token->getClaim('sub');

        return self::validate($token, (string) $orgId, (string) $userId) && self::verify($token, $orgId, $userId);
    }
}

Token Endpoint

<?php
/**
 ***********************************************************************************************
 * @copyright 2004-2016 The Admidio Team
 * @see https://www.admidio.org/
 * @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only
 ***********************************************************************************************
 */

$gPrivateKey = 'fra0950v2nc30';
$properties = array('admin' => true, 'roles' => array(1, 3, 6));

require_once('../../src/libs/server/autoload.php');
require_once('common.php');
require_once('jwtmanager.php');

$mode = $_GET['mode'];

if ($mode === 'create')
{
    // Initialize parameters
    $bAutoLogin = false;
    $loginname  = '';
    $password   = '';
    $organizationId = (int) $gCurrentOrganization->getValue('org_id');

    // Filter parameters
    // parameters could be from login dialog or login plugin !!!
    /**
     * @param string $prefix
     */
    function initLoginParams($prefix = '')
    {
        global $bAutoLogin, $loginname, $password, $organizationId, $gPreferences;

        $loginname = $_POST[$prefix.'usr_login_name'];
        $password  = $_POST[$prefix.'usr_password'];

        if($gPreferences['enable_auto_login'] == 1 && array_key_exists($prefix.'auto_login', $_POST) && $_POST[$prefix.'auto_login'] == 1)
        {
            $bAutoLogin = true;
        }
        // if user can choose organization then save the selection
        if(array_key_exists($prefix.'org_id', $_POST) && is_numeric($_POST[$prefix.'org_id']) && $_POST[$prefix.'org_id'] > 0)
        {
            $organizationId = (int) $_POST[$prefix.'org_id'];
        }
    }

    if(array_key_exists('usr_login_name', $_POST) && $_POST['usr_login_name'] !== '')
    {
        initLoginParams('');
    }

    if(array_key_exists('plg_usr_login_name', $_POST) && $_POST['plg_usr_login_name'] !== '')
    {
        initLoginParams('plg_');
    }

    if($loginname === '' || $password === '')
    {
        $json = array('error' => $gL10n->get('SYS_FIELD_EMPTY', $gL10n->get('SYS_USERNAME') . '/' . $gL10n->get('SYS_PASSWORD')));
        echo json_encode($json);
        exit();
    }

    // Search for username
    $sql = 'SELECT usr_id
              FROM '.TBL_USERS.'
             WHERE UPPER(usr_login_name) = UPPER(\''.$loginname.'\')';
    $userStatement = $gDb->query($sql);

    if ($userStatement->rowCount() === 0)
    {
        $json = array('error' => $gL10n->get('SYS_LOGIN_USERNAME_PASSWORD_INCORRECT'));
        echo json_encode($json);
        exit();
    }

    // if login organization is different to organization of config file then create new session variables
    if($organizationId !== (int) $gCurrentOrganization->getValue('org_id'))
    {
        // read organization of config file with their preferences
        $gCurrentOrganization->readDataById($organizationId);
        $gPreferences = $gCurrentOrganization->getPreferences();

        // read new profile field structure for this organization
        $gProfileFields->readProfileFields($organizationId);

        // save new organization id to session
        $gCurrentSession->setValue('ses_org_id', $organizationId);
        $gCurrentSession->save();
    }

    $userId = (int) $userStatement->fetchColumn();

    // create user object
    $gCurrentUser = new User($gDb, $gProfileFields, $userId);

    $checkLoginReturn = $gCurrentUser->checkLogin($password, $bAutoLogin);

    if (is_string($checkLoginReturn))
    {
        $json = array('error' => $checkLoginReturn);
        echo json_encode($json);
        exit();
    }

    $tokenString = JWTManager::create($organizationId, $userId, $properties);

    $json = array('token' => $tokenString);
    echo json_encode($json);
    exit();
}
if ($mode === 'check')
{
    $tokenString = $_POST['token'];

    $valid = JWTManager::check($tokenString);

    $json = array('valid' => $valid);
    echo json_encode($json);
    exit();
}
if ($mode === 'properties')
{
    $tokenString = $_POST['token'];

    $properties = JWTManager::properties($tokenString);

    $json = array('properties' => $properties);
    echo json_encode($json);
    exit();
}

// php -S localhost:8000

// curl -X POST --data "usr_login_name=Admin&usr_password=Admidio" http://localhost:8000/adm_program/system/token.php?mode=create
// token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhZG1pbiI6dHJ1ZSwicm9sZXMiOlsxLDMsNl0sImp0aSI6IjEyMzQ1NiIsImlzcyI6ImFkbWlkaW8tand0LXBsdWdpbiIsImlhdCI6MTQ3ODczNDI3MCwiZXhwIjoxNDc4NzM3ODcwLCJhdWQiOiIxIiwic3ViIjoiMSJ9.SFPA82Awde3OP2UkDfEW0WC2gnA2DP7D-AzEIOzeZk8
// curl -X POST --data "token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJSwicm9sZXMiOlsxLDMsNl0sImp0aSI6IjEyMzQ1NiIsImlzcyI6ImFkbWlkaW8tand0LXBsdWdpbiIsImlhdCI6MTQ3ODczNDI3MCwiZXhwIjoxNDc4NzM3ODcwLCJhdWQiOiIxIiwic3ViIjoiMSJ9.SFPA82Awde3OP2UkDfEW0WC2gnA2DP7D-AzEIOzeZk8" http://localhost:8000/adm_program/system/token.php?mode=check
// curl -X POST --data "token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJSwicm9sZXMiOlsxLDMsNl0sImp0aSI6IjEyMzQ1NiIsImlzcyI6ImFkbWlkaW8tand0LXBsdWdpbiIsImlhdCI6MTQ3ODczNDI3MCwiZXhwIjoxNDc4NzM3ODcwLCJhdWQiOiIxIiwic3ViIjoiMSJ9.SFPA82Awde3OP2UkDfEW0WC2gnA2DP7D-AzEIOzeZk8" http://localhost:8000/adm_program/system/token.php?mode=properties
--- Want to back this issue? **[Post a bounty on it!](https://www.bountysource.com/issues/39088531-jwt-to-authenticate?utm_campaign=plugin&utm_content=tracker%2F10474012&utm_medium=issues&utm_source=github)** We accept bounties via [Bountysource](https://www.bountysource.com/?utm_campaign=plugin&utm_content=tracker%2F10474012&utm_medium=issues&utm_source=github).
ximex commented 4 years ago

Maybe use PASETO instead of JWT