Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GT-56: Add base class for OIDC AuthTokens, add EOSCAAI AuthToken #430

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
14 changes: 14 additions & 0 deletions 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,6 +86,18 @@ function rejectIfNotAuthenticated($message = null){
try {
Draw_Page($Page_Type);

} catch (BadCredentialsException $error) {
/**
* `show_view('error.php', ...` is not suitable here.
* - setting raw to FALSE triggers another exception because it tries
gregcorbett marked this conversation as resolved.
Show resolved Hide resolved
* to render a pretty error in a GOCDB window, which fails because the
* user isn't authroised.
* - setting raw 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 somewhart 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. */
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();
}
}
}
208 changes: 208 additions & 0 deletions lib/Authentication/AuthTokens/OIDCAuthToken.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
<?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
tofu-rocketry marked this conversation as resolved.
Show resolved Hide resolved
{
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
Member

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 non of the banned groups.
gregcorbett marked this conversation as resolved.
Show resolved Hide resolved
*/
protected function checkBannedGroups()
{
$groupArray = explode($this->groupSplitChar, $_SERVER[$this->groupHeader]);
gregcorbett marked this conversation as resolved.
Show resolved Hide resolved

$presentBadGroups = [];
Copy link
Member

@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
Member

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
Member

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?

);
}
}