Added client SSL certificate authentication as a CakePHP plugin

pull/464/head
Guilherme Capilé 2015-04-12 16:32:57 -03:00
parent c3ea6e4915
commit d0c3b44e34
5 changed files with 312 additions and 0 deletions

View File

@ -108,6 +108,12 @@ CakePlugin::load('Assets'); // having Logable
CakePlugin::load('SysLogLogable');
CakePlugin::load('UrlCache');
/**
* Uncomment the following line to enable client SSL certificate authentication.
* It's also necessary to configure the plugin for more information, please read app/Plugin/CertAuth/reame.md
*/
// CakePlugin::load('CertAuth');
/**
* You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters:
*

View File

@ -6,6 +6,7 @@ $config = array (
'level' => 'medium',
'salt' => 'Rooraenietu8Eeyo<Qu2eeNfterd-dd+',
'cipherSeed' => '',
//'auth'=>array('CertAuth.Certificate'), // additional authentication methods
),
'MISP' =>
array (
@ -48,4 +49,36 @@ $config = array (
'amount' => 5,
'expire' => 300,
),
// Uncomment the following to enable client SSL certificate authentication
/*
'CertAuth' =>
array(
'register'=>true,
'ca'=>'FIRST.Org',
'caId'=>'org',
'mapCa'=>array(
'O'=>'org',
'emailAddress'=>'email',
),
'map'=>array(
'O'=>'org',
'emailAddress'=>'email',
),
'userModel'=>'User',
'userModelKey'=>'nids_sid',
'enableSession' => true,
'syncUser'=>true,
'restApi'=>array(
'url'=>'https://www.first.org/data/members?scope=full&limit=1',
'headers'=>array(),
'param'=>array('email'=>'email'),
'map'=>array(
'uid'=>'nids_sid',
'team'=>'org',
'email'=>'email',
'pgp_public'=>'gpgkey',
),
),
),
*/
);

View File

@ -102,6 +102,13 @@ class AppController extends Controller {
throw new ForbiddenException('The authentication key provided cannot be used for syncing.');
}
}
} else if(!$this->Session->read('Auth.User')) {
// load authentication plugins from Configure::read('Security.auth')
$auth = Configure::read('Security.auth');
if($auth) {
$this->Auth->authenticate = array_merge($auth, $this->Auth->authenticate);
$this->Auth->startup($this);
}
}
// user must accept terms
//

View File

@ -0,0 +1,262 @@
<?php
/**
* Client SSL Certificate Authentication component
*
* Authorizes users based on their SSL credentials.
*
* Copyright (c) FIRST.Org, Inc. (https://first.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @author Guilherme Capilé, Tecnodesign (https://tecnodz.com)
* @copyright Copyright (c) FIRST.Org, Inc. (https://first.org)
* @link http://github.com/FIRSTdotorg/cakephp-CertAuth
* @package CertAuth.Certificate
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
App::uses('AuthComponent', 'Controller/Component');
App::uses('BaseAuthenticate', 'Controller/Component/Auth');
class CertificateAuthenticate extends BaseAuthenticate
{
/**
* Holds the certificate issuer information (available at SSL_CLIENT_I_DN)
*
* @var array
*/
protected static $ca;
/**
* Holds the certificate user information (available at SSL_CLIENT_S_DN)
*
* @var array
*/
protected static $client;
/**
* Holds the user information
*
* @var array
*/
protected static $user;
/**
* Class constructor.
*
* This should only be called once per request, so it doesn't need to store values in
* the instance. Simply checks if the certificate is valid (against configured valid issuers)
* and returns the user information encoded.
*/
public function __construct()
{
self::$ca = self::$client = false;
// this means the client certificate is valid — check nginx/apache configuration
if(isset($_SERVER['SSL_CLIENT_VERIFY']) && $_SERVER['SSL_CLIENT_VERIFY']=='SUCCESS') {
if(isset($_SERVER['SSL_CLIENT_I_DN'])) {
$CA = self::parse($_SERVER['SSL_CLIENT_I_DN'], Configure::read('CertAuth.mapCa'));
// only valid CAs, if this was configured
if($ca=Configure::read('CertAuth.ca')) {
$k = Configure::read('CertAuth.caId');
if(!$k) $k = 'CN';
$id = (isset($CA[$k]))?($CA[$k]):(false);
if(!$id) {
$CA = false;
} else if(is_array($ca)) {
if(!in_array($id, $ca)) $CA = false;
} else if($ca!=$id) {
$CA = false;
}
unset($id, $k);
}
self::$ca = $CA;
unset($CA, $ca);
}
if(self::$ca && isset($_SERVER['SSL_CLIENT_S_DN'])) {
self::$client = self::parse($_SERVER['SSL_CLIENT_S_DN'], Configure::read('CertAuth.map'));
}
}
}
/**
* Parse certificate extensions
*
* @TODO this should properly address the RFC
* @param string $s text to be parsed
* @param (optional) array $map array of mapping extension to User fields
* @return array parsed values
*/
private static function parse($s, $map=null)
{
$r=array();
foreach(preg_split('#/#', $s, null, PREG_SPLIT_NO_EMPTY) as $v) {
if($p=strpos($v, '=')) {
$k = substr($v, 0, $p);
if($map) {
if(isset($map[$k])) {
$k = $map[$k];
} else {
unset($p, $v, $k);
continue;
}
}
$r[$k] = substr($v, $p+1);
}
unset($p, $v, $k);
}
return $r;
}
// to enable stateless authentication
public function getUser(CakeRequest $request)
{
if(is_null(self::$user)) {
if(self::$client) {
self::$user = self::$client;
$sync = Configure::read('CertAuth.syncUser');
if($sync) {
self::getRestUser();
}
// find and fill user with model
$cn = Configure::read('CertAuth.userModel');
if($cn) {
$k = Configure::read('CertAuth.userModelKey');
if($k) {
$q = array($k=>self::$user[$k]);
} else {
$q = self::$user;
}
$User = ClassRegistry::init($cn);
$U = $User->find('first', array(
'conditions' => $q,
'recursive' => false
));
if($U) {
if($sync) {
$write = array();
foreach(self::$user as $k=>$v) {
if(array_key_exists($k, $U[$cn]) && trim($U[$cn][$k])!=trim($v)) {
$write[] = $k;
$U[$cn][$k] = trim($v);
}
unset($k, $v);
}
if($write && !$User->save($U[$cn], true, $write)) {
CakeLog::write('alert', 'Could not update model at database with RestAPI data.');
}
unset($write);
}
self::$user = $U[$cn];
} else if($sync) {
$User->create();
if($User->save(self::$user, true, array_keys(self::$user))) {
$U = $User->read();
self::$user = $U[$cn];
} else {
CakeLog::write('alert', 'Could not insert model at database from RestAPI data.');
}
}
unset($U, $User, $q, $k);
}
unset($cn);
}
}
if(Configure::read('CertAuth.enableSession')) {
if(!CakeSession::read(AuthComponent::$sessionKey)) {
CakeSession::write(AuthComponent::$sessionKey, self::$user);
}
} else {
AuthComponent::$sessionKey = false;
}
return self::$user;
}
// to enable stateless authentication
public function authenticate(CakeRequest $request, CakeResponse $response)
{
return self::getUser($request);
}
/**
* Fetches user information from external REST API
*
* Valid options (should be configured under CertAuth.restApi):
*
* @param (optional) array $options API configuration
* url (string) Where to fetch information from
* headers (array) list of additional headers to be used, reserved for authentication tokens
* params (array) mapping of additional params to be included at the url, uses $user values
* map (array) mapping of the return values to be added to the self::$user
* @return array updated user object
*/
public function getRestUser($options=null, $user=null)
{
if(is_null($options)) {
$options = Configure::read('CertAuth.restApi');
}
if(!is_null($user)) {
self::$user = $user;
}
if(!isset($options['url'])) {
return null;
}
// Create a stream
$req = array(
'http'=>array(
'method'=>'GET',
'header'=>"Accept: application/json\r\n"
),
);
if(isset($options['headers'])) {
foreach($options['headers'] as $k=>$v) {
if(is_int($k)) {
$req['header'] .= "{$v}\r\n";
} else {
$req['header'] .= "{$k}: {$v}\r\n";
}
unset($k, $v);
}
}
$url = $options['url'];
if(isset($options['param'])) {
foreach($options['param'] as $k=>$v) {
if(isset(self::$user[$v])) {
$url .= ((strpos($url, '?'))?('&'):('?'))
. $k . '=' . urlencode(self::$user[$v]);
}
unset($k, $v);
}
}
$ctx = stream_context_create($req);
$a = file_get_contents($url, false, $ctx);
if(!$a) return null;
$A = json_decode($a, true);
if(!isset($A['data'][0])) return false;
if(isset($options['map'])) {
foreach($options['map'] as $k=>$v) {
if(isset($A['data'][0][$k])) {
self::$user[$v] = $A['data'][0][$k];
}
unset($k, $v);
}
}
return self::$user;
}
}

View File

@ -0,0 +1,4 @@
#Client SSL Certificate Authentication for CakePHP
This plugin enables CakePHP applications to use client SSL certificates to stateless authenticate its users. It reads information from the client certificate and can synchronize data with a foreign REST API and the client User model.