Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/config/config.local.neon.example
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ parameters:
options:
ldap:
hostname: "ldap.cuni.cz"
oauth:
cas:
baseUri: "https://idp.cuni.cz/cas/"

sis:
Expand Down
8 changes: 4 additions & 4 deletions app/config/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,10 @@ parameters:
#port: 389
#security: SSL
bindName: "cunipersonalid"
oauth:
cas:
baseUri: "https://idp.cuni.cz/cas/"
fields:
oauth:
cas:
ukco: "cunipersonalid"
email: "mail"
firstName: "givenname"
Expand Down Expand Up @@ -241,13 +241,13 @@ services:

# external login services
- App\Helpers\ExternalLogin\CAS\LDAPLoginService(%CAS.serviceId%, %CAS.options.ldap%, %CAS.fields.ldap%)
- App\Helpers\ExternalLogin\CAS\OAuthLoginService(%CAS.serviceId%, %CAS.options.oauth%, %CAS.fields.oauth%)
- App\Helpers\ExternalLogin\CAS\CASLoginService(%CAS.serviceId%, %CAS.options.cas%, %CAS.fields.cas%)
- App\Helpers\ExternalLogin\ExternalServiceAuthenticator(
@App\Model\Repository\ExternalLogins,
@App\Model\Repository\Users,
@App\Model\Repository\Logins,
@App\Helpers\ExternalLogin\CAS\LDAPLoginService,
@App\Helpers\ExternalLogin\CAS\OAuthLoginService,
@App\Helpers\ExternalLogin\CAS\CASLoginService,
)

# config objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
use App\Exceptions\CASMissingInfoException;

use App\Model\Entity\User;
use GuzzleHttp\Psr7\Request;
use Nette\InvalidArgumentException;
use Nette\Utils\Arrays;
use Nette\Utils\Json;
use Nette\Utils\JsonException;
use Tracy\ILogger;

use GuzzleHttp\Psr7\Request;
use GuzzleHttp\Client;


Expand All @@ -29,7 +30,7 @@
* This is hard to test on a local server, as the CAS will only reveal the sensitive
* personal information to computers in the CUNI network.
*/
class OAuthLoginService implements IExternalLoginService {
class CASLoginService implements IExternalLoginService {

/** @var string Unique identifier of this login service, for example "cas-uk" */
private $serviceId;
Expand All @@ -41,9 +42,9 @@ class OAuthLoginService implements IExternalLoginService {
public function getServiceId(): string { return $this->serviceId; }

/**
* @return string The OAuth authentication
* @return string The CAS authentication
*/
public function getType(): string { return "oauth"; }
public function getType(): string { return "cas"; }

/** @var string Name of JSON field containing user's UKCO */
private $ukcoField;
Expand All @@ -66,13 +67,18 @@ public function getType(): string { return "oauth"; }
/** @var string The base URI for the validation of login tickets */
private $casHttpBaseUri;

/**
* @var ILogger
*/
private $logger;

/**
* Constructor
* @param string $serviceId Identifier of this login service, must be unique
* @param array $options
* @param array $fields
*/
public function __construct(string $serviceId, array $options, array $fields) {
public function __construct(string $serviceId, array $options, array $fields, ILogger $logger) {
$this->serviceId = $serviceId;

// The field names of user's information stored in the CAS LDAP
Expand All @@ -85,6 +91,7 @@ public function __construct(string $serviceId, array $options, array $fields) {

// The CAS HTTP validation endpoint
$this->casHttpBaseUri = Arrays::get($options, "baseUri", "https://idp.cuni.cz/cas/");
$this->logger = $logger;
}

/**
Expand All @@ -106,6 +113,30 @@ public function getUser($credentials): UserData {
return $this->getUserData($ticket, $info);
}

/**
* Internal XML parsing routine for ticket response.
* @param string $ticket
* @param string $body String representation of the response body.
* @param string $namespace XML namespace URI, if detected.
* @return \SimpleXMLElement representing the response body.
* @throws WrongCredentialsException If the XML could not have been parsed.
*/
private function parseXMLBody(string $ticket, string $body, string $namespace = '')
{
libxml_use_internal_errors(true);
$xml = simplexml_load_string($body, 'SimpleXMLElement', 0, $namespace);
$err = libxml_get_errors();
if ($err) {
$this->logger->log("CAS Ticket validation returned following response:\n$body", ILogger::DEBUG);
foreach ($err as $e) {
// Internal XML errors are logges as warnings
$this->logger->log($e, ILogger::WARNING);
}
throw new WrongCredentialsException("The ticket '$ticket' cannot be validated as the response from the server is corrupted or incomplete.");
}
return $xml;
}

/**
* @param string $ticket
* @param string $clientUrl
Expand All @@ -121,7 +152,18 @@ private function validateTicket(string $ticket, string $clientUrl) {

if ($res->getStatusCode() === 200) { // the response should be 200 even if the ticket is invalid
try {
$data = Json::decode($res->getBody(), Json::FORCE_ARRAY);
$body = (string)$res->getBody();

// Parse XML (twice, if necessary, to get right namespace) ...
$xml = $this->parseXMLBody($ticket, $body);
$namespaces = $xml->getDocNamespaces();
if ($namespaces) {
$namespace = empty($namespaces['cas']) ? reset($namespaces) : $namespaces['cas'];
$xml = $this->parseXMLBody($ticket, $body, $namespace);
}

// A trick that utilizes JSON serialization of SimpleXML objects to convert the XML into an array.
$data = JSON::decode(JSON::encode((array)$xml), JSON::FORCE_ARRAY);
} catch (JsonException $e) {
throw new WrongCredentialsException("The ticket '$ticket' cannot be validated as the response from the server is corrupted or incomplete.");
}
Expand All @@ -141,7 +183,7 @@ private function validateTicket(string $ticket, string $clientUrl) {
private function getValidationUrl($ticket, $clientUrl) {
$service = urlencode($clientUrl);
$ticket = urlencode($ticket);
return "{$this->casHttpBaseUri}serviceValidate?service={$service}&ticket={$ticket}&format=json";
return "{$this->casHttpBaseUri}p3/serviceValidate?service={$service}&ticket={$ticket}&format=xml";
}

/**
Expand All @@ -154,8 +196,9 @@ private function getValidationUrl($ticket, $clientUrl) {
*/
private function getUserData($ticket, $data): UserData {
try {
$info = Arrays::get($data, ["serviceResponse", "authenticationSuccess", "attributes"]);
$info = Arrays::get($data, ["authenticationSuccess", "attributes"]);
} catch (InvalidArgumentException $e) {
$this->logger->log("Ticket validation did not return successful response with attributes:\n" . var_export($data, true), ILogger::ERROR);
throw new WrongCredentialsException("The ticket '$ticket' is not valid and does not belong to a CUNI student or staff or it was already used.");
}

Expand Down