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 composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"simplesamlphp/assert": "^1.9",
"simplesamlphp/composer-module-installer": "^1.5",
"simplesamlphp/simplesamlphp": "^2.5@dev",
"simplesamlphp/simplesamlphp-module-ldap": "^1.2",
"simplesamlphp/simplesamlphp-module-ldap": "^v2.5.0",
"simplesamlphp/xml-cas-module-slate": "^1.2",
"simplesamlphp/xml-cas": "^2.3",
"simplesamlphp/xml-common": "^2.5",
Expand Down
104 changes: 82 additions & 22 deletions docs/cas.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,84 @@
# Using the CAS authentication source with SimpleSAMLphp

This is completely based on the original cas authentication,
the only difference is this is authentication module and not a script.
This is completely based on the original CAS authentication;
the only difference is this is an authentication module, not a script.

## Setting up the CAS authentication module

Adding an authentication source
### Adding an authentication source

In new deployments using ldap v2.5+, configure LDAP as a separate authsource in the ldap module and reference it by id from CAS.

Example authsource.php:

```php
'example-cas' => [
'cas:CAS',
'cas' => [
'login' => 'https://cas.example.com/login',
'validate' => 'https://cas.example.com/validate',
'logout' => 'https://cas.example.com/logout'
'login' => 'https://cas.example.com/login',
'validate' => 'https://cas.example.com/validate', // CAS v2
'logout' => 'https://cas.example.com/logout',
],
'ldap' => [
'authsource' => 'ldap-backend',
],
],

// LDAP authsource (dnpattern mode)
'ldap-backend' => [
'ldap:Ldap',

// REQUIRED in v2.5: one or more LDAP URLs
'connection_string' => 'ldaps://ldap.example.com',

// Optional extras
'encryption' => 'ssl',
'version' => 3,
'options' => [
'network_timeout' => 3,
'referrals' => false,
],

// Dnpattern mode (no search)
'dnpattern' => 'uid=%username%,cn=people,dc=example,dc=com',
'search.enable' => false,

// 'attributes' => ['uid', 'cn', 'mail'],
]
```

OR:

```php
'example-cas' => [
'cas:CAS',
'cas' => [
'login' => 'https://cas.example.com/login',
'serviceValidate' => 'https://cas.example.com/serviceValidate', // CAS v3
'logout' => 'https://cas.example.com/logout',
],
'ldap' => [
'servers' => 'ldaps://ldaps.example.be:636/',
'enable_tls' => true,
'searchbase' => 'ou=people,dc=org,dc=com',
'searchattributes' => 'uid',
'attributes' => ['uid','cn'],
'priv_user_dn' => 'cn=simplesamlphp,ou=applications,dc=org,dc=com',
'priv_user_pw' => 'password',
'authsource' => 'ldap-backend',
],
],

// LDAP authsource (search mode)
'ldap-backend' => [
'ldap:Ldap',
'connection_string' => 'ldaps://ldap1.example.com ldaps://ldap2.example.com',
'search' => [
'username' => 'cn=simplesamlphp,ou=apps,dc=example,dc=com',
'password' => 'secret',
'base' => ['ou=people,dc=example,dc=com'],
'filter' => '(uid=%username%)',
'scope' => 'sub',
],
'attributes' => ['*'],
'attributes.binary' => ['jpegPhoto'],
'timeout' => 3,
'options' => [
'network_timeout' => 3,
'referrals' => false,
],
],
```
Expand All @@ -39,7 +93,7 @@ To get them, call `serviceValidate`, either directly:

```php
'cas' => [
'serviceValidate' => 'https://cas.example.com/serviceValidate',
'serviceValidate' => 'https://cas.example.com/serviceValidate', // CAS v3
]
```

Expand All @@ -62,18 +116,18 @@ You can opt in to Slate support:
'serviceValidate' => 'https://cas.example.com/p3/serviceValidate',
// Enable Slate support (optional)
'slate.enabled' => true,

