Skip to content

Instantly share code, notes, and snippets.

@littlefyr
Created September 10, 2013 23:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save littlefyr/6517301 to your computer and use it in GitHub Desktop.
Save littlefyr/6517301 to your computer and use it in GitHub Desktop.
A couple sets of changes to get http://wordpress.org/plugins/saml-20-single-sign-on/developers/ working with OKTA. lib\classes\saml_client.php saml\modules\saml\lib\Auth\Source\SP.php Your mileage may vary
<?php
class SAML_Client
{
private $saml;
private $opt;
private $secretsauce;
function __construct()
{
$this->settings = new SAML_Settings();
require_once(constant('SAMLAUTH_ROOT') . '/saml/lib/_autoload.php');
if( $this->settings->get_enabled() )
{
$this->saml = new SimpleSAML_Auth_Simple((string)get_current_blog_id());
add_action('wp_authenticate',array($this,'authenticate'));
add_action('wp_logout',array($this,'logout'));
add_action('login_init',array($this,'login_page'));
}
// Hash to generate password for SAML users.
// This is never actually used by the user, but we need to know what it is, and it needs to be consistent
// WARNING: If the WP AUTH_KEY is changed, all SAML users will be unable to login! In cases where this is
// actually desired, such as an intrusion, you must delete SAML users or manually set their passwords.
// it's messy, so be careful!
$this->secretsauce = constant('AUTH_KEY');
}
/**
* Handle the login page.
*/
public function login_page()
{
$final = (isset($_REQUEST["redirect_to"])) ? $_REQUEST["redirect_to"] : site_url();
$this->authenticate($final);
}
/**
* Authenticates the user using SAML
*
* @return void
*/
public function authenticate($redirect_to = '')
{
if( isset($_GET['loggedout']) && $_GET['loggedout'] == 'true' )
{
header('Location: ' . get_option('siteurl'));
exit();
}
else
{
if (trim($redirect_to) == false) {
$redirect_to = SimpleSAML_Utilities::selfURL();
}
$this->saml->requireAuth( array('ReturnTo' => $redirect_to));
$attrs = $this->saml->getAttributes();
if(array_key_exists($this->settings->get_attribute('username'), $attrs) )
{
$username = $attrs[$this->settings->get_attribute('username')][0];
if(get_user_by('login',$username))
{
$this->simulate_signon($username, $redirect_to);
}
else
{
$this->new_user($attrs, $redirect_to);
}
}
else
{
die('A username was not provided.');
}
}
}
/**
* Sends the user to the SAML Logout URL (using SLO if available) and then redirects to the site homepage
*
* @return void
*/
public function logout()
{
$this->saml->logout( get_option('siteurl') );
}
/**
* Creates a new user in the WordPress database using attributes from the IdP
*
* @param array $attrs The array of attributes created by SimpleSAMLPHP
* @return void
*/
private function new_user($attrs, $redirect_to)
{
if( array_key_exists($this->settings->get_attribute('username'),$attrs) )
{
$login = (array_key_exists($this->settings->get_attribute('username'),$attrs)) ? $attrs[$this->settings->get_attribute('username')][0] : 'NULL';
$email = (array_key_exists($this->settings->get_attribute('email'),$attrs)) ? $attrs[$this->settings->get_attribute('email')][0] : '';
$first_name = (array_key_exists($this->settings->get_attribute('firstname'),$attrs)) ? $attrs[$this->settings->get_attribute('firstname')][0] : '';
$last_name = (array_key_exists($this->settings->get_attribute('lastname'),$attrs)) ? $attrs[$this->settings->get_attribute('lastname')][0] : '';
$display_name = $first_name . ' ' . $last_name;
}
else
{
die('A username was not provided.');
}
$role = $this->update_role();
if( $role !== false )
{
$user_opts = array(
'user_login' => $login ,
'user_pass' => $this->user_password($login,$this->secretsauce) ,
'user_email' => $email ,
'first_name' => $first_name ,
'last_name' => $last_name ,
'display_name' => $display_name ,
'role' => $role
);
wp_insert_user($user_opts);
$this->simulate_signon($login, $redirect_to);
}
else
{
die('The website administrator has not given you permission to log in.');
}
}
/**
* Authenticates the user with WordPress using wp_signon()
*
* @param string $username The user to log in as.
* @return void
*/
private function simulate_signon($username, $redirect_to)
{
remove_filter('wp_authenticate',array($this,'authenticate'));
$this->update_role();
$login = array(
'user_login' => $username,
'user_password' => $this->user_password($username,$this->secretsauce),
'remember' => false
);
$use_ssl = ( defined('FORCE_SSL_ADMIN') && constant('FORCE_SSL_ADMIN') === true ) ? true : '';
$result = wp_signon($login,$use_ssl);
if(is_wp_error($result))
{
echo $result->get_error_message();
exit();
}
else
{
wp_redirect($redirect_to);
// wp_redirect(get_admin_url());
exit();
}
}
/**
* Updates a user's role if their current one doesn't match the attributes provided by the IdP
*
* @return string
*/
private function update_role()
{
$attrs = $this->saml->getAttributes();
if(array_key_exists($this->settings->get_attribute('groups'), $attrs) )
{
if( in_array($this->settings->get_group('admin'),$attrs[$this->settings->get_attribute('groups')]) )
{
$role = 'administrator';
}
elseif( in_array($this->settings->get_group('editor'),$attrs[$this->settings->get_attribute('groups')]) )
{
$role = 'editor';
}
elseif( in_array($this->settings->get_group('author'),$attrs[$this->settings->get_attribute('groups')]) )
{
$role = 'author';
}
elseif( in_array($this->settings->get_group('contributor'),$attrs[$this->settings->get_attribute('groups')]) )
{
$role = 'contributor';
}
elseif( in_array($this->settings->get_group('subscriber'),$attrs[$this->settings->get_attribute('groups')]) )
{
$role = 'subscriber';
}
elseif( $this->settings->get_allow_unlisted_users() )
{
$role = 'subscriber';
}
else
{
$role = false;
}
}
else
{
$role = false;
}
$user = get_user_by('login',$attrs[$this->settings->get_attribute('username')][0]);
if($user)
{
$user->set_role($role);
}
return $role;
}
/**
* Generates a SHA-256 HMAC hash using the username and secret key
*
* @param string $value the user's username
* @param string $key a secret key
* @return string
*/
private function user_password($value,$key)
{
$hash = hash_hmac('sha256',$value,$key);
return $hash;
}
public function show_password_fields($show_password_fields) {
return false;
}
public function disable_function() {
die('Disabled');
}
} // End of Class SamlAuth
<?php
class sspmod_saml_Auth_Source_SP extends SimpleSAML_Auth_Source {
/**
* The entity ID of this SP.
*
* @var string
*/
private $entityId;
/**
* The metadata of this SP.
*
* @var SimpleSAML_Configuration.
*/
private $metadata;
/**
* The IdP the user is allowed to log into.
*
* @var string|NULL The IdP the user can log into, or NULL if the user can log into all IdPs.
*/
private $idp;
/**
* URL to discovery service.
*
* @var string|NULL
*/
private $discoURL;
/**
* Constructor for SAML SP authentication source.
*
* @param array $info Information about this authentication source.
* @param array $config Configuration.
*/
public function __construct($info, $config) {
assert('is_array($info)');
assert('is_array($config)');
/* Call the parent constructor first, as required by the interface. */
parent::__construct($info, $config);
if (!isset($config['entityID'])) {
$config['entityID'] = $this->getMetadataURL();
}
/* For compatibility with code that assumes that $metadata->getString('entityid') gives the entity id. */
$config['entityid'] = $config['entityID'];
$this->metadata = SimpleSAML_Configuration::loadFromArray($config, 'authsources[' . var_export($this->authId, TRUE) . ']');
$this->entityId = $this->metadata->getString('entityID');
$this->idp = $this->metadata->getString('idp', NULL);
$this->discoURL = $this->metadata->getString('discoURL', NULL);
if (empty($this->discoURL) && SimpleSAML_Module::isModuleEnabled('discojuice')) {
$this->discoURL = SimpleSAML_Module::getModuleURL('discojuice/central.php');
}
}
/**
* Retrieve the URL to the metadata of this SP.
*
* @return string The metadata URL.
*/
public function getMetadataURL() {
return SimpleSAML_Module::getModuleURL('saml/sp/metadata.php/' . urlencode($this->authId));
}
/**
* Retrieve the entity id of this SP.
*
* @return string The entity id of this SP.
*/
public function getEntityId() {
return $this->entityId;
}
/**
* Retrieve the metadata of this SP.
*
* @return SimpleSAML_Configuration The metadata of this SP.
*/
public function getMetadata() {
return $this->metadata;
}
/**
* Retrieve the metadata of an IdP.
*
* @param string $entityId The entity id of the IdP.
* @return SimpleSAML_Configuration The metadata of the IdP.
*/
public function getIdPMetadata($entityId) {
assert('is_string($entityId)');
if ($this->idp !== NULL && $this->idp !== $entityId) {
throw new SimpleSAML_Error_Exception('Cannot retrieve metadata for IdP ' . var_export($entityId, TRUE) .
' because it isn\'t a valid IdP for this SP.');
}
$metadataHandler = SimpleSAML_Metadata_MetaDataStorageHandler::getMetadataHandler();
/* First, look in saml20-idp-remote. */
try {
return $metadataHandler->getMetaDataConfig($entityId, 'saml20-idp-remote');
} catch (Exception $e) {
/* Metadata wasn't found. */
}
/* Not found in saml20-idp-remote, look in shib13-idp-remote. */
try {
return $metadataHandler->getMetaDataConfig($entityId, 'shib13-idp-remote');
} catch (Exception $e) {
/* Metadata wasn't found. */
}
/* Not found. */
throw new SimpleSAML_Error_Exception('Could not find the metadata of an IdP with entity ID ' . var_export($entityId, TRUE));
}
/**
* Send a SAML1 SSO request to an IdP.
*
* @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP.
* @param array $state The state array for the current authentication.
*/
private function startSSO1(SimpleSAML_Configuration $idpMetadata, array $state) {
$idpEntityId = $idpMetadata->getString('entityid');
$state['saml:idp'] = $idpEntityId;
$ar = new SimpleSAML_XML_Shib13_AuthnRequest();
$ar->setIssuer($this->entityId);
$id = SimpleSAML_Auth_State::saveState($state, 'saml:sp:sso');
$ar->setRelayState($id);
$useArtifact = $idpMetadata->getBoolean('saml1.useartifact', NULL);
if ($useArtifact === NULL) {
$useArtifact = $this->metadata->getBoolean('saml1.useartifact', FALSE);
}
if ($useArtifact) {
$shire = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId . '/artifact');
} else {
$shire = SimpleSAML_Module::getModuleURL('saml/sp/saml1-acs.php/' . $this->authId);
}
$url = $ar->createRedirect($idpEntityId, $shire);
SimpleSAML_Logger::debug('Starting SAML 1 SSO to ' . var_export($idpEntityId, TRUE) .
' from ' . var_export($this->entityId, TRUE) . '.');
SimpleSAML_Utilities::redirect($url);
}
/**
* Send a SAML2 SSO request to an IdP.
*
* @param SimpleSAML_Configuration $idpMetadata The metadata of the IdP.
* @param array $state The state array for the current authentication.
*/
private function startSSO2(SimpleSAML_Configuration $idpMetadata, array $state) {
if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] < 0) {
SimpleSAML_Auth_State::throwException($state, new SimpleSAML_Error_ProxyCountExceeded("ProxyCountExceeded"));
}
//I Added This try/catch block
try {
$ar = sspmod_saml_Message::buildAuthnRequest($this->metadata, $idpMetadata);
} catch (Exception $e) {
$RelayStateUrl = $state['SimpleSAML_Auth_Default.ReturnURL'];
$IdP =$idpMetadata->getString('SingleSignOnService');
$qry = array('query' => 'RelayState=' . urlencode($RelayStateUrl));
$redir = add_query_arg('RelayState', urlencode($RelayStateUrl), $IdP);
wp_redirect($redir);
}
$ar->setAssertionConsumerServiceURL(SimpleSAML_Module::getModuleURL('saml/sp/saml2-acs.php/' . $this->authId));
if (isset($state['SimpleSAML_Auth_Default.ReturnURL'])) {
$ar->setRelayState($state['SimpleSAML_Auth_Default.ReturnURL']);
}
if (isset($state['saml:AuthnContextClassRef'])) {
$accr = SimpleSAML_Utilities::arrayize($state['saml:AuthnContextClassRef']);
$ar->setRequestedAuthnContext(array('AuthnContextClassRef' => $accr));
}
if (isset($state['ForceAuthn'])) {
$ar->setForceAuthn((bool)$state['ForceAuthn']);
}
if (isset($state['isPassive'])) {
$ar->setIsPassive((bool)$state['isPassive']);
}
if (isset($state['saml:NameIDPolicy'])) {
if (is_string($state['saml:NameIDPolicy'])) {
$policy = array(
'Format' => (string)$state['saml:NameIDPolicy'],
'AllowCreate' => TRUE,
);
} elseif (is_array($state['saml:NameIDPolicy'])) {
$policy = $state['saml:NameIDPolicy'];
} else {
throw new SimpleSAML_Error_Exception('Invalid value of $state[\'saml:NameIDPolicy\'].');
}
$ar->setNameIdPolicy($policy);
}
if (isset($state['saml:IDPList'])) {
$IDPList = $state['saml:IDPList'];
} else {
$IDPList = array();
}
$ar->setIDPList(array_unique(array_merge($this->metadata->getArray('IDPList', array()),
$idpMetadata->getArray('IDPList', array()),
(array) $IDPList)));
if (isset($state['saml:ProxyCount']) && $state['saml:ProxyCount'] !== null) {
$ar->setProxyCount($state['saml:ProxyCount']);
} elseif ($idpMetadata->getInteger('ProxyCount', null) !== null) {
$ar->setProxyCount($idpMetadata->getInteger('ProxyCount', null));
} elseif ($this->metadata->getInteger('ProxyCount', null) !== null) {
$ar->setProxyCount($this->metadata->getInteger('ProxyCount', null));
}
$requesterID = array();
if (isset($state['saml:RequesterID'])) {
$requesterID = $state['saml:RequesterID'];
}
if (isset($state['core:SP'])) {
$requesterID[] = $state['core:SP'];
}
$ar->setRequesterID($requesterID);
if (isset($state['saml:Extensions'])) {
$ar->setExtensions($state['saml:Extensions']);
}
$id = SimpleSAML_Auth_State::saveState($state, 'saml:sp:sso', TRUE);
$ar->setId($id);
SimpleSAML_Logger::debug('Sending SAML 2 AuthnRequest to ' . var_export($idpMetadata->getString('entityid'), TRUE));
$b = new SAML2_HTTPRedirect();
$this->sendSAML2AuthnRequest($state, $b, $ar);
assert('FALSE');
}
/**
* Function to actually send the authentication request.
*
* This function does not return.
*
* @param array &$state The state array.
* @param SAML2_Binding $binding The binding.
* @param SAML2_AuthnRequest $ar The authentication request.
*/
public function sendSAML2AuthnRequest(array &$state, SAML2_Binding $binding, SAML2_AuthnRequest $ar) {
$binding->send($ar);
assert('FALSE');
}
/**
* Send a SSO request to an IdP.
*
* @param string $idp The entity ID of the IdP.
* @param array $state The state array for the current authentication.
*/
public function startSSO($idp, array $state) {
assert('is_string($idp)');
$idpMetadata = $this->getIdPMetadata($idp);
$type = $idpMetadata->getString('metadata-set');
switch ($type) {
case 'shib13-idp-remote':
$this->startSSO1($idpMetadata, $state);
assert('FALSE'); /* Should not return. */
case 'saml20-idp-remote':
$this->startSSO2($idpMetadata, $state);
assert('FALSE'); /* Should not return. */
default:
/* Should only be one of the known types. */
assert('FALSE');
}
}
/**
* Start an IdP discovery service operation.
*
* @param array $state The state array.
*/
private function startDisco(array $state) {
$id = SimpleSAML_Auth_State::saveState($state, 'saml:sp:sso');
$config = SimpleSAML_Configuration::getInstance();
$discoURL = $this->discoURL;
if ($discoURL === NULL) {
/* Fallback to internal discovery service. */
$discoURL = SimpleSAML_Module::getModuleURL('saml/disco.php');
}
$returnTo = SimpleSAML_Module::getModuleURL('saml/sp/discoresp.php', array('AuthID' => $id));
$params = array(
'entityID' => $this->entityId,
'return' => $returnTo,
'returnIDParam' => 'idpentityid'
);
if(isset($state['saml:IDPList'])) {
$params['IDPList'] = $state['saml:IDPList'];
}
SimpleSAML_Utilities::redirect($discoURL, $params);
}
/**
* Start login.
*
* This function saves the information about the login, and redirects to the IdP.
*
* @param array &$state Information about the current authentication.
*/
public function authenticate(&$state) {
assert('is_array($state)');
/* We are going to need the authId in order to retrieve this authentication source later. */
$state['saml:sp:AuthId'] = $this->authId;
$idp = $this->idp;
if (isset($state['saml:idp'])) {
$idp = (string)$state['saml:idp'];
}
if ($idp === NULL && isset($state['saml:IDPList']) && sizeof($state['saml:IDPList']) == 1) {
$idp = $state['saml:IDPList'][0];
}
if ($idp === NULL) {
$this->startDisco($state);
assert('FALSE');
}
$this->startSSO($idp, $state);
assert('FALSE');
}
/**
* Start a SAML 2 logout operation.
*
* @param array $state The logout state.
*/
public function startSLO2(&$state) {
assert('is_array($state)');
assert('array_key_exists("saml:logout:IdP", $state)');
assert('array_key_exists("saml:logout:NameID", $state)');
assert('array_key_exists("saml:logout:SessionIndex", $state)');
$id = SimpleSAML_Auth_State::saveState($state, 'saml:slosent');
$idp = $state['saml:logout:IdP'];
$nameId = $state['saml:logout:NameID'];
$sessionIndex = $state['saml:logout:SessionIndex'];
$idpMetadata = $this->getIdPMetadata($idp);
$endpoint = $idpMetadata->getDefaultEndpoint('SingleLogoutService', array(SAML2_Const::BINDING_HTTP_REDIRECT), FALSE);
if ($endpoint === FALSE) {
SimpleSAML_Logger::info('No logout endpoint for IdP ' . var_export($idp, TRUE) . '.');
return;
}
$lr = sspmod_saml_Message::buildLogoutRequest($this->metadata, $idpMetadata);
$lr->setNameId($nameId);
$lr->setSessionIndex($sessionIndex);
$lr->setRelayState($id);
$encryptNameId = $idpMetadata->getBoolean('nameid.encryption', NULL);
if ($encryptNameId === NULL) {
$encryptNameId = $this->metadata->getBoolean('nameid.encryption', FALSE);
}
if ($encryptNameId) {
$lr->encryptNameId(sspmod_saml_Message::getEncryptionKey($idpMetadata));
}
$b = new SAML2_HTTPRedirect();
$b->send($lr);
assert('FALSE');
}
/**
* Start logout operation.
*
* @param array $state The logout state.
*/
public function logout(&$state) {
assert('is_array($state)');
assert('array_key_exists("saml:logout:Type", $state)');
$logoutType = $state['saml:logout:Type'];
switch ($logoutType) {
case 'saml1':
/* Nothing to do. */
return;
case 'saml2':
$this->startSLO2($state);
return;
default:
/* Should never happen. */
assert('FALSE');
}
}
/**
* Handle a response from a SSO operation.
*
* @param array $state The authentication state.
* @param string $idp The entity id of the IdP.
* @param array $attributes The attributes.
*/
public function handleResponse(array $state, $idp, array $attributes) {
assert('is_string($idp)');
assert('array_key_exists("LogoutState", $state)');
assert('array_key_exists("saml:logout:Type", $state["LogoutState"])');
$idpMetadata = $this->getIdpMetadata($idp);
$spMetadataArray = $this->metadata->toArray();
$idpMetadataArray = $idpMetadata->toArray();
$authProcState = array(
'saml:sp:IdP' => $idp,
'saml:sp:State' => $state,
'ReturnCall' => array('sspmod_saml_Auth_Source_SP', 'onProcessingCompleted'),
'Attributes' => $attributes,
'Destination' => $spMetadataArray,
'Source' => $idpMetadataArray,
);
if (isset($state['saml:sp:NameID'])) {
$authProcState['saml:sp:NameID'] = $state['saml:sp:NameID'];
}
if (isset($state['saml:sp:SessionIndex'])) {
$authProcState['saml:sp:SessionIndex'] = $state['saml:sp:SessionIndex'];
}
$pc = new SimpleSAML_Auth_ProcessingChain($idpMetadataArray, $spMetadataArray, 'sp');
$pc->processState($authProcState);
self::onProcessingCompleted($authProcState);
}
/**
* Handle a logout request from an IdP.
*
* @param string $idpEntityId The entity ID of the IdP.
*/
public function handleLogout($idpEntityId) {
assert('is_string($idpEntityId)');
/* Call the logout callback we registered in onProcessingCompleted(). */
$this->callLogoutCallback($idpEntityId);
}
/**
* Called when we have completed the procssing chain.
*
* @param array $authProcState The processing chain state.
*/
public static function onProcessingCompleted(array $authProcState) {
assert('array_key_exists("saml:sp:IdP", $authProcState)');
assert('array_key_exists("saml:sp:State", $authProcState)');
assert('array_key_exists("Attributes", $authProcState)');
$idp = $authProcState['saml:sp:IdP'];
$state = $authProcState['saml:sp:State'];
$sourceId = $state['saml:sp:AuthId'];
$source = SimpleSAML_Auth_Source::getById($sourceId);
if ($source === NULL) {
throw new Exception('Could not find authentication source with id ' . $sourceId);
}
/* Register a callback that we can call if we receive a logout request from the IdP. */
$source->addLogoutCallback($idp, $state);
$state['Attributes'] = $authProcState['Attributes'];
if (isset($state['saml:sp:isUnsolicited']) && (bool)$state['saml:sp:isUnsolicited']) {
if (!empty($state['saml:sp:RelayState'])) {
$redirectTo = $state['saml:sp:RelayState'];
} else {
$redirectTo = $source->getMetadata()->getString('RelayState', '/');
}
SimpleSAML_Auth_Default::handleUnsolicitedAuth($sourceId, $state, $redirectTo);
}
SimpleSAML_Auth_Source::completeAuth($state);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment