Skip to content
Open
16 changes: 15 additions & 1 deletion htdocs/web_portal/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
// Require GocContextPath which is used in most of the views scripts
require_once __DIR__.'/GocContextPath.php';

use org\gocdb\security\authentication\BadCredentialsException;

// Set the timezone
date_default_timezone_set("UTC");

Expand Down Expand Up @@ -84,9 +86,21 @@ function rejectIfNotAuthenticated($message = null){
try {
Draw_Page($Page_Type);

} catch (BadCredentialsException $error) {
/**
* `show_view('error.php', ..., $rawOutput)` is not suitable here.
* - setting rawOutput to FALSE triggers another exception because it
* tries to render a pretty error in a GOCDB window, which fails because
* the user isn't authroised.
* - setting rawOutput to TRUE also isn't ideal as it displays html tags
* in the otherwise nicely formatted output.
* die-ing like this atleast gives the user a somewhat nicely formatted
* error.
*/
die($error->getMessage());
} catch (ErrorException $e) {
/* ErrorExceptions may be thrown by an invalid configuration so it is
not safe to try to give a pretty output. Set 'raw' to true. */
not safe to try to give a pretty output. Set 'rawOutput' to true. */
show_view('error.php', $e->getMessage(), NULL, TRUE);
die();
} catch(Exception $e) {
Expand Down
31 changes: 31 additions & 0 deletions lib/Authentication/AuthTokens/EOSCAAIAuthToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace org\gocdb\security\authentication;

/**
* AuthToken for use with the EOSC AAI
*
* Requires installation/config of mod_auth_openidc before use.
*
* The token is stateless because it relies on the mod_auth_openidc
* session and simply reads the attributes stored in the session.
*/
class EOSCAAIAuthToken extends OIDCAuthToken
{
public function __construct()
{
$this->acceptedIssuers = array("https://aai-demo.eosc-portal.eu/auth/realms/core");
$this->authRealm = "EOSC Proxy IdP";
$this->groupHeader = "OIDC_CLAIM_eduperson_entitlement";
$this->groupSplitChar = ',';
$this->bannedGroups = array();
$this->requiredGroups = array("urn:geant:eosc-portal.eu:res:gocdb.eosc-portal.eu");
$this->helpString = 'Please seek assistance by opening a ticket against the ' .
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$this->helpString = 'Please seek assistance by opening a ticket against the ' .
$this->helpMessage = 'Please seek assistance by opening a ticket against the ' .

helpMessage is probably more readable than helpString

'"EOSC AAI: Core Infrastructure Proxy" group in ' .
'<a href=https://eosc-helpdesk.eosc-portal.eu/>https://eosc-helpdesk.eosc-portal.eu/</a>';

if (isset($_SERVER['OIDC_access_token'])) {
$this->setTokenFromSession();
}
}
}
211 changes: 211 additions & 0 deletions lib/Authentication/AuthTokens/OIDCAuthToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
<?php

namespace org\gocdb\security\authentication;

/**
* An abstract class for the logic of integrating with IdPs via OIDC.
*
* It is expected that concrete subclasses are created for each
* new IdP GOCDB integrates with via OIDC, providing specific information
* for that IdP.
*
* Any subclass will require installation/config of mod_auth_openidc
* before use.
*
* Any subclass token is expected to be stateless because it relies on the
* mod_auth_openidc session and simply reads the attributes stored in the
* session.
*/
abstract class OIDCAuthToken implements IAuthentication
{
private $userDetails = null;
private $authorities = array();
private $principal;
protected $acceptedIssuers;
protected $authRealm;
protected $groupHeader;
protected $groupSplitChar;
protected $bannedGroups;
protected $requiredGroups;
protected $helpString;

/**
* {@see IAuthentication::eraseCredentials()}
*/
public function eraseCredentials()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this function and validate() function below left blank?

{
}

/**
* {@see IAuthentication::getAuthorities()}
*/
public function getAuthorities()
{
return $this->authorities;
}

/**
* {@see IAuthentication::getCredentials()}
* @return string An empty string as passwords are not used by this token.
*/
public function getCredentials()
{
return ""; // none used in this token, handled by IdP
}

/**
* A custom object used to store additional user details.
* Allows non-security related user information (such as email addresses,
* telephone numbers etc) to be stored in a convenient location.
* {@see IAuthentication::getDetails()}
*
* @return Object or null if not used
*/
public function getDetails()
{
return $this->userDetails;
}

/**
* {@see IAuthentication::getPrinciple()}
* @return string unique principle string of user
*/
public function getPrinciple()
{
return $this->principal;
}

/**
* {@see IAuthentication::setAuthorities($authorities)}
*/
public function setAuthorities($authorities)
{
$this->authorities = $authorities;
}

/**
* {@see IAuthentication::setDetails($userDetails)}
* @param Object $userDetails
*/
public function setDetails($userDetails)
{
$this->userDetails = $userDetails;
}

/**
* {@see IAuthentication::validate()}
*/
public function validate()
{
}

/**
* {@see IAuthentication::isPreAuthenticating()}
*/
public static function isPreAuthenticating()
{
return true;
}

/**
* Returns true, this token reads the session attributes and so
* does not need to be stateful itself.
* {@see IAuthentication::isStateless()}
*/
public static function isStateless()
{
return true;
}

/**
* Set principal/User details from the session and check group membership.
*/
protected function setTokenFromSession()
{
if (in_array($_SERVER['OIDC_CLAIM_iss'], $this->acceptedIssuers, true)) {
$this->principal = $_SERVER['REMOTE_USER'];
$this->userDetails = array(
'AuthenticationRealm' => array($this->authRealm)
);

// Check group membership is acceptable.
$this->checkBannedGroups();
$this->checkRequiredGroups();
}
}

/**
* Check the token lists all the required groups.
*/
protected function checkRequiredGroups()
{
$groupArray = explode(
$this->groupSplitChar,
$_SERVER[$this->groupHeader]
);

// Build up a list of missing groups.
$missingGoodGroups = [];
foreach ($this->requiredGroups as $group) {
if (!in_array($group, $groupArray)) {
$missingGoodGroups[] = $group;
}
}

// If the list of missing groups is not empty, reject the user.
if (!empty($missingGoodGroups)) {
$this->rejectUser(
'You are missing the following group(s):',
$missingGoodGroups
);
}
}

/**
* Check the token lists none of the banned groups.
*/
protected function checkBannedGroups()
{
$groupArray = explode(
$this->groupSplitChar,
$_SERVER[$this->groupHeader]
);

$presentBadGroups = [];
Copy link
Contributor

@rowan04 rowan04 Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this block of code start with a comment, like in the similar function checkRequiredGroups() above?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saying something like "// Build up a list of banned groups"

foreach ($this->bannedGroups as $group) {
if (in_array($group, $groupArray)) {
$presentBadGroups[] = $group;
}
}

// If the list of present bad groups is not empty, reject the user.
if (!empty($presentBadGroups)) {
$this->rejectUser(
'We do not grant access to GOCDB to members of the following group(s):',
$presentBadGroups
);
}
}

/**
* Craft a BadCredentialsException exception.
*
* Uses the given error message to provide the end user more context.
*
* @param string $errorContext Context for the error.
* @param string[] $groupArray An array of group memberships
*/
protected function rejectUser($errorContext, $groupArray)
{
// For readability, when listing groups to the user,
// start each one on a new line with a '-' character.
$prependString = '<br />- ';
$groupString = implode($prependString, $groupArray);
throw new BadCredentialsException(
null,
'You do not belong to the correct group(s) ' .
'to gain access to this site.<br /><br />' . $errorContext .
$prependString . $groupString . '<br /><br />' . $this->helpString
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is $prependString needed here, when $groupString starts with $prependString?

);
}
}