// Optional XPath-based attribute mappings
'attributes' => [
// Standard CAS attributes
'uid' => 'cas:user',
'mail' => 'cas:attributes/cas:mail',
'uid' => 'cas:user',
'mail' => 'cas:attributes/cas:mail',

// Slate namespaced attributes inside cas:attributes
'slate_person' => 'cas:attributes/slate:person',
'slate_round' => 'cas:attributes/slate:round',
'slate_ref' => 'cas:attributes/slate:ref',

// Some deployments also place vendor elements at the top level
'slate_person_top' => '/cas:serviceResponse/cas:authenticationSuccess/slate:person',
],
Expand Down Expand Up @@ -105,10 +159,10 @@ for each value:
```php
'cas' => [
'attributes' => [
'uid' => 'cas:user',
'sn' => 'cas:attributes/cas:sn',
'uid' => 'cas:user',
'sn' => 'cas:attributes/cas:sn',
'givenName' => 'cas:attributes/cas:firstname',
'mail' => 'cas:attributes/cas:mail',
'mail' => 'cas:attributes/cas:mail',
],
],
```
Expand All @@ -131,3 +185,9 @@ set `ldap` to `null`:
'ldap' => null,
]
```

### Troubleshooting

- Mismatch between validate (v2) and serviceValidate (v3): ensure you use the correct endpoint for your CAS server.
- Attribute mappings: verify XPath keys match your CAS response (case‑sensitive).
- LDAP connection issues: confirm connection_string, credentials, and base DN; consider increasing `network_timeout` while testing.
58 changes: 34 additions & 24 deletions src/Auth/Source/CAS.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use SimpleSAML\Configuration;
use SimpleSAML\Logger;
use SimpleSAML\Module;
use SimpleSAML\Module\ldap\Auth\Ldap;
use SimpleSAML\Module\ldap\Auth\Source\Ldap;
use SimpleSAML\Slate\XML\AuthenticationSuccess as SlateAuthnSuccess;
use SimpleSAML\Slate\XML\ServiceResponse as SlateServiceResponse;
use SimpleSAML\Utils;
Expand All @@ -28,7 +28,6 @@
use function preg_split;
use function strcmp;
use function strval;
use function var_export;

/**
* Authenticate using CAS.
Expand Down Expand Up @@ -240,13 +239,13 @@ private function casServiceValidate(string $ticket, string $service): array
// array is empty or not set then an empty array will be returned.
$attributesFromQueryConfiguration = $this->parseQueryAttributes($dom);
if (!empty($attributesFromQueryConfiguration)) {
// Overwrite attributes from parseAuthenticationSuccess with configured
// XPath-based attributes, instead of combining them.
// Overwrite attributes from parseAuthenticationSuccess with configured
// XPath-based attributes, instead of combining them.
foreach ($attributesFromQueryConfiguration as $name => $values) {
// Ensure a clean, unique list of string values
// Ensure a clean, unique list of string values
$values = array_values(array_unique(array_map('strval', $values)));

// Configuration wins: replace any existing attribute with the same name
// Configuration wins: replace any existing attribute with the same name
$attributes[$name] = $values;
}
}
Expand Down Expand Up @@ -281,35 +280,46 @@ protected function casValidation(string $ticket, string $service): array

/**
* Called by linkback, to finish validate/ finish logging in.
*
* @param array<mixed> $state
*/

public function finalStep(array &$state): void
{
$ticket = $state['cas:ticket'];
$stateId = Auth\State::saveState($state, self::STAGE_INIT);
$service = Module::getModuleURL('cas/linkback.php', ['stateId' => $stateId]);
list($username, $casAttributes) = $this->casValidation($ticket, $service);

[$username, $casAttributes] = $this->casValidation($ticket, $service);

$ldapAttributes = [];

$config = Configuration::loadFromArray(
$this->ldapConfig,
'Authentication source ' . var_export($this->authId, true),
);
if (!empty($this->ldapConfig['servers'])) {
$ldap = new Ldap(
$config->getString('servers'),
$config->getOptionalBoolean('enable_tls', false),
$config->getOptionalBoolean('debug', false),
$config->getOptionalInteger('timeout', 0),
$config->getOptionalInteger('port', 389),
$config->getOptionalBoolean('referrals', true),
);

$ldapAttributes = $ldap->validate($this->ldapConfig, $username);
if ($ldapAttributes === false) {
throw new Exception("Failed to authenticate against LDAP-server.");
// Expect $this->ldapConfig to contain an 'authsource' key when LDAP is desired
$backendId = $this->ldapConfig['authsource'] ?? null;

if ($backendId !== null) {
/** @var \SimpleSAML\Auth\Source|null $source */
$source = Auth\Source::getById($backendId);
if ($source === null) {
throw new Exception('Could not find authentication source with id ' . $backendId);
}

// Ensure we only call getAttributes() on an LDAP authsource that supports it
if (!$source instanceof Ldap) {
throw new Exception(sprintf(
"Configured ldap.authsource '%s' is not an LDAP authsource.",
$backendId,
));
}

try {
$ldapAttributes = $source->getAttributes($username);
} catch (Exception $e) {
Logger::debug('CAS - ldap lookup failed: ' . $e->getMessage());
$ldapAttributes = [];
}
}

$attributes = array_merge_recursive($casAttributes, $ldapAttributes);
$state['Attributes'] = $attributes;
}
Expand Down
15 changes: 15 additions & 0 deletions tests/config/authsources.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,19 @@
],
'ldap' => [],
],
// LDAP backend used by CAS ldap tests
'ldap-backend' => [
'ldap:Ldap',
'connection_string' => 'ldap://ldap.invalid.example.test', // invalid host to force failure later
'search' => [
'base' => ['dc=example,dc=com'],
'filter' => '(uid=%username%)',
'scope' => 'sub',
],
// Optional:
// 'attributes' => ['*'],
// 'attributes.binary' => [],
// 'timeout' => 3,
// 'options' => ['network_timeout' => 3, 'referrals' => false],
],
];
Loading