first commit

This commit is contained in:
2025-07-18 16:20:14 +07:00
commit 98af45c018
16382 changed files with 3148096 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
`saml:AuthnContextClassRef`
===========================
IDP-side filter for setting the `AuthnContextClassRef` element in the authentication response.
Examples
--------
'authproc.idp' => [
92 => [
'class' => 'saml:AuthnContextClassRef',
'AuthnContextClassRef' => 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport',
],
],

View File

@@ -0,0 +1,22 @@
`saml:ExpectedAuthnContextClassRef`
===================
SP side attribute filter to validate AuthnContextClassRef.
This filter checks the AuthnContextClassRef in the authentication response, and accepts or denies the access depending on the provided strength measure of authentication from IdP.
You can list the accepted authentitcation context values in the Service Provider configuration file.
If the given AuthnContextClassRef does not match any accepted value, the user will be redirected to an error page. It's useful to harmonize the SP's requested AuthnContextClassRef (another authproc filter), but you can accept more authentication strength measures than you requested for.
Examples
--------
'authproc.sp' => [
91 => [
'class' => 'saml:ExpectedAuthnContextClassRef',
'accepted' => [
'urn:oasis:names:tc:SAML:2.0:post:ac:classes:nist-800-63:3',
'urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
],
],
],

View File

@@ -0,0 +1,28 @@
`saml:PairwiseID`
===================
Filter to insert a pairwise-id that complies with the
[SAML V2.0 Subject Identifier Attributes Profile][specification].
[specification]: http://docs.oasis-open.org/security/saml-subject-id-attr/v1.0/saml-subject-id-attr-v1.0.pdf
This filter will take an attribute and a scope as input and transforms this
into a anonymized and scoped identifier that is globally unique for a given
user & service provider combination.
Note:
Since the subject-id is specified as single-value attribute, only the first
value of `identifyingAttribute` and `scopeAttribute` are considered.
Examples
--------
```php
'authproc' => [
50 => [
'class' => 'saml:PairwiseID',
'identifyingAttribute' => 'uid',
'scopeAttribute' => 'scope',
],
],
```

View File

@@ -0,0 +1,35 @@
`saml:SubjectID`
===================
Filter to insert a subject-id that complies with the
[SAML V2.0 Subject Identifier Attributes Profile][specification].
[specification]: http://docs.oasis-open.org/security/saml-subject-id-attr/v1.0/saml-subject-id-attr-v1.0.pdf
This filter will take an attribute and a scope as input and transforms this
into a scoped identifier that is globally unique for a given user.
**Note**
If privacy is of your concern, you may want to hash the unique part of the subject-id. Hashing also ensures
that the output is compliant with the specification. If you do not want to hash the unique part, you _have_
to ensure that the `identifyingAttribute` always contains a value that is in line with the specification!
If you are also worried about correlation of IDs between diffent SP's, use the PairwiseID-filter instead.
**Note**
Since the subject-id is specified as single-value attribute, only the first
value of `identifyingAttribute` and `scopeAttribute` are considered.
Examples
--------
```php
'authproc' => [
50 => [
'class' => 'saml:SubjectID',
'identifyingAttribute' => 'uid',
'scopeAttribute' => 'scope',
'hashed' => true,
],
],
```

View File

@@ -0,0 +1,73 @@
Scoped Attributes Filtering
===========================
This document describes the **FilterScopes** attribute filter in the saml module.
This filter allows a Service Provider to make sure the scopes included in the values
of certain attributes correspond to what the Identity Provider declares in its
metadata. If the IdP includes a list of scopes in the metadata, only those scopes will
be allowed. On the other hand, if no scopes are declared or the scope is not included
in the list of declared scopes, it will be matched against the domain used by the
SAML `SingleSignOnService` endpoint. This means the `example.com` scope will be
allowed in attributes received from an IdP whose `SingleSignOnService` endpoint
is located on the `example.com` top domain or any subdomain of that. Such scope will
be rejected though if the match with the IdP's endpoint does not happen at the top
level, like for example with `example.com.domain.net`.
If you are configuring the metadata of an IdP manually, remember to add an array
to it with the key `scope`, containing the list of scopes expected from that entity.
Configuration
-------------
This filter can be configured in the `config/authsources.php` file, inside the
`authproc` array of the corresponding SAML authentication source in use.
Note that this filter **can only be used with SAML authentication sources**.
Here are the options available for the filter:
`attributes`
: An array containing a list of attributes that are scoped and therefore should be evaluated.
Defaults to _eduPersonPrincipalName_ and _eduPersonScopedAffiliation_.
Examples
--------
Basic configuration:
```php
'authproc' => [
90 => [
'class' => 'saml:FilterScopes',
],
],
```
Specify `mail` and `eduPersonPrincipalName` as scoped attributes:
```php
'authproc' => [
90 => [
'class' => 'saml:FilterScopes',
'attributes' => [
'mail',
'eduPersonPrincipalName',
],
],
],
```
Specify the same attributes in OID format:
```php
'authproc' => [
90 => [
'class' => 'saml:FilterScopes',
'attributes' => [
'urn:oid:0.9.2342.19200300.100.1.3',
'urn:oid:1.3.6.1.4.1.5923.1.1.1.6',
],
],
],
```

View File

@@ -0,0 +1,123 @@
# Key rollover with SimpleSAMLphp
This document gives a quick guide to doing key rollover with a SimpleSAMLphp service provider or identity provider.
## Background
A key rollover must perform several steps so that authentication does not break while remote SPs and IdPs learn about the new certificate. If you follow this procedure there should be no need for a synchronised switchover moment. It takes the following steps, which are detailed below:
1. You generate a new keypair for your entity to use.
2. You configure both old and new key in your SimpleSAMLphp config. Your
entity publishes metadata with two certificates in it. Meanwhile it continues to sign with the old key.
3. Relying parties (remote SPs or IdPs) refresh your metadata and will hence thereby trust the two listed certs (old and new). If they do not automatically refresh metadata their config needs to be updated manually to trust both the old and new certificate.
4. Once all relying parties have updated metadata with both certificates, you remove old certificate from your SimpleSAMLphp configuration, effectively doing the actual key rollover. Your SimpleSAMLphp now signs with the new key. Everything remains working because all relying parties will trust old and new certificate.
5. Your SimpleSAMLphp now publishes metadata with only the new cert. Relying parties will refresh metadata and drop the old certificate, not trusting it anymore (or remove the old certificate from their config manually). This last step is essential to ensure that the old certificate is actually distrusted.
## The steps
### Create the new key and certificate
First you must create the new key that you are going to use.
To create a self signed certificate, you may use the following command:
```bash
cd cert
openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out new.crt -keyout new.pem
```
### Add the new key to SimpleSAMLphp
Where you add the new key depends on whether you are doing key rollover for a service provider or an identity provider.
If you are doing key rollover for a service provider, the new key must be added to `config/authsources.php`.
To do key rollover for an identity provider, you must add the new key to `metadata/saml20-idp-hosted.php`.
If you are changing the keys for both an service provider and identity provider at the same time, you must update both locations.
The new certificate, private key and private key passphrase are added to the configuration with the prefix `new_`:
When the new key is added, SimpleSAMLphp will attempt to use both the new key and the old key for decryption of messages, but only the old key will be used for signing messages.
The metadata will be updated to list the new key for signing and encryption, and the old key will no longer listed as available for encryption.
This ensures that both those entities that use your old metadata and those that use your new metadata will be able to send and receive messages from you.
**Examples**:
In `config/authsources.php`:
```php
'default-sp' => [
'saml:SP',
'privatekey' => 'old.pem',
'certificate' => 'old.crt',
// When private key is passphrase protected.
'privatekey_pass' => '<old-secret>',
'new_privatekey' => 'new.pem',
'new_certificate' => 'new.crt',
// When new private key is passphrase protected.
'new_privatekey_pass' => '<new-secret>',
],
```
In `metadata/saml20-idp-hosted.php`:
```php
$metadata['urn:x-simplesamlphp:idp'] = [
'host' => '__DEFAULT__',
'auth' => 'example-userpass',
'privatekey' => 'old.pem',
'certificate' => 'old.crt',
// When private key is passphrase protected.
'privatekey_pass' => '<old-secret>',
'new_privatekey' => 'new.pem',
'new_certificate' => 'new.crt',
// When new private key is passphrase protected.
'new_privatekey_pass' => '<new-secret>',
];
```
### Distribute your new metadata
Now, you need to make sure that all your peers are using your new metadata.
How you go about this depends on how your peers have added your metadata.
If your peers are configured to automatically fetch the metadata directly from you, all you need to do is to wait for all of them to fetch the new metadata.
If you are part of an federation, you would probably either send it to the federation operators or use a federation tool to ask for the metadata to be updated.
Once the peers are using your new metadata, they will accept messages from you signed with either your old or your new key.
If they send encrypted messages to you, they will use your new key for encryption.
### Remove the old key
Once you are certain that all your peers are using the new metadata, you must remove the old key.
Replace the existing `privatekey`, `privatekey_pass` and `certificate` values op in your configuration with values from the `new_privatekey`, `new_privatekey_pass` and `new_certificate`, and remove the latter options..
This will cause your old key to be removed from your metadata.
**Examples**:
In `config/authsources.php`:
```php
'default-sp' => [
'saml:SP',
'certificate' => 'new.crt',
'privatekey' => 'new.pem',
// When private key is passphrase protected.
'privatekey_pass' => '<new-secret>',
],
In `metadata/saml20-idp-hosted.php`:
```php
$metadata['urn:x-simplesamlphp:idp'] = [
'host' => '__DEFAULT__',
'auth' => 'example-userpass',
'certificate' => 'new.crt',
'privatekey' => 'new.pem',
// When private key is passphrase protected.
'privatekey_pass' => '<new-secret>',
];
```
### Distribute your final metadata
Now you need to update the metadata of all your peers again, so that your old signing certificate is removed.
This will cause those entities to no longer accept messages signed using your old key.

View File

@@ -0,0 +1,157 @@
# NameID generation filters
This document describes the NameID generation filters in the saml module.
## Common options
`NameQualifier`
: The NameQualifier attribute for the generated NameID.
This can be a string that is used as the value directly.
It can also be `true`, in which case we use the IdP entity ID as the NameQualifier.
If it is `false`, no NameQualifier will be included.
: The default is `false`, which means that we will not include a NameQualifier by default.
`SPNameQualifier`
: The SPNameQualifier attribute for the generated NameID.
This can be a string that is used as the value directly.
It can also be `true`, in which case we use the SP entity ID as the SPNameQualifier.
If it is `false`, no SPNameQualifier will be included.
: The default is `true`, which means that we will use the SP entity ID.
## `saml:AttributeNameID`
Uses the value of an attribute to generate a NameID.
**Options**:
`identifyingAttribute`
: The name of the attribute we should use as the unique user ID.
`Format`
: The `Format` attribute of the generated NameID.
## `saml:PersistentNameID`
Generates a persistent NameID with the format `urn:oasis:names:tc:SAML:2.0:nameid-format:persistent`.
The filter will take the user ID from the attribute described in the `identifyingAttribute` option, and hash it with the `secretsalt` from `config.php`, and the SP and IdP entity ID.
The resulting hash is sent as the persistent NameID.
**Options**:
`identifyingAttribute`
: The name of the attribute we should use as the unique user ID.
## `saml:TransientNameID`
Generates a transient NameID with the format `urn:oasis:names:tc:SAML:2.0:nameid-format:transient`.
No extra options are available for this filter.
## `saml:SQLPersistentNameID`
Generates and stores persistent NameIDs in a SQL database.
This filter generates and stores a persistent NameID in a SQL database.
To use this filter, either specify the `store` option and a database,
or configure SimpleSAMLphp to use a SQL datastore.
See the `store.type` configuration option in `config.php`.
**Options**:
`identifyingAttribute`
: The name of the attribute we should use as the unique user ID.
`allowUnspecified`
: Whether a persistent NameID should be created if the SP does not specify any NameID format in the request.
The default is `false`.
`allowDifferent`
: Whether a persistent NameID should be created if there are only other NameID formats specified in the request or the SP's metadata.
The default is `false`.
`alwaysCreate`
: Whether to ignore an explicit `AllowCreate="false"` in the authentication request's NameIDPolicy.
The default is `false`, which will only create new NameIDs when the SP specifies `AllowCreate="true"` in the authentication request.
`store`
: An array of database options passed to `\SimpleSAML\Database`, keys prefixed with `database.`.
The default is `[]`, which uses the global SQL datastore.
Setting both `allowUnspecified` and `alwaysCreate` to `true` causes `saml:SQLPersistentNameID` to behave like `saml:PersistentNameID` (and other NameID generation filters), at the expense of creating unnecessary entries in the SQL datastore.
## `saml:PersistentNameID2TargetedID`
Stores a persistent NameID in the `eduPersonTargetedID`-attribute.
This filter is not actually a NameID generation filter.
Instead, it takes a persistent NameID and adds it as an attribute in the assertion.
This can be used to set the `eduPersonTargetedID`-attribute to the same value as the persistent NameID.
**Options**:
`attribute`
: The name of the attribute we should store the result in.
The default is `eduPersonTargetedID`.
`nameId`
: Whether the generated attribute should be an saml:NameID element.
The default is `true`.
**Example**:
This example makes three NameIDs available:
'authproc' => [
1 => [
'class' => 'saml:TransientNameID',
],
2 => [
'class' => 'saml:PersistentNameID',
'identifyingAttribute' => 'eduPersonPrincipalName',
],
3 => [
'class' => 'saml:AttributeNameID',
'identifyingAttribute' => 'mail',
'Format' => 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
],
],
Storing persistent NameIDs in a SQL database:
'authproc' => [
1 => [
'class' => 'saml:TransientNameID',
],
2 => [
'class' => 'saml:SQLPersistentNameID',
'identifyingAttribute' => 'eduPersonPrincipalName',
],
],
Generating Persistent NameID and eduPersonTargetedID.
'authproc' => [
// Generate the persistent NameID.
2 => [
'class' => 'saml:PersistentNameID',
'identifyingAttribute' => 'eduPersonPrincipalName',
],
// Add the persistent to the eduPersonTargetedID attribute
60 => [
'class' => 'saml:PersistentNameID2TargetedID',
'attribute' => 'eduPersonTargetedID', // The default
'nameId' => true, // The default
],
// Use OID attribute names.
90 => [
'class' => 'core:AttributeMap',
'name2oid',
],
],
// The URN attribute NameFormat for OID attributes.
'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
'attributeencodings' => [
'urn:oid:1.3.6.1.4.1.5923.1.1.1.10' => 'raw', /* eduPersonTargetedID with oid NameFormat is a raw XML value */
],

View File

@@ -0,0 +1,67 @@
`saml:NameIDAttribute`
======================
Filter that extracts the NameID we received in the authentication response and adds it as an attribute.
Parameters
----------
`attribute`
: The name of the attribute we should create.
The default is `nameid`.
`format`
: The format string for the attribute.
The default is `%I!%S!%V`.
The format string accepts the following replacements:
* `%I`: The IdP that issued the NameID.
This will be the `NameQualifier` element of the NameID if it is present, or the entity ID of the IdP we received the response from if not.
* `%S`: The SP the NameID was issued to.
This will be the `SPNameQualifier` element of the NameID if it is present, or the entity ID of this SP otherwise.
* `%V`: The value of the NameID.
* `%F`: The format of the NameID.
* `%%`: Will be replaced with a single `%`.
Examples
--------
Minimal configuration:
'default-sp' => [
'saml:SP',
'authproc' => [
20 => 'saml:NameIDAttribute',
],
],
Custom attribute name:
'default-sp' => [
'saml:SP',
'authproc' => [
20 => [
'class' => 'saml:NameIDAttribute',
'attribute' => 'someattributename',
],
],
],
Only extract the value of the NameID.
'default-sp' => [
'saml:SP',
'authproc' => [
20 => [
'class' => 'saml:NameIDAttribute',
'format' => '%V',
],
],
],
See also
--------
* [The description of the `saml:SP` authentication source.](../saml:sp)
* [How to generate various NameIDs on the IdP.](../saml:nameid)

View File

@@ -0,0 +1,483 @@
# `saml:SP`
This authentication source is used to authenticate against SAML 2 IdPs.
## Metadata
The metadata for your SP will be available from the federation page on your SimpleSAMLphp installation.
SimpleSAMLphp supports generating metadata with the MDUI and MDRPI metadata extensions
and with entity attributes. See the documentation for those extensions for more details:
* [MDUI extension](../simplesamlphp-metadata-extensions-ui)
* [MDRPI extension](../simplesamlphp-metadata-extensions-rpi)
* [Attributes extension](../simplesamlphp-metadata-extensions-attributes)
**Parameters**:
These are parameters that can be used at runtime to control the authentication.
All these parameters override the equivalent option from the configuration.
`saml:AuthnContextClassRef`
: The AuthnContextClassRef that will be sent in the login request.
`saml:AuthnContextComparison`
: The Comparison attribute of the AuthnContext that will be sent in the login request.
This parameter won't be used unless `saml:AuthnContextClassRef` is set and contains one or more values.
Possible values:
* `SAML2\Constants::COMPARISON_EXACT` (default)
* `SAML2\Constants::COMPARISON_BETTER`
* `SAML2\Constants::COMPARISON_MINIMUM`
* `SAML2\Constants::COMPARISON_MAXIMUM`
`ForceAuthn`
: Force authentication allows you to force re-authentication of users even if the user has a SSO session at the IdP.
`saml:idp`
: The entity ID of the IdP we should send an authentication request to.
`isPassive`
: Send a passive authentication request.
`IDPList`
: List of IdP entity ids that should be sent in the AuthnRequest to the IdP in the IDPList element, part of the
Scoping element.
`saml:Extensions`
: The samlp:Extensions (an XML chunk) that will be sent in the login request.
`saml:logout:Extensions`
: The samlp:Extensions (an XML chunk) that will be sent in the logout request.
`saml:NameID`
: Add a Subject element with a NameID to the SAML AuthnRequest for the IdP.
This must be a \SAML2\XML\saml\NameID object.
`saml:NameIDPolicy`
: The format of the NameID we request from the IdP: an array in the form of
`[ 'Format' => the format, 'AllowCreate' => true or false ]`.
Set to an empty array `[]` to omit sending any specific NameIDPolicy element
in the AuthnRequest.
`saml:Audience`
: Add a Conditions element to the SAML AuthnRequest containing an
AudienceRestriction with one or more audiences.
## Authentication data
Some SAML-specific attributes are available to the application after authentication.
To retrieve these attributes, the application can use the `getAuthData()`-function from the [SP API](./simplesamlphp-sp-api).
The following attributes are available:
`saml:sp:IdP`
: The entityID of the IdP the user is authenticated against.
`saml:sp:NameID`
: The NameID the user was issued by the IdP.
This is a \SAML2\XML\saml\NameID object with the various fields from the NameID.
`saml:sp:SessionIndex`
: The SessionIndex we received from the IdP.
**Options**:
`acs.Bindings`
: List of bindings the SP should support. If it is unset, all will be added.
: Possible values:
* `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST`
* `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact`
* `urn:oasis:names:tc:SAML:2.0:profiles:holder-of-key:SSO:browser`
`assertion.encryption`
: Whether assertions received by this SP must be encrypted. The default value is `false`.
If this option is set to `true`, unencrypted assertions will be rejected.
: Note that this option can be overridden for a specific IdP in saml20-idp-remote.
`AssertionConsumerService`
: List of Assertion Consumer Services in the generated metadata.
Specified in the format detailed in the
[Metadata endpoints](./simplesamlphp-metadata-endpoints) documentation.
Note that this list is taken at face value, so it's not useful to
list anything here that the SP auth source does not actually
support (unless the URLs point externally).
`AssertionConsumerServiceIndex`
: The Assertion Consumer Service Index to be used in the AuthnRequest in place of the Assertion
Service Consumer URL.
`attributes`
: List of attributes this SP requests from the IdP.
This list will be added to the generated metadata.
: The attributes will be added without a `NameFormat` by default.
Use the `attributes.NameFormat` option to specify the `NameFormat` for the attributes.
: An associative array can be used, mixing both elements with and without keys. When a key is
specified for an element of the array, it will be used as the friendly name of the attribute
in the generated metadata.
: *Note*: This list will only be added to the metadata if the `name`-option is also specified.
`attributes.NameFormat`
: The `NameFormat` for the requested attributes.
`attributes.index`
: The `index` attribute that is set in the md:AttributeConsumingService element. Integer value that defaults to `0`.
`attributes.isDefault`
: If present, sets the `isDefault` attribute in the md:AttributeConsumingService element. Boolean value, when
unset, the attribute will be omitted.
`attributes.required`
: If you have attributes added you can here specify which should be marked as required.
: The attributes should still be present in `attributes`.
`AuthnContextClassRef`
: The SP can request authentication with one or more specific authentication context classses.
One example of usage could be if the IdP supports both username/password authentication as well as software-PKI.
Set this to a string for one class identifier or an array of requested class identifiers.
`AuthnContextComparison`
: The Comparison attribute of the AuthnContext that will be sent in the login request.
This parameter won't be used unless `saml:AuthnContextClassRef` is set and contains one or more values.
Possible values:
* `SAML2\Constants::COMPARISON_EXACT` (default)
* `SAML2\Constants::COMPARISON_BETTER`
* `SAML2\Constants::COMPARISON_MINIMUM`
* `SAML2\Constants::COMPARISON_MAXIMUM`
`authproc`
: Processing filters that should be run after SP authentication.
See the [authentication processing filter manual](simplesamlphp-authproc).
`certData`
: Base64 encoded certificate data. Can be used instead of the `certificate` option.
`certificate`
: File name of certificate for this SP. This certificate will be included in generated metadata.
`contacts`
: Specify contacts in addition to the `technical` contact configured through `config/config.php`.
: For example, specifying a support contact:
'contacts' => [
[
'contactType' => 'support',
'emailAddress' => 'support@example.org',
'givenName' => 'John',
'surName' => 'Doe',
'telephoneNumber' => '+31(0)12345678',
'company' => 'Example Inc.',
],
],
: Valid values for `contactType` are: `technical`, `support`, `administrative`, `billing` and `other`. All
fields, except `contactType` are OPTIONAL.
`description`
: A description of this SP.
Will be added to the generated metadata, in an AttributeConsumingService element.
: This option can be translated into multiple languages by specifying the value as an array of language-code to translated description:
'description' => [
'en' => 'A service',
'no' => 'En tjeneste',
],
: *Note*: For this to be added to the metadata, you must also specify the `attributes` and `name` options.
`disable_scoping`
: Whether sending of samlp:Scoping elements in authentication requests should be suppressed. The default value is `false`.
When set to `true`, no scoping elements will be sent. This does not comply with the SAML2 specification, but allows
interoperability with ADFS which [does not support Scoping elements](https://docs.microsoft.com/en-za/azure/active-directory/develop/active-directory-single-sign-on-protocol-reference#scoping).
: Note that this option also exists in the IdP remote configuration. An entry
in the IdP-remote metadata overrides this the option in the SP
configuration.
`enable_unsolicited`
: Whether this SP is willing to process unsolicited responses. The default value is `true`.
`discoURL`
: Set which IdP discovery service this SP should use.
If this is unset, the IdP discovery service specified in the global option `idpdisco.url.saml20` in `config/config.php` will be used.
If that one is also unset, the builtin default discovery service will be used.
`encryption.blacklisted-algorithms`
: Blacklisted encryption algorithms. This is an array containing the algorithm identifiers.
: Note that this option can be set for each IdP in the [IdP-remote metadata](../simplesamlphp-reference-idp-remote).
`entityID`
: The entity ID this SP should use. (Must be set or an error will be generated.)
: The entity ID must be a URI, that is unlikely to change for technical or political
reasons. We recommend it to be a domain name, like above, if your organization's main
domain is `example.org` and this SP is for the application `myapp`.
The URL does not have to resolve to actual content, it's
just an identifier. Hence you don't need to and should not change it if the actual domain
of your application changes.
: For guidance in picking an entityID, see
[InCommon's best practice](https://spaces.at.internet2.edu/display/federation/saml-metadata-entityid)
on the matter.
`ForceAuthn`
: Force authentication allows you to force re-authentication of users even if the user has a SSO session at the IdP.
`idp`
: The entity ID this SP should connect to.
: If this option is unset, an IdP discovery service page will be shown.
`IsPassive`
: IsPassive allows you to enable passive authentication by default for this SP.
`key_name`
: The name of the certificate. It is possible the IDP requires your certificate to have a name.
If provided, it will be exposed in the SAML 2.0 metadata as `KeyName` inside the `KeyDescriptor`. This also requires a certificate to be provided.
`name`
: The name of this SP.
Will be added to the generated metadata, in an AttributeConsumingService element.
: This option can be translated into multiple languages by specifying the value as an array of language-code to translated name:
'name' => [
'en' => 'A service',
'no' => 'En tjeneste',
],,
: *Note*: You must also specify at least one attribute in the `attributes` option for this element to be added to the metadata.
`nameid.encryption`
: Whether NameIDs sent from this SP should be encrypted. The default
value is `false`.
: Note that this option can be set for each IdP in the [IdP-remote metadata](../simplesamlphp-reference-idp-remote).
`NameIDFormat`
: An array of the format(s) listed in the SP metadata that this SP will accept. Example:
'NameIDFormat' => [
\SAML2\Constants::NAMEID_PERSISTENT,
\SAML2\Constants::NAMEID_TRANSIENT,
],
`NameIDPolicy`
: The format of the NameID we request from the IdP in the AuthnRequest:
an array in the form of
`[ 'Format' => the format, 'AllowCreate' => true or false ]`.
Set to an empty array `[]` to omit sending any specific NameIDPolicy element
in the AuthnRequest. When the entire option or either array key is unset,
the defaults are transient and true respectively.
`OrganizationName`, `OrganizationDisplayName`, `OrganizationURL`
: The name and URL of the organization responsible for this IdP.
You need to either specify *all three* or none of these options.
: The Name does not need to be suitable for display to end users, the DisplayName should be.
The URL is a website the user can access for more information about the organization.
: This option can be translated into multiple languages by specifying the value as an array of language-code to translated name:
'OrganizationName' => [
'en' => 'Voorbeeld Organisatie Foundation b.a.',
'nl' => 'Stichting Voorbeeld Organisatie b.a.',
],
'OrganizationDisplayName' => [
'en' => 'Example organization',
'nl' => 'Voorbeeldorganisatie',
],
'OrganizationURL' => [
'en' => 'https://example.com',
'nl' => 'https://example.com/nl',
],
`privatekey`
: File name of private key to be used for signing messages and decrypting messages from the IdP. This option is only required if you use encrypted assertions or if you enable signing of messages.
`privatekey_pass`
: The passphrase for the private key, if it is encrypted. If the private key is unencrypted, this can be left out.
`ProviderName`
: Human readable name of the local SP sent with the authentication request.
`ProtocolBinding`
: The binding that should be used for SAML2 authentication responses.
This option controls the binding that is requested through the AuthnRequest message to the IdP.
By default the HTTP-Post binding is used.
`redirect.sign`
: Whether authentication requests, logout requests and logout responses sent from this SP should be signed. The default is `false`.
If set, the `AuthnRequestsSigned` attribute of the `SPSSODescriptor` element in SAML 2.0 metadata will contain its value. This
option takes precedence over the `sign.authnrequest` option in any metadata generated for this SP.
`redirect.validate`
: Whether logout requests and logout responses received by this SP should be validated. The default is `false`.
`RegistrationInfo`
: Allows to specify information about the registrar of this SP. Please refer to the
[MDRPI extension](../simplesamlphp-metadata-extensions-rpi) document for further information.
`RelayState`
: The page the user should be redirected to after an IdP initiated SSO.
`RequestInitiation`
: Enable the [Service Provider Request Initiation Protocol](https://wiki.oasis-open.org/security/RequestInitProtProf).
To validate the `target` the `trusted.url.domains` configuration option has to be used.
`saml.SOAPClient.certificate`
: A file with a certificate *and* private key that should be used when issuing SOAP requests from this SP.
If this option isn't specified, the SP private key and certificate will be used.
: This option can also be set to `false`, in which case no client certificate will be used.
`saml.SOAPClient.privatekey_pass`
: The passphrase of the privatekey in `saml.SOAPClient.certificate`.
`saml20.hok.assertion`
: Enable support for the SAML 2.0 Holder-of-Key SSO profile.
See the documentation for the [Holder-of-Key profile](./simplesamlphp-hok-sp).
`sign.authnrequest`
: Whether to sign authentication requests sent from this SP. If set, the `AuthnRequestsSigned` attribute of the
`SPSSODescriptor` element in SAML 2.0 metadata will contain its value.
: Note that this option also exists in the IdP-remote metadata, and
any value in the IdP-remote metadata overrides the one configured
in the SP configuration.
`sign.logout`
: Whether to sign logout messages sent from this SP.
: Note that this option also exists in the IdP-remote metadata, and
any value in the IdP-remote metadata overrides the one configured
in the SP configuration.
`signature.algorithm`
: The algorithm to use when signing any message generated by this service provider. Defaults to RSA-SHA256.
: Possible values:
* `http://www.w3.org/2000/09/xmldsig#rsa-sha1`
*Note*: the use of SHA1 is **deprecated** and will be disallowed in the future.
* `http://www.w3.org/2001/04/xmldsig-more#rsa-sha256`
The default.
* `http://www.w3.org/2001/04/xmldsig-more#rsa-sha384`
* `http://www.w3.org/2001/04/xmldsig-more#rsa-sha512`
`SingleLogoutServiceBinding`
: List of SingleLogoutService bindings the SP will claim support for (can be empty).
: Possible values:
* `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect`
* `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST`
* `urn:oasis:names:tc:SAML:2.0:bindings:SOAP`
`SingleLogoutServiceLocation`
: The Single Logout Service URL published in the generated metadata.
`validate.logout`
: Whether we require signatures on logout messages sent to this SP.
: Note that this option also exists in the IdP-remote metadata, and
any value in the IdP-remote metadata overrides the one configured
in the IdP metadata.
`WantAssertionsSigned`
: Whether assertions received by this SP must be signed. The default value is `false`.
The value set for this option will be used to set the `WantAssertionsSigned` attribute of the `SPSSODescriptor` element in
the exported SAML 2.0 metadata.
**Examples**:
Here we will list some examples for this authentication source.
### Minimal
'example-minimal' => [
'saml:SP',
'entityID' => 'https://myapp.example.org',
],
### Connecting to a specific IdP
'example' => [
'saml:SP',
'entityID' => 'https://myapp.example.org',
'idp' => 'https://example.net/saml-idp',
],
### Encryption and signing
This SP will accept encrypted assertions, and will sign and validate all messages.
'example-enc' => [
'saml:SP',
'entityID' => 'https://myapp.example.org',
'certificate' => 'example.crt',
'privatekey' => 'example.key',
'privatekey_pass' => 'secretpassword',
'redirect.sign' => true,
'redirect.validate' => true,
],
### Specifying attributes and required attributes
An SP that wants eduPersonPrincipalName and mail, where eduPersonPrincipalName should be listed as required:
'example-attributes => [
'saml:SP',
'entityID' => 'https://myapp.example.org',
'name' => [ // Name required for AttributeConsumingService-element.
'en' => 'Example service',
'no' => 'Eksempeltjeneste',
],
'attributes' => [
'eduPersonPrincipalName',
'mail',
// Specify friendly names for these attributes:
'sn' => 'urn:oid:2.5.4.4',
'givenName' => 'urn:oid:2.5.4.42',
],
'attributes.required' => [
'eduPersonPrincipalName',
],
'attributes.NameFormat' => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic',
],
### Limiting supported AssertionConsumerService endpoint bindings
'example-acs-limit' => [
'saml:SP',
'entityID' => 'https://myapp.example.org',
'acs.Bindings' => [
'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST',
],
],
### Requesting a specific authentication method
$auth = new \SimpleSAML\Auth\Simple('default-sp');
$auth->login([
'saml:AuthnContextClassRef' => 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
]);
### Using samlp:Extensions
$dom = \SAML2\DOMDocumentFactory::create();
$ce = $dom->createElementNS('http://www.example.com/XFoo', 'xfoo:test', 'Test data!');
$ext[] = new \SAML2\XML\Chunk($ce);
$auth = new \SimpleSAML\Auth\Simple('default-sp');
$auth->login([
'saml:Extensions' => $ext,
]);

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
use Webmozart\Assert\Assert;
use SimpleSAML\{Configuration, Utils};
use SimpleSAML\Locale\Translate;
use SimpleSAML\Metadata\MetaDataStorageHandler;
function saml_hook_sanitycheck(array &$hookinfo): void
{
Assert::keyExists($hookinfo, 'errors');
Assert::keyExists($hookinfo, 'info');
define('MODID', '[saml] ');
$config = Configuration::getInstance();
$cryptoUtils = new Utils\Crypto();
// perform some sanity checks on the configured certificates
if ($config->getOptionalBoolean('enable.saml20-idp', false) !== false) {
$handler = MetaDataStorageHandler::getMetadataHandler();
try {
$metadata = $handler->getMetaDataCurrent('saml20-idp-hosted');
} catch (Exception $e) {
$hookinfo['errors'][] = MODID . Translate::noop('Hosted IdP metadata present');
}
if (isset($metadata)) {
$metadata_config = Configuration::loadfromArray($metadata);
$private = $cryptoUtils->loadPrivateKey($metadata_config, false);
$public = $cryptoUtils->loadPublicKey($metadata_config, false);
$matches = matchingKeyPair($public['PEM'], $private['PEM'], $private['password']);
$hookinfo[$matches ? 'info' : 'errors'][] = MODID .
Translate::noop('Matching key-pair for signing assertions');
$private = $cryptoUtils->loadPrivateKey($metadata_config, false, 'new_');
if ($private !== null) {
$public = $cryptoUtils->loadPublicKey($metadata_config, false, 'new_');
$matches = matchingKeyPair($public['PEM'], $private['PEM'], $private['password']);
$hookinfo[$matches ? 'info' : 'errors'][] = MODID .
Translate::noop('Matching key-pair for signing assertions (rollover key)');
}
}
}
if ($config->getOptionalBoolean('metadata.sign.enable', false) !== false) {
$private = $cryptoUtils->loadPrivateKey($config, true, 'metadata.sign.');
$public = $cryptoUtils->loadPublicKey($config, true, 'metadata.sign.');
$matches = matchingKeyPair($public['PEM'], $private['PEM'], $private['password']);
$hookinfo[$matches ? 'info' : 'errors'][] =
MODID . Translate::noop('Matching key-pair for signing metadata');
}
}
function matchingKeyPair(string $publicKey, string $privateKey, ?string $password = null): bool
{
return openssl_x509_check_private_key($publicKey, [$privateKey, $password]);
}

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: af\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Jou verifikasie konteks is nie deur die diens aanvaar nie. Waarskynlik te"
" swak of nie twee-faktor nie."
msgid "Wrong authentication context"
msgstr "Verkeerde verifikasie konteks"

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: ar\n"
"Language-Team: \n"
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n>=3 "
"&& n<=10 ? 3 : n>=11 && n<=99 ? 4 : 5)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"طريقة التحقق التي استخدمتها غير مقبولة لمقدم الخدمة, غالبا هي أقل من "
"متطلبات مقدم الخدمة الذي قد يطلب تحققاأكثر تعقيدا"
msgid "Wrong authentication context"
msgstr "طريقة التحقق المستخدمة غير صحيحة"

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: cs\n"
"Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Váš autentizační kontext není touto službou akceptován. Pravděpodobné "
"příliš snadné nebo nedvoufaktorové."
msgid "Wrong authentication context"
msgstr "Špatný autentizační kontext"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: da\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Din authentication context er ikke accepteret at denne tjeneste. "
"Formentlig for svag eller manglende 2-faktor login."
msgid "Wrong authentication context"
msgstr "Forkert authentication context"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: de\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Ihr Authentifizierungskontext wird von diesem Dienst nicht akzeptiert. "
"Wahrscheinlich ist er zu schwach oder nicht Zwei-Faktor."
msgid "Wrong authentication context"
msgstr "Falscher Authentifizierungskontext"

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: el\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Η μέθοδος ταυτοποίησής σας δεν είναι αποδεκτή σε αυτή την υπηρεσία. "
"Πιθανώς είναι πολύ αδύναμη, π.χ. δεν χρησιμοποιήθηκε επαλήθευση δύο "
"βημάτων."
msgid "Wrong authentication context"
msgstr "Εσφαλμένη μέθοδος ταυτοποίησης"

View File

@@ -0,0 +1,46 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: en\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too "
"weak or not two-factor."
msgstr ""
"Your authentication context is not accepted at this service. Probably too "
"weak or not two-factor."
msgid "Invalid Identity Provider"
msgstr "Invalid Identity Provider"
msgid ""
"You already have a valid session with an identity provider (<em>%IDP%</em>) "
"that is not accepted by <em>%SP%</em>. Would you like to log out from your "
"existing session and log in again with another identity provider?"
msgstr ""
"You already have a valid session with an identity provider (<em>%IDP%</em>) "
"that is not accepted by <em>%SP%</em>. Would you like to log out from your "
"existing session and log in again with another identity provider?"
msgid "Wrong authentication context"
msgstr "Wrong authentication context"
msgid "SimpleSAMLphp"
msgstr ""
msgid "Yes, continue"
msgstr ""
msgid "No, cancel"
msgstr ""

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2019-12-12 13:23+0200\n"
"PO-Revision-Date: 2019-12-12 13:23+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n"
msgid ""
"You already have a valid session with an identity provider "
"(<em>%IDP%</em>) that is not accepted by <em>%SP%</em>. Would you like to"
" log out from your existing session and log in again with another "
"identity provider?"
msgstr ""
"O se ntse o na le seshene e sebetsang ho mofani wa boitsebiso "
"(<em>%IDP%</em>) ya sa amohelweng ke<em>%SP%</em>. Na o ka lakatsa ho "
"tswa sesheneng ya hao e teng mme o kene ka mofani wa boitsebiso e mong?"
msgid "Invalid Identity Provider"
msgstr "Mofani wa Boitsebiso ha Nepahala"

View File

@@ -0,0 +1,40 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: es\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Este servicio no acepta el contexto de su autenticación. Probablemente es"
" demasiado débil o no está usando un segundo factor de autenticación."
msgid "Invalid Identity Provider"
msgstr "Proveedor de Identidad inválido"
msgid ""
"You already have a valid session with an identity provider "
"(<em>%IDP%</em>) that is not accepted by <em>%SP%</em>. Would you like to"
" log out from your existing session and log in again with another "
"identity provider?"
msgstr ""
"Ya existe una sesión válida con un proveedor de identidad "
"(<em>%IDP%</em>) que <em>%SP%</em> no acepta. ¿Desea cerrar su sesión "
"actual e iniciar una nueva con otro proveedor de identidad?"
msgid "Wrong authentication context"
msgstr "Contexto de autenticación incorrecto"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: et\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"See teenus ei aktsepteeri sinu autentimiskonteksti. Tõenäoliselt on see "
"liiga nõrk või pole kaheastmeline."
msgid "Wrong authentication context"
msgstr "Vale autentimiskontekst"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: eu\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Zerbitzu honek ez du onartzen zure kautotze testuingurua. Ziurrenik "
"ahulegia da edo ez da bi faktoredun kautotzea. "
msgid "Wrong authentication context"
msgstr "Kautotze testu inguru okerra"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: fr\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n > 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Votre contexte d'authentification n'est pas accepté pour ce service. "
"Probablement trop faible ou pas forte."
msgid "Wrong authentication context"
msgstr "Contexte d'authentification érroné"

View File

@@ -0,0 +1,29 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: hr\n"
"Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Odabrana usluga ne prihvaća vaš autentikacijski kontekst. Najvjerojatnije"
" zato što vaš autentikacijski servis ne zadovoljava razinu sigurnosti "
"koju odabrana usluga zahtijeva."
msgid "Wrong authentication context"
msgstr "Neispravan autentikacijski kontekst"

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: hu\n"
"Language-Team: \n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"A mód, ahogyan azonosított téged a személyazonosság szolgáltatód, nem "
"elfogadott ennél a szolgáltatásnál. Valószínűleg túl gyenge, vagy nem "
"kétfaktoros."
msgid "Wrong authentication context"
msgstr "Elutasított azonosítási mód"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: id\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Konteks otentikasi Anda tidak tersedia pada layanan ini. Mungkin terlalu "
"lemah atau bukan keamanan-ganda."
msgid "Wrong authentication context"
msgstr "Konteks otentikasi keliru"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: it\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Il tuo contesto di autenticazione non è accettato da questo servizio. "
"Probabilmente troppo debole o non a due fattori."
msgid "Wrong authentication context"
msgstr "Contesto di autenticazione errato"

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: lt\n"
"Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"(n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Jūsų autentikacijos kontekstas šioje paslaugoje nepriimamas. Greičiausiai"
" jis yra per silpnas arba ne dviejų faktorių."
msgid "Wrong authentication context"
msgstr "Neteisingas autentifikacijos kontekstas"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: nl\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Uw authenticatiecontext wordt niet geaccepteerd door deze dienst. "
"Wellicht is deze de zwak of niet twee-factor."
msgid "Wrong authentication context"
msgstr "Verkeerde authenticatiecontext"

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: nn\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Du prøver å logga inn med ein innloggingsmetode som ikkje er støtta i "
"tenesta. Truleg er autentiseringsnivåiet for lågt, eller tenesta krav "
"tofaktor-innlogging."
msgid "Wrong authentication context"
msgstr "Feil innloggingsmetode"

View File

@@ -0,0 +1,29 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: ro\n"
"Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100"
" < 20)) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Contextul de autentificare folosit nu este acceptat pentru acest "
"serviciu. Probabil contextul de autentificare este prea slab/simplu sau "
"nu este de tipul cu 2 factori de autentificare."
msgid "Wrong authentication context"
msgstr "Context de autentificare greșit"

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: ru\n"
"Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Ваш контекст аутентификации не принят данным сервисом. Либо слишком "
"слабый, либо не двух-факторный."
msgid "Wrong authentication context"
msgstr "Неправильный контекст аутентификации"

View File

@@ -0,0 +1,46 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: sk\n"
"Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n == 1 ? 0 : (n >= 2 && n <= 4 ? 1 : 2))\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too "
"weak or not two-factor."
msgstr ""
"Váš autentifikačný kontext nie je touto službou akceptovaný. Pravdepodobne moc "
"slabý alebo nie je dvoj-faktorový."
msgid "Invalid Identity Provider"
msgstr "Neplatný poskytovateľ identity"
msgid ""
"You already have a valid session with an identity provider (<em>%IDP%</em>) "
"that is not accepted by <em>%SP%</em>. Would you like to log out from your "
"existing session and log in again with another identity provider?"
msgstr ""
"Už máte platnú reláciu s poskytovateľom identity (<em>%IDP%</em>), "
"ktorá nie je akceptovaná službou <em>%SP%</em>. Chceli by ste sa odhlásiť zo "
"svojej existujúcej relácie a prihlásiť sa iným poskytovateľom identity?"
msgid "Wrong authentication context"
msgstr "Zlý autentifikačný kontext"
msgid "SimpleSAMLphp"
msgstr ""
msgid "Yes, continue"
msgstr ""
msgid "No, cancel"
msgstr ""

View File

@@ -0,0 +1,28 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: sr\n"
"Language-Team: \n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Vaš kontekst za autentifikaciju nije podržan od strane ovog servisa. "
"Verovatno je previše slab, ili nije autentifikacija sa dva faktora."
msgid "Wrong authentication context"
msgstr "Pogrešan kontekst za autentifikaciju"

View File

@@ -0,0 +1,27 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: sv\n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr ""
"Din autentiseringsteknik är inte godkänd av denna tjänst, troligen är den"
" för svag eller är inte en två-faktorsteknik."
msgid "Wrong authentication context"
msgstr "Felaktig autentiseringsteknik"

View File

@@ -0,0 +1,29 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2018-11-15 14:48+0200\n"
"PO-Revision-Date: 2018-11-15 14:48+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n"
msgid "Invalid Identity Provider"
msgstr "Isiboneleli Sesazisi Esingasebenziyo"
msgid ""
"You already have a valid session with an identity provider "
"(<em>%IDP%</em>) that is not accepted by <em>%SP%</em>. Would you like to"
" log out from your existing session and log in again with another "
"identity provider?"
msgstr ""
"Sele unayo iseshoni esebenzayo nomboneleli wesazisi (<em>%IDP%</em>) "
"engamkelwanga yi-<em>%SP%</em>. Ingaba ungathanda ukuphuma kwiseshoni "
"yakho esele ikho uze ungene kwakhona ngomnye umboneleli wesazisi?"

View File

@@ -0,0 +1,25 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: zh\n"
"Language-Team: \n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr "此服务不接受您的验证上下文。可能因为验证太弱或非双因素验证。"
msgid "Wrong authentication context"
msgstr "错误的验证上下文"

View File

@@ -0,0 +1,25 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2016-10-12 09:23+0200\n"
"PO-Revision-Date: 2016-10-14 12:14+0200\n"
"Last-Translator: \n"
"Language: zh_Hant_TW\n"
"Language-Team: \n"
"Plural-Forms: nplurals=1; plural=0\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 2.3.4\n"
msgid ""
"Your authentication context is not accepted at this service. Probably too"
" weak or not two-factor."
msgstr "您的驗證碼無法被接受。可能是強度太弱或是未使用兩段式驗證。"
msgid "Wrong authentication context"
msgstr "錯誤驗證碼"

View File

@@ -0,0 +1,29 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: SimpleSAMLphp 1.15\n"
"Report-Msgid-Bugs-To: simplesamlphp-translation@googlegroups.com\n"
"POT-Creation-Date: 2018-11-15 14:48+0200\n"
"PO-Revision-Date: 2018-11-15 14:48+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Generated-By: Babel 1.3\n"
msgid "Invalid Identity Provider"
msgstr "Umhlinzeki Kamazisi Ongalungile"
msgid ""
"You already have a valid session with an identity provider "
"(<em>%IDP%</em>) that is not accepted by <em>%SP%</em>. Would you like to"
" log out from your existing session and log in again with another "
"identity provider?"
msgstr ""
"Kakade uneseshini evumelekile nomhlinzeki kamazisi (<em>%IDP%</em>) "
"engamukelwa okuthi <em>%SP%</em>. Ungathanda ukuphuma kuseshini yakho "
"ekhona kakade futhi uphinde ungene ngomunye umhlinzeki kamazisi?"

View File

@@ -0,0 +1,100 @@
---
saml-proxy-invalidSession:
path: /proxy/invalidSession
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\Proxy::invalidSession'
}
methods: [GET, POST]
saml-disco:
path: /disco
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\Disco::disco'
}
methods: [GET, POST]
saml-sp-discoResponse:
path: /sp/discoResponse
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::discoResponse'
}
methods: [GET]
saml-sp-login:
path: /sp/login/{sourceId}
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::login'
}
methods: [GET]
saml-sp-wrongAuthnContextClassRef:
path: /sp/wrongAuthnContextClassRef
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::wrongAuthnContextClassRef'
}
methods: [GET]
saml-sp-assertionConsumerService:
path: /sp/saml2-acs.php/{sourceId}
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::assertionConsumerService'
}
methods: [GET, POST]
saml-sp-singleLogoutService:
path: /sp/saml2-logout.php/{sourceId}
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::singleLogoutService'
}
methods: [GET, POST]
saml-sp-metadata:
path: /sp/metadata/{sourceId}
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::metadata'
}
methods: [GET]
saml-legacy-sp-metadata:
path: /sp/metadata.php/{sourceId}
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\ServiceProvider::metadata',
path: /saml/sp/metadata, permanent: true
}
methods: [GET]
websso-single-sign-on:
path: /idp/singleSignOnService
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\WebBrowserSingleSignOn::singleSignOnService'
}
methods: [GET, POST]
websso-artifact-resolution:
path: /idp/artifactResolutionService
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\WebBrowserSingleSignOn::artifactResolutionService'
}
methods: [GET, POST]
websso-metadata:
path: /idp/metadata
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\Metadata::metadata'
}
methods: [GET]
websso-single-logout:
path: /idp/singleLogout
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\SingleLogout::singleLogout'
}
methods: [GET, POST]
websso-init-single-logout:
path: /idp/initSingleLogout
defaults: {
_controller: 'SimpleSAML\Module\saml\Controller\SingleLogout::initSingleLogout'
}
methods: [GET]

View File

@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SimpleSAML\Error;
use SimpleSAML\Logger;
use SimpleSAML\Module\saml\BaseNameIDGenerator;
/**
* Authentication processing filter to create a NameID from an attribute.
*
* @package SimpleSAMLphp
*/
class AttributeNameID extends BaseNameIDGenerator
{
/**
* The attribute we should use as the NameID.
*
* @var string
*/
private string $identifyingAttribute;
/**
* Initialize this filter, parse configuration.
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*
* @throws \SimpleSAML\Error\Exception If the required options 'Format' or 'identifyingAttribute' are missing.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
if (!isset($config['Format'])) {
throw new Error\Exception("AttributeNameID: Missing required option 'Format'.");
}
$this->format = (string) $config['Format'];
if (!isset($config['identifyingAttribute'])) {
throw new Error\Exception("AttributeNameID: Missing required option 'identifyingAttribute'.");
}
$this->identifyingAttribute = (string) $config['identifyingAttribute'];
}
/**
* Get the NameID value.
*
* @param array $state The state array.
* @return string|null The NameID value.
*/
protected function getValue(array &$state): ?string
{
if (
!isset($state['Attributes'][$this->identifyingAttribute])
|| count($state['Attributes'][$this->identifyingAttribute]) === 0
) {
Logger::warning(
'Missing attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating attribute NameID.',
);
return null;
}
if (count($state['Attributes'][$this->identifyingAttribute]) > 1) {
Logger::warning(
'More than one value in attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating attribute NameID.',
);
return null;
}
// just in case the first index is no longer 0
$value = array_values($state['Attributes'][$this->identifyingAttribute]);
$value = strval($value[0]);
if (empty($value)) {
Logger::warning(
'Empty value in attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating attribute NameID.',
);
return null;
}
return $value;
}
}

View File

@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Error;
/**
* Filter for setting the AuthnContextClassRef in the response.
*
* @package SimpleSAMLphp
*/
class AuthnContextClassRef extends ProcessingFilter
{
/**
* The URI we should set as the AuthnContextClassRef in the login response.
*
* @var string|null
*/
private ?string $authnContextClassRef = null;
/**
* Initialize this filter.
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*
* @throws \SimpleSAML\Error\Exception if the mandatory 'AuthnContextClassRef' option is missing.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
if (!isset($config['AuthnContextClassRef'])) {
throw new Error\Exception('Missing AuthnContextClassRef option in processing filter.');
}
$this->authnContextClassRef = strval($config['AuthnContextClassRef']);
}
/**
* Set the AuthnContextClassRef in the SAML 2 response.
*
* @param array &$state The state array for this request.
*/
public function process(array &$state): void
{
$state['saml:AuthnContextClassRef'] = $this->authnContextClassRef;
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Auth;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Error;
use SimpleSAML\Logger;
use SimpleSAML\Module;
use SimpleSAML\Utils;
/**
* Attribute filter to validate AuthnContextClassRef values.
*
* Example configuration:
*
* 91 => [
* 'class' => 'saml:ExpectedAuthnContextClassRef',
* 'accepted' => [
* 'urn:oasis:names:tc:SAML:2.0:post:ac:classes:nist-800-63:3',
* 'urn:oasis:names:tc:SAML:2.0:ac:classes:Password',
* ],
* ],
*
* @package SimpleSAMLphp
*/
class ExpectedAuthnContextClassRef extends ProcessingFilter
{
/**
* Array of accepted AuthnContextClassRef
* @var array
*/
private array $accepted;
/**
* AuthnContextClassRef of the assertion
* @var string|null
*/
private ?string $AuthnContextClassRef = null;
/**
* Initialize this filter, parse configuration
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*
* @throws \SimpleSAML\Error\Exception if the mandatory 'accepted' configuration option is missing.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
if (empty($config['accepted'])) {
Logger::error(
'ExpectedAuthnContextClassRef: Configuration error. There is no accepted AuthnContextClassRef.',
);
throw new Error\Exception(
'ExpectedAuthnContextClassRef: Configuration error. There is no accepted AuthnContextClassRef.',
);
}
$this->accepted = $config['accepted'];
}
/**
* @param array &$state The current request
*/
public function process(array &$state): void
{
Assert::keyExists($state, 'Attributes');
$this->AuthnContextClassRef = $state['saml:sp:State']['saml:sp:AuthnContext'];
if (!in_array($this->AuthnContextClassRef, $this->accepted, true)) {
$this->unauthorized($state);
}
}
/**
* When the process logic determines that the user is not
* authorized for this service, then forward the user to
* an 403 unauthorized page.
*
* Separated this code into its own method so that child
* classes can override it and change the action. Forward
* thinking in case a "chained" ACL is needed, more complex
* permission logic.
*
* @param array $state
*/
protected function unauthorized(array &$state): void
{
Logger::error(
'ExpectedAuthnContextClassRef: Invalid authentication context: ' . strval($this->AuthnContextClassRef) .
'. Accepted values are: ' . var_export($this->accepted, true),
);
$id = Auth\State::saveState($state, 'saml:ExpectedAuthnContextClassRef:unauthorized');
$url = Module::getModuleURL(
'saml/sp/wrongAuthnContextClassRef',
);
$httpUtils = new Utils\HTTP();
$httpUtils->redirectTrustedURL($url, ['StateId' => $id]);
}
}

View File

@@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Logger;
use SimpleSAML\Utils;
/**
* Filter to remove attribute values which are not properly scoped.
*
* @package SimpleSAMLphp
*/
class FilterScopes extends ProcessingFilter
{
/**
* @var string[] Stores any pre-configured scoped attributes which come from the filter configuration.
*/
private array $scopedAttributes = [
'eduPersonScopedAffiliation',
'eduPersonPrincipalName',
];
/**
* Constructor for the processing filter.
*
* @param array &$config Configuration for this filter.
* @param mixed $reserved For future use.
*/
public function __construct(array &$config, $reserved)
{
parent::__construct($config, $reserved);
if (array_key_exists('attributes', $config) && !empty($config['attributes'])) {
$this->scopedAttributes = $config['attributes'];
}
}
/**
* This method applies the filter, removing any values
*
* @param array &$state the current request
*/
public function process(array &$state): void
{
$src = $state['Source'];
$validScopes = [];
$host = '';
if (array_key_exists('scope', $src) && is_array($src['scope']) && !empty($src['scope'])) {
$validScopes = $src['scope'];
} else {
$ep = Utils\Config\Metadata::getDefaultEndpoint($state['Source']['SingleSignOnService']);
$host = parse_url($ep['Location'], PHP_URL_HOST) ?? '';
}
foreach ($this->scopedAttributes as $attribute) {
if (!isset($state['Attributes'][$attribute])) {
continue;
}
$values = $state['Attributes'][$attribute];
$newValues = [];
foreach ($values as $value) {
@list(, $scope) = explode('@', $value, 2);
if ($scope === null) {
$newValues[] = $value;
continue; // there's no scope
}
if (in_array($scope, $validScopes, true)) {
$newValues[] = $value;
} elseif (strpos($host, $scope) === strlen($host) - strlen($scope)) {
$newValues[] = $value;
} else {
Logger::warning("Removing value '$value' for attribute '$attribute'. Undeclared scope.");
}
}
if (empty($newValues)) {
Logger::warning("No suitable values for attribute '$attribute', removing it.");
unset($state['Attributes'][$attribute]); // remove empty attributes
} else {
$state['Attributes'][$attribute] = $newValues;
}
}
}
}

View File

@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SAML2\Constants;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Error;
/**
* Authentication processing filter to create an attribute from a NameID.
*
* @package SimpleSAMLphp
*/
class NameIDAttribute extends ProcessingFilter
{
/**
* The attribute we should save the NameID in.
*
* @var string
*/
private string $attribute;
/**
* The format of the NameID in the attribute.
*
* @var array
*/
private array $format;
/**
* Initialize this filter, parse configuration.
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
if (isset($config['attribute'])) {
$this->attribute = strval($config['attribute']);
} else {
$this->attribute = 'nameid';
}
if (isset($config['format'])) {
$format = strval($config['format']);
} else {
$format = '%I!%S!%V';
}
$this->format = self::parseFormat($format);
}
/**
* Parse a NameID format string into an array.
*
* @param string $format The format string.
* @return array The format string broken into its individual components.
*
* @throws \SimpleSAML\Error\Exception if the replacement is invalid.
*/
private static function parseFormat(string $format): array
{
$ret = [];
$pos = 0;
while (($next = strpos($format, '%', $pos)) !== false) {
$ret[] = substr($format, $pos, $next - $pos);
$replacement = $format[$next + 1];
switch ($replacement) {
case 'F':
$ret[] = 'Format';
break;
case 'I':
$ret[] = 'NameQualifier';
break;
case 'S':
$ret[] = 'SPNameQualifier';
break;
case 'V':
$ret[] = 'Value';
break;
case '%':
$ret[] = '%';
break;
default:
throw new Error\Exception('NameIDAttribute: Invalid replacement: "%' . $replacement . '"');
}
$pos = $next + 2;
}
$ret[] = substr($format, $pos);
return $ret;
}
/**
* Convert NameID to attribute.
*
* @param array &$state The request state.
*/
public function process(array &$state): void
{
Assert::keyExists($state['Source'], 'entityid');
Assert::keyExists($state['Destination'], 'entityid');
if (!isset($state['saml:sp:NameID'])) {
return;
}
$rep = $state['saml:sp:NameID'];
Assert::notNull($rep->getValue());
if ($rep->getFormat() === null) {
$rep->setFormat(Constants::NAMEID_UNSPECIFIED);
}
if ($rep->getSPNameQualifier() === null) {
$rep->setSPNameQualifier($state['Source']['entityid']);
}
if ($rep->getNameQualifier() === null) {
$rep->setNameQualifier($state['Destination']['entityid']);
}
$value = '';
$isString = true;
foreach ($this->format as $element) {
if ($isString) {
$value .= $element;
} elseif ($element === '%') {
$value .= '%';
} else {
$value .= call_user_func([$rep, 'get' . $element]);
}
$isString = !$isString;
}
$state['Attributes'][$this->attribute] = [$value];
}
}

View File

@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SAML2\Constants;
use SimpleSAML\{Auth, Utils};
use function strtolower;
/**
* Filter to generate the Pairwise ID attribute.
*
* See: http://docs.oasis-open.org/security/saml-subject-id-attr/v1.0/csprd01/saml-subject-id-attr-v1.0-csprd01.html
*
* By default, this filter will generate the ID based on the UserID of the current user.
* This is generated from the attribute configured in 'identifyingAttribute' in the
* authproc-configuration.
*
* NOTE: since the subject-id is specified as single-value attribute, only the first value of `identifyingAttribute`
* and `scopeAttribute` are considered.
*
* Example - generate from attribute:
* <code>
* 'authproc' => [
* 50 => [
* 'saml:PairwiseID',
* 'identifyingAttribute' => 'uid',
* 'scopeAttribute' => 'example.org',
* ]
* ]
* </code>
*
* @package SimpleSAMLphp
*/
class PairwiseID extends SubjectID
{
/**
* The name for this class
*/
public const NAME = 'PairwiseID';
/**
* Initialize this filter.
*
* @param array &$config Configuration information about this filter.
* @param mixed $reserved For future use.
*/
public function __construct(array &$config, $reserved)
{
parent::__construct($config, $reserved);
}
/**
* Apply filter to add the Pairwise ID.
*
* @param array &$state The current state.
*/
public function process(array &$state): void
{
$userID = $this->getIdentifyingAttribute($state);
$scope = $this->getScopeAttribute($state);
if ($scope === null || $userID === null) {
// Attributes missing, precondition not met
return;
}
if (!empty($state['saml:RequesterID'])) {
// Proxied request - use actual SP entity ID
$sp_entityid = $state['saml:RequesterID'][0];
} else {
$sp_entityid = $state['core:SP'];
}
// Calculate hash
$hash = $this->calculateHash($userID . '|' . $sp_entityid);
$value = strtolower($hash . '@' . $scope);
$this->validateGeneratedIdentifier($value);
$state['Attributes'][Constants::ATTR_PAIRWISE_ID] = [$value];
}
}

View File

@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SAML2\Constants;
use SimpleSAML\Error;
use SimpleSAML\Logger;
use SimpleSAML\Module\saml\BaseNameIDGenerator;
use SimpleSAML\Utils;
/**
* Authentication processing filter to generate a persistent NameID.
*
* @package SimpleSAMLphp
*/
class PersistentNameID extends BaseNameIDGenerator
{
/**
* Which attribute contains the unique identifier of the user.
*
* @var string
*/
private string $identifyingAttribute;
/**
* Initialize this filter, parse configuration.
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*
* @throws \SimpleSAML\Error\Exception If the required option 'identifyingAttribute' is missing.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
$this->format = Constants::NAMEID_PERSISTENT;
if (!isset($config['identifyingAttribute'])) {
throw new Error\Exception("PersistentNameID: Missing required option 'identifyingAttribute'.");
}
$this->identifyingAttribute = $config['identifyingAttribute'];
}
/**
* Get the NameID value.
*
* @param array $state The state array.
* @return string|null The NameID value.
*/
protected function getValue(array &$state): ?string
{
if (!isset($state['Destination']['entityid'])) {
Logger::warning('No SP entity ID - not generating persistent NameID.');
return null;
}
$spEntityId = $state['Destination']['entityid'];
if (!isset($state['Source']['entityid'])) {
Logger::warning('No IdP entity ID - not generating persistent NameID.');
return null;
}
$idpEntityId = $state['Source']['entityid'];
if (
!isset($state['Attributes'][$this->identifyingAttribute])
|| count($state['Attributes'][$this->identifyingAttribute]) === 0
) {
Logger::warning(
'Missing attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating persistent NameID.',
);
return null;
}
if (count($state['Attributes'][$this->identifyingAttribute]) > 1) {
Logger::warning(
'More than one value in attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating persistent NameID.',
);
return null;
}
// just in case the first index is no longer 0
$uid = array_values($state['Attributes'][$this->identifyingAttribute]);
$uid = $uid[0];
if (empty($uid)) {
Logger::warning(
'Empty value in attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating persistent NameID.',
);
return null;
}
$configUtils = new Utils\Config();
$secretSalt = $configUtils->getSecretSalt();
$uidData = 'uidhashbase' . $secretSalt;
$uidData .= strlen($idpEntityId) . ':' . $idpEntityId;
$uidData .= strlen($spEntityId) . ':' . $spEntityId;
$uidData .= strlen($uid) . ':' . $uid;
$uidData .= $secretSalt;
return sha1($uidData);
}
}

View File

@@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SAML2\Constants;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Logger;
/**
* Authentication processing filter to create the eduPersonTargetedID attribute from the persistent NameID.
*
* @package SimpleSAMLphp
*/
class PersistentNameID2TargetedID extends ProcessingFilter
{
/**
* The attribute we should save the NameID in.
*
* @var string
*/
private string $attribute;
/**
* Whether we should insert it as an saml:NameID element.
*
* @var bool
*/
private bool $nameId;
/**
* Initialize this filter, parse configuration.
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
if (isset($config['attribute'])) {
$this->attribute = strval($config['attribute']);
} else {
$this->attribute = 'eduPersonTargetedID';
}
if (isset($config['nameId'])) {
$this->nameId = (bool) $config['nameId'];
} else {
$this->nameId = true;
}
}
/**
* Store a NameID to attribute.
*
* @param array &$state The request state.
*/
public function process(array &$state): void
{
if (!isset($state['saml:NameID'][Constants::NAMEID_PERSISTENT])) {
Logger::warning(
'Unable to generate eduPersonTargetedID because no persistent NameID was available.',
);
return;
}
/** @var \SAML2\XML\saml\NameID $nameID */
$nameID = $state['saml:NameID'][Constants::NAMEID_PERSISTENT];
$state['Attributes'][$this->attribute] = [(!$this->nameId) ? $nameID->getValue() : $nameID];
}
}

View File

@@ -0,0 +1,199 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SAML2\Constants as C;
use SimpleSAML\Error;
use SimpleSAML\Logger;
use SimpleSAML\Module\saml\BaseNameIDGenerator;
use SimpleSAML\Module\saml\Error as SAMLError;
use SimpleSAML\Module\saml\IdP\SQLNameID;
/**
* Authentication processing filter to generate a persistent NameID.
*
* @package SimpleSAMLphp
*/
class SQLPersistentNameID extends BaseNameIDGenerator
{
/**
* Which attribute contains the unique identifier of the user.
*
* @var string
*/
private string $identifyingAttribute;
/**
* Whether we should create a persistent NameID if not explicitly requested (as saml:PersistentNameID does).
*
* @var boolean
*/
private bool $allowUnspecified = false;
/**
* Whether we should create a persistent NameID if a different format is requested.
*
* @var boolean
*/
private bool $allowDifferent = false;
/**
* Whether we should ignore allowCreate in the NameID policy
*
* @var boolean
*/
private bool $alwaysCreate = false;
/**
* Database store configuration.
*
* @var array
*/
private array $storeConfig = [];
/**
* Initialize this filter, parse configuration.
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*
* @throws \SimpleSAML\Error\Exception If the 'identifyingAttribute' option is not specified.
*/
public function __construct(array &$config, $reserved)
{
parent::__construct($config, $reserved);
$this->format = C::NAMEID_PERSISTENT;
if (!isset($config['identifyingAttribute'])) {
throw new Error\Exception("PersistentNameID: Missing required option 'identifyingAttribute'.");
}
$this->identifyingAttribute = $config['identifyingAttribute'];
if (isset($config['allowUnspecified'])) {
$this->allowUnspecified = (bool) $config['allowUnspecified'];
}
if (isset($config['allowDifferent'])) {
$this->allowDifferent = (bool) $config['allowDifferent'];
}
if (isset($config['alwaysCreate'])) {
$this->alwaysCreate = (bool) $config['alwaysCreate'];
}
if (isset($config['store'])) {
$this->storeConfig = (array) $config['store'];
}
}
/**
* Get the NameID value.
*
* @param array $state The state array.
* @return string|null The NameID value.
*
* @throws \SimpleSAML\Module\saml\Error if the NameID creation policy is invalid.
*/
protected function getValue(array &$state): ?string
{
if (!isset($state['saml:NameIDFormat']) && !$this->allowUnspecified) {
Logger::debug(
'SQLPersistentNameID: Request did not specify persistent NameID format, ' .
'not generating persistent NameID.',
);
return null;
}
$validNameIdFormats = @array_filter([
$state['saml:NameIDFormat'],
$state['SPMetadata']['NameIDFormat'],
]);
if (
count($validNameIdFormats)
&& !in_array($this->format, $validNameIdFormats, true)
&& !$this->allowDifferent
) {
Logger::debug(
'SQLPersistentNameID: SP expects different NameID format (' .
implode(', ', $validNameIdFormats) . '), not generating persistent NameID.',
);
return null;
}
if (!isset($state['Destination']['entityid'])) {
Logger::warning('SQLPersistentNameID: No SP entity ID - not generating persistent NameID.');
return null;
}
$spEntityId = $state['Destination']['entityid'];
if (!isset($state['Source']['entityid'])) {
Logger::warning('SQLPersistentNameID: No IdP entity ID - not generating persistent NameID.');
return null;
}
$idpEntityId = $state['Source']['entityid'];
if (
!isset($state['Attributes'][$this->identifyingAttribute])
|| count($state['Attributes'][$this->identifyingAttribute]) === 0
) {
Logger::warning(
'SQLPersistentNameID: Missing attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating persistent NameID.',
);
return null;
}
if (count($state['Attributes'][$this->identifyingAttribute]) > 1) {
Logger::warning(
'SQLPersistentNameID: More than one value in attribute ' .
var_export($this->identifyingAttribute, true) .
' on user - not generating persistent NameID.',
);
return null;
}
// just in case the first index is no longer 0
$uid = array_values($state['Attributes'][$this->identifyingAttribute]);
$uid = $uid[0];
if (empty($uid)) {
Logger::warning(
'Empty value in attribute ' . var_export($this->identifyingAttribute, true) .
' on user - not generating persistent NameID.',
);
return null;
}
$value = SQLNameID::get($idpEntityId, $spEntityId, $uid, $this->storeConfig);
if ($value !== null) {
Logger::debug(
'SQLPersistentNameID: Found persistent NameID ' . var_export($value, true) . ' for user ' .
var_export($uid, true) . '.',
);
return $value;
}
if ((!isset($state['saml:AllowCreate']) || !$state['saml:AllowCreate']) && !$this->alwaysCreate) {
Logger::warning(
'SQLPersistentNameID: Did not find persistent NameID for user, and not allowed to create new NameID.',
);
throw new SAMLError(
C::STATUS_RESPONDER,
C::STATUS_INVALID_NAMEID_POLICY,
);
}
$value = bin2hex(openssl_random_pseudo_bytes(20));
Logger::debug(
'SQLPersistentNameID: Created persistent NameID ' . var_export($value, true) . ' for user ' .
var_export($uid, true) . '.',
);
SQLNameID::add($idpEntityId, $spEntityId, $uid, $value, $this->storeConfig);
return $value;
}
}

View File

@@ -0,0 +1,284 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SAML2\Constants;
use SAML2\Exception\ProtocolViolationException;
use SimpleSAML\{Auth, Logger, Utils};
use SimpleSAML\Assert\Assert;
use function array_key_exists;
use function explode;
use function hash_hmac;
use function preg_match;
use function strpos;
use function strtolower;
use function sprintf;
/**
* Filter to generate the subject ID attribute.
*
* See: http://docs.oasis-open.org/security/saml-subject-id-attr/v1.0/csprd01/saml-subject-id-attr-v1.0-csprd01.html
*
* By default, this filter will generate the ID based on the UserID of the current user.
* This is generated from the attribute configured in 'identifyingAttribute' in the
* authproc-configuration.
*
* NOTE: since the subject-id is specified as single-value attribute, only the first value of `identifyingAttribute`
* and `scopeAttribute` are considered.
*
* Example - generate from attribute:
* <code>
* 'authproc' => [
* 50 => [
* 'saml:SubjectID',
* 'identifyingAttribute' => 'uid',
* 'scopeAttribute' => 'scope',
* ]
* ]
* </code>
*
* @package SimpleSAMLphp
*/
class SubjectID extends Auth\ProcessingFilter
{
/**
* The name for this class
*/
public const NAME = 'SubjectID';
/**
* The regular expression to match the scope
*
* @var string
*/
public const SCOPE_PATTERN = '/^[a-z0-9][a-z0-9.-]{0,126}$/i';
/**
* The regular expression to match the specifications
*
* @var string
*/
public const SPEC_PATTERN = '/^[a-z0-9][a-z0-9=-]{0,126}@[a-z0-9][a-z0-9.-]{0,126}$/i';
/**
* The regular expression to match worrisome identifiers that need to raise a warning
*
* @var string
*/
public const WARN_PATTERN = '/^[a-z0-9][a-z0-9=-]{3,}@[a-z0-9][a-z0-9.-]+\.[a-z]{2,}$/i';
/**
* The attribute we should generate the subject id from.
*
* @var string
*/
protected string $identifyingAttribute;
/**
* The attribute we should use for the scope of the subject id.
*
* @var string
*/
protected string $scopeAttribute;
/**
* Whether the unique part of the subject id must be hashed
*
* @var bool
*/
private bool $hashed = false;
/**
* @var \SimpleSAML\Utils\Config
*/
protected Utils\Config $configUtils;
/**
* @var \SimpleSAML\Logger|string
* @psalm-var \SimpleSAML\Logger|class-string
*/
protected $logger = Logger::class;
/**
* Initialize this filter.
*
* @param array &$config Configuration information about this filter.
* @param mixed $reserved For future use.
*/
public function __construct(array &$config, $reserved)
{
parent::__construct($config, $reserved);
Assert::keyExists($config, 'identifyingAttribute', "Missing mandatory 'identifyingAttribute' config setting.");
Assert::keyExists($config, 'scopeAttribute', "Missing mandatory 'scopeAttribute' config setting.");
Assert::stringNotEmpty($config['identifyingAttribute']);
Assert::stringNotEmpty($config['scopeAttribute']);
$this->identifyingAttribute = $config['identifyingAttribute'];
$this->scopeAttribute = $config['scopeAttribute'];
if (array_key_exists('hashed', $config)) {
Assert::boolean($config['hashed']);
$this->hashed = $config['hashed'];
}
$this->configUtils = new Utils\Config();
}
/**
* Apply filter to add the subject ID.
*
* @param array &$state The current state.
*/
public function process(array &$state): void
{
$userID = $this->getIdentifyingAttribute($state);
$scope = $this->getScopeAttribute($state);
if ($scope === null || $userID === null) {
// Attributes missing, precondition not met
return;
}
if ($this->hashed === true) {
$value = strtolower($this->calculateHash($userID) . '@' . $scope);
} else {
$value = strtolower($userID . '@' . $scope);
}
$this->validateGeneratedIdentifier($value);
$state['Attributes'][Constants::ATTR_SUBJECT_ID] = [$value];
}
/**
* Retrieve the identifying attribute from the state and test it for erroneous conditions
*
* @param array $state
* @return string|null
* @throws \SimpleSAML\Assert\AssertionFailedException if the pre-conditions are not met
*/
protected function getIdentifyingAttribute(array $state): ?string
{
if (
!array_key_exists('Attributes', $state)
|| !array_key_exists($this->identifyingAttribute, $state['Attributes'])
) {
$this->logger::warning(sprintf(
"saml:" . static::NAME . ": Missing attribute '%s', which is needed to generate the ID.",
$this->identifyingAttribute,
));
return null;
}
$userID = $state['Attributes'][$this->identifyingAttribute][0];
Assert::stringNotEmpty(
$userID,
'saml:' . static::NAME . ': \'identifyingAttribute\' cannot be an empty string.',
);
return $userID;
}
/**
* Retrieve the scope attribute from the state and test it for erroneous conditions
*
* @param array $state
* @return string|null
* @throws \SimpleSAML\Assert\AssertionFailedException if the scope is an empty string
* @throws \SAML2\Exception\ProtocolViolationException if the pre-conditions are not met
*/
protected function getScopeAttribute(array $state): ?string
{
if (!array_key_exists('Attributes', $state) || !array_key_exists($this->scopeAttribute, $state['Attributes'])) {
$this->logger::warning(sprintf(
"saml:" . static::NAME . ": Missing attribute '%s', which is needed to generate the ID.",
$this->scopeAttribute,
));
return null;
}
$scope = $state['Attributes'][$this->scopeAttribute][0];
Assert::stringNotEmpty($scope, 'saml:' . static::NAME . ': \'scopeAttribute\' cannot be an empty string.');
// If the value is scoped, extract the scope from it
if (strpos($scope, '@') !== false) {
$scope = explode('@', $scope, 2);
$scope = $scope[1];
}
Assert::regex(
$scope,
self::SCOPE_PATTERN,
'saml:' . static::NAME . ': \'scopeAttribute\' contains illegal characters.',
ProtocolViolationException::class,
);
return $scope;
}
/**
* Test the generated identifier to ensure it's compliant with the specifications.
* Log a warning when the generated value is considered to be weak
*
* @param string $value
* @return void
* @throws \SAML2\Exception\ProtocolViolationException if the post-conditions are not met
*/
protected function validateGeneratedIdentifier(string $value): void
{
Assert::regex(
$value,
self::SPEC_PATTERN,
'saml:' . static::NAME . ': Generated ID \'' . $value . '\' contains illegal characters.',
ProtocolViolationException::class,
);
if (preg_match(self::WARN_PATTERN, $value) === 0) {
$this->logger::warning(
'saml:' . static::NAME . ': Generated ID \'' . $value . '\' can hardly be considered globally unique.',
);
}
}
/**
* Calculate the hash for the unique part of the identifier.
*/
protected function calculateHash(string $input): string
{
$salt = $this->configUtils->getSecretSalt();
return hash_hmac('sha256', $input, $salt, false);
}
/**
* Inject the \SimpleSAML\Logger dependency.
*
* @param \SimpleSAML\Logger $logger
*/
public function setLogger(Logger $logger): void
{
$this->logger = $logger;
}
/**
* Inject the \SimpleSAML\Utils\Config dependency.
*
* @param \SimpleSAML\Utils\Config $configUtils
*/
public function setConfigUtils(Utils\Config $configUtils): void
{
$this->configUtils = $configUtils;
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Auth\Process;
use SAML2\Constants;
use SimpleSAML\Module\saml\BaseNameIDGenerator;
use SimpleSAML\Utils;
/**
* Authentication processing filter to generate a transient NameID.
*
* @package SimpleSAMLphp
*/
class TransientNameID extends BaseNameIDGenerator
{
/**
* Initialize this filter, parse configuration
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
$this->format = Constants::NAMEID_TRANSIENT;
}
/**
* Get the NameID value.
*
* @param array $state The state array.
* @return string|null The NameID value.
*/
protected function getValue(array &$state): ?string
{
$randomUtils = new Utils\Random();
return $randomUtils->generateID();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml;
use SAML2\XML\saml\NameID;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Logger;
/**
* Base filter for generating NameID values.
*
* @package SimpleSAMLphp
*/
abstract class BaseNameIDGenerator extends ProcessingFilter
{
/**
* What NameQualifier should be used.
* Can be one of:
* - a string: The qualifier to use.
* - FALSE: Do not include a NameQualifier. This is the default.
* - TRUE: Use the IdP entity ID.
*
* @var string|bool
*/
private string|bool $nameQualifier;
/**
* What SPNameQualifier should be used.
* Can be one of:
* - a string: The qualifier to use.
* - FALSE: Do not include a SPNameQualifier.
* - TRUE: Use the SP entity ID. This is the default.
*
* @var string|bool
*/
private string|bool $spNameQualifier;
/**
* The format of this NameID.
*
* This property must be set by the subclass.
*
* @var string|null
*/
protected ?string $format = null;
/**
* Initialize this filter, parse configuration.
*
* @param array $config Configuration information about this filter.
* @param mixed $reserved For future use.
*/
public function __construct(array $config, $reserved)
{
parent::__construct($config, $reserved);
if (isset($config['NameQualifier'])) {
$this->nameQualifier = $config['NameQualifier'];
} else {
$this->nameQualifier = false;
}
if (isset($config['SPNameQualifier'])) {
$this->spNameQualifier = $config['SPNameQualifier'];
} else {
$this->spNameQualifier = true;
}
}
/**
* Get the NameID value.
*
* @return string|null The NameID value.
*/
abstract protected function getValue(array &$state): ?string;
/**
* Generate transient NameID.
*
* @param array &$state The request state.
*/
public function process(array &$state): void
{
Assert::string($this->format);
$value = $this->getValue($state);
if ($value === null) {
return;
}
$nameId = new NameID();
$nameId->setValue($value);
$nameId->setFormat($this->format);
if ($this->nameQualifier === true) {
if (isset($state['IdPMetadata']['entityid'])) {
$nameId->setNameQualifier($state['IdPMetadata']['entityid']);
} else {
Logger::warning('No IdP entity ID, unable to set NameQualifier.');
}
} elseif (is_string($this->nameQualifier)) {
$nameId->setNameQualifier($this->nameQualifier);
}
if ($this->spNameQualifier === true) {
if (isset($state['SPMetadata']['entityid'])) {
$nameId->setSPNameQualifier($state['SPMetadata']['entityid']);
} else {
Logger::warning('No SP entity ID, unable to set SPNameQualifier.');
}
} elseif (is_string($this->spNameQualifier)) {
$nameId->setSPNameQualifier($this->spNameQualifier);
}
/** @psalm-suppress PossiblyNullArrayOffset */
$state['saml:NameID'][$this->format] = $nameId;
}
}

View File

@@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Controller;
use SimpleSAML\Configuration;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\XHTML\IdPDisco;
/**
* Controller class for the saml module.
*
* This class serves the different views available in the module.
*
* @package simplesamlphp/simplesamlphp
*/
class Disco
{
/**
* Controller constructor.
*
* It initializes the global configuration for the controllers implemented here.
*
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
*/
public function __construct(
protected Configuration $config,
) {
}
/**
* Built-in IdP discovery service
*
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function disco(): RunnableResponse
{
$disco = new IdPDisco(['saml20-idp-remote'], 'saml');
return new RunnableResponse([$disco, 'handleRequest']);
}
}

View File

@@ -0,0 +1,122 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Controller;
use Exception;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\Metadata as SSPMetadata;
use SimpleSAML\Metadata\MetaDataStorageHandler;
use SimpleSAML\Module;
use SimpleSAML\Module\saml\IdP\SAML2 as SAML2_IdP;
use SimpleSAML\Utils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Controller class for the IdP metadata.
*
* This class serves the different views available.
*
* @package simplesamlphp/simplesamlphp
*/
class Metadata
{
/** @var \SimpleSAML\Utils\Auth */
protected Utils\Auth $authUtils;
/** @var \SimpleSAML\Metadata\MetaDataStorageHandler */
protected MetadataStorageHandler $mdHandler;
/**
* Controller constructor.
*
* It initializes the global configuration for the controllers implemented here.
*
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
*/
public function __construct(
protected Configuration $config,
) {
$this->authUtils = new Utils\Auth();
$this->mdHandler = MetaDataStorageHandler::getMetadataHandler();
}
/**
* Inject the \SimpleSAML\Utils\Auth dependency.
*
* @param \SimpleSAML\Utils\Auth $authUtils
*/
public function setAuthUtils(Utils\Auth $authUtils): void
{
$this->authUtils = $authUtils;
}
/**
* Inject the \SimpleSAML\Metadata\MetadataStorageHandler dependency.
*/
public function setMetadataStorageHandler(MetadataStorageHandler $mdHandler): void
{
$this->mdHandler = $mdHandler;
}
/**
* This endpoint will offer the SAML 2.0 IdP metadata.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \SimpleSAML\HTTP\RunnableResponse|\Symfony\Component\HttpFoundation\Response
*/
public function metadata(Request $request): Response
{
if ($this->config->getBoolean('enable.saml20-idp') === false || !Module::isModuleEnabled('saml')) {
throw new Error\Error(Error\ErrorCodes::NOACCESS, null, 403);
}
// check if valid local session exists
$protectedMetadata = $this->config->getOptionalBoolean('admin.protectmetadata', false);
if ($protectedMetadata && !$this->authUtils->isAdmin()) {
return new RunnableResponse([$this->authUtils, 'requireAdmin']);
}
try {
if ($request->query->has('idpentityid')) {
$idpentityid = $request->query->get('idpentityid');
} else {
$idpentityid = $this->mdHandler->getMetaDataCurrentEntityID('saml20-idp-hosted');
}
$metaArray = SAML2_IdP::getHostedMetadata($idpentityid, $this->mdHandler);
$metaBuilder = new SSPMetadata\SAMLBuilder($idpentityid);
$metaBuilder->addMetadataIdP20($metaArray);
$metaBuilder->addOrganizationInfo($metaArray);
$metaxml = $metaBuilder->getEntityDescriptorText();
// sign the metadata if enabled
$metaxml = SSPMetadata\Signer::sign($metaxml, $metaArray, 'SAML 2 IdP');
$response = new Response();
$response->setEtag(hash('sha256', $metaxml));
$response->setCache([
'no_cache' => $protectedMetadata === true,
'public' => $protectedMetadata === false,
'private' => $protectedMetadata === true,
]);
if ($response->isNotModified($request)) {
return $response;
}
$response->headers->set('Content-Type', 'application/samlmetadata+xml');
$response->headers->set('Content-Disposition', 'attachment; filename="idp-metadata.xml"');
$response->setContent($metaxml);
return $response;
} catch (Exception $exception) {
throw new Error\Error(Error\ErrorCodes::METADATA, $exception);
}
}
}

View File

@@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Controller;
use Exception;
use SAML2\Constants;
use SimpleSAML\Auth;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\IdP;
use SimpleSAML\Module\saml\Auth\Source\SP;
use SimpleSAML\Module\saml\Error\NoAvailableIDP;
use SimpleSAML\XHTML\Template;
use Symfony\Component\HttpFoundation\{Request, Response};
/**
* Controller class for the saml module.
*
* This class serves the different views available in the module.
*
* @package simplesamlphp/simplesamlphp
*/
class Proxy
{
/**
* @var \SimpleSAML\Auth\State|string
* @psalm-var \SimpleSAML\Auth\State|class-string
*/
protected $authState = Auth\State::class;
/**
* Controller constructor.
*
* It initializes the global configuration for the controllers implemented here.
*
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
*/
public function __construct(
protected Configuration $config,
) {
}
/**
* Inject the \SimpleSAML\Auth\State dependency.
*
* @param \SimpleSAML\Auth\State $authState
*/
public function setAuthState(Auth\State $authState): void
{
$this->authState = $authState;
}
/**
* This controller will handle the case of a user with an existing session that's not valid for a specific
* Service Provider, since the authenticating IdP is not in the list of IdPs allowed by the SP.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \SimpleSAML\XHTML\Template|\Symfony\Component\HttpFoundation\Response
*/
public function invalidSession(Request $request): Response
{
// retrieve the authentication state
$stateId = $request->query->get('AuthState'); // GET
if ($stateId === null && $request->request->has('AuthState')) {
$stateId = $request->request->get('AuthState'); // POST
}
if (!is_string($stateId)) {
throw new Error\BadRequest('Missing mandatory parameter: AuthState');
}
try {
// try to get the state
$state = $this->authState::loadState($stateId, 'saml:proxy:invalid_idp');
} catch (Exception $e) {
// the user probably hit the back button after starting the logout,
// try to recover the state with another stage
$state = $this->authState::loadState($stateId, 'core:Logout:afterbridge');
// success! Try to continue with reauthentication, since we no longer have a valid session here
$idp = IdP::getById($state['core:IdP']);
return new RunnableResponse([SP::class, 'reauthPostLogout'], [$idp, $state]);
}
if ($request->request->has('cancel')) {
// the user does not want to logout, cancel login
$this->authState::throwException(
$state,
new NoAvailableIDP(
Constants::STATUS_RESPONDER,
'User refused to reauthenticate with any of the IdPs requested.',
),
);
}
if ($request->request->has('continue')) {
/** @var \SimpleSAML\Module\saml\Auth\Source\SP $as */
$as = new \SimpleSAML\Auth\Simple($state['saml:sp:AuthId']);
// log the user out before being able to login again
return new RunnableResponse([$as, 'login'], [$state]);
}
$template = new Template($this->config, 'saml:proxy/invalid_session.twig');
$template->data['AuthState'] = $stateId;
/** @var \SimpleSAML\Configuration $idpmdcfg */
$idpmdcfg = $state['saml:sp:IdPMetadata'];
$template->data['entity_idp'] = $idpmdcfg->toArray();
$template->data['entity_sp'] = $state['SPMetadata'];
return $template;
}
}

View File

@@ -0,0 +1,733 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Controller;
use Exception;
use SAML2\Assertion;
use SAML2\Binding;
use SAML2\Constants;
use SAML2\Exception\Protocol\UnsupportedBindingException;
use SAML2\HTTPArtifact;
use SAML2\HTTPPost;
use SAML2\HTTPRedirect;
use SAML2\LogoutRequest;
use SAML2\LogoutResponse;
use SAML2\Response as SAML2_Response;
use SAML2\SOAP;
use SAML2\XML\saml\Issuer;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Auth;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\Logger;
use SimpleSAML\Metadata;
use SimpleSAML\Module;
use SimpleSAML\Module\saml\Auth\Source\SP;
use SimpleSAML\Session;
use SimpleSAML\Store\StoreFactory;
use SimpleSAML\Utils;
use SimpleSAML\XHTML\Template;
use Symfony\Component\HttpFoundation\{Request, Response};
use function array_merge;
use function count;
use function end;
use function get_class;
use function in_array;
use function is_null;
use function time;
use function var_export;
/**
* Controller class for the saml module.
*
* This class serves the different views available in the module.
*
* @package simplesamlphp/simplesamlphp
*/
class ServiceProvider
{
/**
* @var \SimpleSAML\Auth\State|string
* @psalm-var \SimpleSAML\Auth\State|class-string
*/
protected $authState = Auth\State::class;
/** @var \SimpleSAML\Utils\Auth */
protected Utils\Auth $authUtils;
/**
* Controller constructor.
*
* It initializes the global configuration for the controllers implemented here.
*
* @param Configuration $config The configuration to use by the controllers.
* @param Session $session The Session to use by the controllers.
*/
public function __construct(
protected Configuration $config,
protected Session $session,
) {
$this->authUtils = new Utils\Auth();
}
/**
* Inject the \SimpleSAML\Auth\State dependency.
*
* @param \SimpleSAML\Auth\State $authState
*/
public function setAuthState(Auth\State $authState): void
{
$this->authState = $authState;
}
/**
* Inject the \SimpleSAML\Utils\Auth dependency.
*
* @param \SimpleSAML\Utils\Auth $authUtils
*/
public function setAuthUtils(Utils\Auth $authUtils): void
{
$this->authUtils = $authUtils;
}
/**
* Start single sign-on for an SP identified with the specified Authsource ID
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param string $sourceId
*
* @return \SimpleSAML\HTTP\RunnableResponse
* @throws Error\Exception
*/
public function login(Request $request, string $sourceId): RunnableResponse
{
// Initialize all the dependencies
$authSource = new Auth\Simple($sourceId);
$spSource = $authSource->getAuthSource();
if (!($spSource instanceof SP)) {
throw new Error\Exception('Authsource must be of type saml:SP.');
}
$httpUtils = new Utils\HTTP();
$returnTo = $this->loginHandler($request, $authSource, $spSource, $httpUtils);
// Redirect to the returnTo destination
return new RunnableResponse([$httpUtils, 'redirectTrustedURL'], [$returnTo]);
}
/**
* @param Request $request
* @param Auth\Simple $authSource
* @param Auth\Source $spSource
* @param Utils\HTTP $httpUtils
*
* @return string
* @throws BadRequest
* @throws Error\Exception
*/
protected function loginHandler(
Request $request,
Auth\Simple $authSource,
Auth\Source $spSource,
Utils\HTTP $httpUtils,
): string {
$options = [];
if ($spSource->isRequestInitiation()) {
if ($request->query->has('target')) {
$options['ReturnTo'] = $httpUtils->checkURLAllowed($request->query->get('target'));
}
if ($request->query->has('forceAuthn')) {
$options['ForceAuthn'] = $request->query->getBoolean('forceAuthn');
}
if ($request->query->has('entityID')) {
$options['saml:idp'] = $request->query->get('entityID');
}
if ($request->query->has('isPassive')) {
$options['isPassive'] = $request->query->getBoolean('isPassive');
}
}
if (
!isset($options['ReturnTo'])
&& !$request->query->has('ReturnTo')
&& !$spSource->getMetadata()->hasValue('RelayState')
) {
throw new Error\BadRequest('Missing ReturnTo parameter.');
}
if (!isset($options['ReturnTo'])) {
$options['ReturnTo'] = $httpUtils->checkURLAllowed(
$request->query->get('ReturnTo') ?? $spSource->getMetadata()->getString('RelayState'),
);
}
$authData = $authSource->getAuthDataArray();
if (
$authSource->isAuthenticated()
&& $spSource->isRequestInitiation()
) {
if (
// Check the IdP we are currently authenticated to
(isset($authData['saml:sp:IdP'], $options['saml:idp'])
&& $options['saml:idp'] !== $authData['saml:sp:IdP'])
||
(isset($options['ForceAuthn']) && $options['ForceAuthn'])
) {
// Force a re-authentication
$authSource->login($options);
}
// We are already authenticated, do nothing
}
/**
* Try to authenticate
*/
$authSource->requireAuth($options);
// Return the redirect target
return $options['ReturnTo'];
}
/**
* Handler for response from IdP discovery service.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function discoResponse(Request $request): RunnableResponse
{
if (!$request->query->has('AuthID')) {
throw new Error\BadRequest('Missing AuthID to discovery service response handler');
}
$authId = $request->query->get('AuthID');
if (!$request->query->has('idpentityid')) {
throw new Error\BadRequest('Missing idpentityid to discovery service response handler');
}
$idpEntityId = $request->query->get('idpentityid');
$state = $this->authState::loadState($authId, 'saml:sp:sso');
// Find authentication source
Assert::keyExists($state, 'saml:sp:AuthId');
$sourceId = $state['saml:sp:AuthId'];
$source = Auth\Source::getById($sourceId);
if ($source === null) {
throw new Exception('Could not find authentication source with id ' . $sourceId);
}
if (!($source instanceof SP)) {
throw new Error\Exception('Source type changed?');
}
return new RunnableResponse([$source, 'startSSO'], [$idpEntityId, $state]);
}
/**
* @return \SimpleSAML\XHTML\Template
*/
public function wrongAuthnContextClassRef(): Template
{
return new Template($this->config, 'saml:sp/wrong_authncontextclassref.twig');
}
/**
* Handler for the Assertion Consumer Service.
*
* @param string $sourceId
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function assertionConsumerService(string $sourceId): RunnableResponse
{
/** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */
$source = Auth\Source::getById($sourceId, SP::class);
$spMetadata = $source->getMetadata();
try {
$b = Binding::getCurrentBinding();
} catch (UnsupportedBindingException $e) {
throw new Error\Error(Error\ErrorCodes::ACSPARAMS, $e, 400);
}
if ($b instanceof HTTPArtifact) {
$b->setSPMetadata($spMetadata);
}
$response = $b->receive();
if (!($response instanceof SAML2_Response)) {
throw new Error\BadRequest('Invalid message received at AssertionConsumerService endpoint.');
}
$issuer = $response->getIssuer();
if ($issuer === null) {
// no Issuer in the response. Look for an unencrypted assertion with an issuer
foreach ($response->getAssertions() as $a) {
if ($a instanceof Assertion) {
// we found an unencrypted assertion, there should be an issuer here
$issuer = $a->getIssuer();
break;
}
}
if ($issuer === null) {
// no issuer found in the assertions
throw new Exception('Missing <saml:Issuer> in message delivered to AssertionConsumerService.');
}
}
$issuer = $issuer->getValue();
$prevAuth = $this->session->getAuthData($sourceId, 'saml:sp:prevAuth');
$httpUtils = new Utils\HTTP();
if ($prevAuth !== null && $prevAuth['id'] === $response->getId() && $prevAuth['issuer'] === $issuer) {
/**
* OK, it looks like this message has the same issuer
* and ID as the SP session we already have active. We
* therefore assume that the user has somehow triggered
* a resend of the message.
* In that case we may as well just redo the previous redirect
* instead of displaying a confusing error message.
*/
Logger::info(sprintf(
'%s - %s',
'Duplicate SAML 2 response detected',
'ignoring the response and redirecting the user to the correct page.',
));
if (isset($prevAuth['redirect'])) {
return new RunnableResponse([$httpUtils, 'redirectTrustedURL'], [$prevAuth['redirect']]);
}
Logger::info('No RelayState or ReturnURL available, cannot redirect.');
throw new Error\Exception('Duplicate assertion received.');
}
$idpMetadata = null;
$state = null;
$stateId = $response->getInResponseTo();
if (!empty($stateId)) {
// this should be a response to a request we sent earlier
try {
$state = $this->authState::loadState($stateId, 'saml:sp:sso');
} catch (Exception $e) {
// something went wrong,
Logger::warning(sprintf(
'Could not load state specified by InResponseTo: %s Processing response as unsolicited.',
$e->getMessage(),
));
}
}
$enableUnsolicited = $spMetadata->getOptionalBoolean('enable_unsolicited', true);
if ($state === null && $enableUnsolicited === false) {
throw new Error\BadRequest('Unsolicited responses are denied by configuration.');
}
if ($state) {
// check that the authentication source is correct
Assert::keyExists($state, 'saml:sp:AuthId');
if ($state['saml:sp:AuthId'] !== $sourceId) {
throw new Error\Exception(
"The authentication source id in the URL doesn't match the authentication"
. " source that sent the request.",
);
}
// check that the issuer is the one we are expecting
Assert::keyExists($state, 'ExpectedIssuer');
if ($state['ExpectedIssuer'] !== $issuer) {
$idpMetadata = $source->getIdPMetadata($issuer);
$idplist = $idpMetadata->getOptionalArrayize('IDPList', []);
if (!in_array($state['ExpectedIssuer'], $idplist, true)) {
Logger::warning(
'The issuer of the response not match to the identity provider we sent the request to.',
);
}
}
} else {
// this is an unsolicited response
$relaystate = $spMetadata->getOptionalString('RelayState', $response->getRelayState());
$state = [
'saml:sp:isUnsolicited' => true,
'saml:sp:AuthId' => $sourceId,
'saml:sp:RelayState' => $relaystate === null ? null : $httpUtils->checkURLAllowed($relaystate),
];
}
Logger::debug('Received SAML2 Response from ' . var_export($issuer, true) . '.');
if (is_null($idpMetadata)) {
$idpMetadata = $source->getIdPmetadata($issuer);
}
try {
$assertions = Module\saml\Message::processResponse($spMetadata, $idpMetadata, $response);
} catch (Module\saml\Error $e) {
// the status of the response wasn't "success"
$e = $e->toException();
$this->authState::throwException($state, $e);
Assert::true(false);
}
$authenticatingAuthority = null;
$nameId = null;
$sessionIndex = null;
$expire = null;
$attributes = [];
$foundAuthnStatement = false;
$storeType = $this->config->getOptionalString('store.type', 'phpsession');
$store = StoreFactory::getInstance($storeType);
foreach ($assertions as $assertion) {
// check for duplicate assertion (replay attack)
if ($store !== false) {
$aID = $assertion->getId();
if ($store->get('saml.AssertionReceived', $aID) !== null) {
$e = new Error\Exception('Received duplicate assertion.');
$this->authState::throwException($state, $e);
}
$notOnOrAfter = $assertion->getNotOnOrAfter();
if ($notOnOrAfter === null) {
$notOnOrAfter = time() + 24 * 60 * 60;
} else {
$notOnOrAfter += 60; // we allow 60 seconds clock skew, so add it here also
}
$store->set('saml.AssertionReceived', $aID, true, $notOnOrAfter);
}
if ($authenticatingAuthority === null) {
$authenticatingAuthority = $assertion->getAuthenticatingAuthority();
}
if ($nameId === null) {
$nameId = $assertion->getNameId();
}
if ($sessionIndex === null) {
$sessionIndex = $assertion->getSessionIndex();
}
if ($expire === null) {
$expire = $assertion->getSessionNotOnOrAfter();
}
$attributes = array_merge($attributes, $assertion->getAttributes());
if ($assertion->getAuthnInstant() !== null) {
// assertion contains AuthnStatement, since AuthnInstant is a required attribute
$foundAuthnStatement = true;
}
}
$assertion = end($assertions);
if (!$foundAuthnStatement) {
$e = new Error\Exception('No AuthnStatement found in assertion(s).');
$this->authState::throwException($state, $e);
}
if ($expire !== null) {
$logoutExpire = $expire;
} else {
// just expire the logout association 24 hours into the future
$logoutExpire = time() + 24 * 60 * 60;
}
if (!empty($nameId)) {
// register this session in the logout store
Module\saml\SP\LogoutStore::addSession($sourceId, $nameId, $sessionIndex, $logoutExpire);
// we need to save the NameID and SessionIndex for logout
$logoutState = [
'saml:logout:Type' => 'saml2',
'saml:logout:IdP' => $issuer,
'saml:logout:NameID' => $nameId,
'saml:logout:SessionIndex' => $sessionIndex,
];
$state['saml:sp:NameID'] = $nameId; // no need to mark it as persistent, it already is
} else {
/*
* No NameID provided, we can't logout from this IdP!
*
* Even though interoperability profiles "require" a NameID, the SAML 2.0 standard does not require
* it to be present in assertions. That way, we could have a Subject with only a SubjectConfirmation,
* or even no Subject element at all.
*
* In case we receive a SAML assertion with no NameID, we can be graceful and continue, but we won't
* be able to perform a Single Logout since the SAML logout profile mandates the use of a NameID to
* identify the individual we want to be logged out. In order to minimize the impact of this, we keep
* logout state information (without saving it to the store), marking the IdP as SAML 1.0, which
* does not implement logout. Then we can safely log the user out from the local session, skipping
* Single Logout upstream to the IdP.
*/
$logoutState = [
'saml:logout:Type' => 'saml1',
];
}
$state['LogoutState'] = $logoutState;
$state['saml:AuthenticatingAuthority'] = $authenticatingAuthority;
$state['saml:AuthenticatingAuthority'][] = $issuer;
$state['PersistentAuthData'][] = 'saml:AuthenticatingAuthority';
$state['saml:AuthnInstant'] = $assertion->getAuthnInstant();
$state['PersistentAuthData'][] = 'saml:AuthnInstant';
$state['saml:sp:SessionIndex'] = $sessionIndex;
$state['PersistentAuthData'][] = 'saml:sp:SessionIndex';
$state['saml:sp:AuthnContext'] = $assertion->getAuthnContextClassRef();
$state['PersistentAuthData'][] = 'saml:sp:AuthnContext';
if ($expire !== null) {
$state['Expire'] = $expire;
}
// note some information about the authentication, in case we receive the same response again
$state['saml:sp:prevAuth'] = [
'id' => $response->getId(),
'issuer' => $issuer,
'inResponseTo' => $response->getInResponseTo(),
];
if (isset($state['\SimpleSAML\Auth\Source.ReturnURL'])) {
$state['saml:sp:prevAuth']['redirect'] = $state['\SimpleSAML\Auth\Source.ReturnURL'];
} elseif (isset($state['saml:sp:RelayState'])) {
$state['saml:sp:prevAuth']['redirect'] = $state['saml:sp:RelayState'];
}
$state['PersistentAuthData'][] = 'saml:sp:prevAuth';
return new RunnableResponse([$source, 'handleResponse'], [$state, $issuer, $attributes]);
}
/**
* Logout endpoint handler for SAML SP authentication client.
*
* This endpoint handles both logout requests and logout responses.
*
* @param string $sourceId
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function singleLogoutService(string $sourceId): RunnableResponse
{
/** @var \SimpleSAML\Module\saml\Auth\Source\SP $source */
$source = Auth\Source::getById($sourceId);
if ($source === null) {
throw new Error\Exception('No authentication source with id \'' . $sourceId . '\' found.');
} elseif (!($source instanceof \SimpleSAML\Module\saml\Auth\Source\SP)) {
throw new Error\Exception('Source type changed?');
}
try {
$binding = Binding::getCurrentBinding();
} catch (UnsupportedBindingException $e) {
throw new Error\Error(Error\ErrorCodes::SLOSERVICEPARAMS, $e, 400);
}
$message = $binding->receive();
$issuer = $message->getIssuer();
if ($issuer instanceof Issuer) {
$idpEntityId = $issuer->getValue();
} else {
$idpEntityId = $issuer;
}
if ($idpEntityId === null) {
// Without an issuer we have no way to respond to the message.
throw new Error\BadRequest('Received message on logout endpoint without issuer.');
}
$spEntityId = $source->getEntityId();
$idpMetadata = $source->getIdPMetadata($idpEntityId);
$spMetadata = $source->getMetadata();
Module\saml\Message::validateMessage($idpMetadata, $spMetadata, $message);
$httpUtils = new Utils\HTTP();
$destination = $message->getDestination();
if ($destination !== null && $destination !== $httpUtils->getSelfURLNoQuery()) {
throw new Error\Exception('Destination in logout message is wrong.');
}
if ($message instanceof LogoutResponse) {
$relayState = $message->getRelayState();
if ($relayState === null) {
// Somehow, our RelayState has been lost.
throw new Error\BadRequest('Missing RelayState in logout response.');
}
if (!$message->isSuccess()) {
Logger::warning(
'Unsuccessful logout. Status was: ' . Module\saml\Message::getResponseError($message),
);
}
$state = $this->authState::loadState($relayState, 'saml:slosent');
$state['saml:sp:LogoutStatus'] = $message->getStatus();
return new RunnableResponse([Auth\Source::class, 'completeLogout'], [&$state]);
} elseif ($message instanceof LogoutRequest) {
Logger::debug('module/saml2/sp/logout: Request from ' . $idpEntityId);
Logger::stats('saml20-idp-SLO idpinit ' . $spEntityId . ' ' . $idpEntityId);
if ($message->isNameIdEncrypted()) {
try {
$keys = Module\saml\Message::getDecryptionKeys($idpMetadata, $spMetadata);
} catch (Exception $e) {
throw new Error\Exception('Error decrypting NameID: ' . $e->getMessage());
}
$blacklist = Module\saml\Message::getBlacklistedAlgorithms($idpMetadata, $spMetadata);
$lastException = null;
foreach ($keys as $i => $key) {
try {
$message->decryptNameId($key, $blacklist);
Logger::debug('Decryption with key #' . $i . ' succeeded.');
$lastException = null;
break;
} catch (Exception $e) {
Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage());
$lastException = $e;
}
}
if ($lastException !== null) {
throw $lastException;
}
}
$nameId = $message->getNameId();
$sessionIndexes = $message->getSessionIndexes();
/** @psalm-suppress PossiblyNullArgument This will be fixed in saml2 5.0 */
$numLoggedOut = Module\saml\SP\LogoutStore::logoutSessions($sourceId, $nameId, $sessionIndexes);
if ($numLoggedOut === false) {
// This type of logout was unsupported. Use the old method
$source->handleLogout($idpEntityId);
$numLoggedOut = count($sessionIndexes);
}
// Create and send response
$lr = Module\saml\Message::buildLogoutResponse($spMetadata, $idpMetadata);
$lr->setRelayState($message->getRelayState());
$lr->setInResponseTo($message->getId());
// If we set a key, we're sending a signed message
$signedMessage = $lr->getSignatureKey() ? true : false;
if ($numLoggedOut < count($sessionIndexes)) {
Logger::warning('Logged out of ' . $numLoggedOut . ' of ' . count($sessionIndexes) . ' sessions.');
}
$dst = $idpMetadata->getEndpointPrioritizedByBinding(
'SingleLogoutService',
[
Constants::BINDING_HTTP_REDIRECT,
Constants::BINDING_HTTP_POST,
],
);
if (!($binding instanceof SOAP)) {
$binding = Binding::getBinding($dst['Binding']);
if (isset($dst['ResponseLocation'])) {
$dst = $dst['ResponseLocation'];
} else {
$dst = $dst['Location'];
}
$binding->setDestination($dst);
if ($signedMessage && ($binding instanceof HTTPRedirect || $binding instanceof HTTPPost)) {
/**
* Bindings 3.4.5.2 - Security Considerations (HTTP-Redirect)
* Bindings 3.5.5.2 - Security Considerations (HTTP-POST)
*
* If the message is signed, the Destination XML attribute in the root SAML element of the protocol
* message MUST contain the URL to which the sender has instructed the user agent to deliver the
* message. The recipient MUST then verify that the value matches the location at which the message
* has been received.
*/
$lr->setDestination($dst);
}
} else {
$lr->setDestination($dst['Location']);
}
return new RunnableResponse([$binding, 'send'], [$lr]);
} else {
throw new Error\BadRequest('Unknown message received on logout endpoint: ' . get_class($message));
}
}
/**
* Metadata endpoint for SAML SP
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param string $sourceId
* @return \Symfony\Component\HttpFoundation\Response|\SimpleSAML\HTTP\RunnableResponse
*/
public function metadata(Request $request, string $sourceId): Response
{
$protectedMetadata = $this->config->getOptionalBoolean('admin.protectmetadata', false);
if ($protectedMetadata && !$this->authUtils->isAdmin()) {
return new RunnableResponse([$this->authUtils, 'requireAdmin']);
}
$source = Auth\Source::getById($sourceId);
if ($source === null) {
throw new Error\AuthSource($sourceId, 'Could not find authentication source.');
}
if (!($source instanceof SP)) {
throw new Error\AuthSource(
$sourceId,
'The authentication source is not a SAML Service Provider.',
);
}
$entityId = $source->getEntityId();
$spconfig = $source->getMetadata();
$metaArray20 = $source->getHostedMetadata();
$metaBuilder = new Metadata\SAMLBuilder($entityId);
$metaBuilder->addMetadataSP20($metaArray20, $source->getSupportedProtocols());
$metaBuilder->addOrganizationInfo($metaArray20);
$xml = $metaBuilder->getEntityDescriptorText();
// sign the metadata if enabled
$metaxml = Metadata\Signer::sign($xml, $spconfig->toArray(), 'SAML 2 SP');
$response = new Response();
$response->setEtag(hash('sha256', $metaxml));
$response->setCache([
'no_cache' => $protectedMetadata === true,
'public' => $protectedMetadata === false,
'private' => $protectedMetadata === true,
]);
if ($response->isNotModified($request)) {
return $response;
}
$response->headers->set('Content-Type', 'application/samlmetadata+xml');
$response->headers->set('Content-Disposition', 'attachment; filename="' . basename($sourceId) . '.xml"');
$response->setContent($metaxml);
return $response;
}
}

View File

@@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Controller;
use SAML2\Exception\Protocol\UnsupportedBindingException;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\IdP;
use SimpleSAML\Logger;
use SimpleSAML\Metadata\MetaDataStorageHandler;
use SimpleSAML\Module;
use SimpleSAML\Utils;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller class for the Single Logout Profile.
*
* This class serves the different views available.
*
* @package simplesamlphp/simplesamlphp
*/
class SingleLogout
{
/** @var \SimpleSAML\Metadata\MetaDataStorageHandler */
protected MetaDataStorageHandler $mdHandler;
/**
* @var \SimpleSAML\IdP
* @psalm-var \SimpleSAML\IdP|class-string
*/
protected $idp = IdP::class;
/**
* Controller constructor.
*
* It initializes the global configuration for the controllers implemented here.
*
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
*/
public function __construct(
protected Configuration $config,
) {
$this->mdHandler = MetaDataStorageHandler::getMetadataHandler();
}
/**
* Inject the \SimpleSAML\IdP dependency.
*
* @param \SimpleSAML\IdP $idp
*/
public function setIdp(IdP $idp): void
{
$this->idp = $idp;
}
/**
* Inject the \SimpleSAML\Metadata\MetaDataStorageHandler dependency.
*
* @param \SimpleSAML\Metadata\MetaDataStorageHandler $mdHandler
*/
public function setMetadataStorageHandler(MetaDataStorageHandler $mdHandler): void
{
$this->mdHandler = $mdHandler;
}
/**
* This SAML 2.0 endpoint can receive incoming LogoutRequests. It will also send LogoutResponses,
* and LogoutRequests and also receive LogoutResponses. It is implementing SLO at the SAML 2.0 IdP.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function singleLogout(Request $request): RunnableResponse
{
Logger::info('SAML2.0 - IdP.SingleLogoutService: Accessing SAML 2.0 IdP endpoint SingleLogoutService');
if ($this->config->getBoolean('enable.saml20-idp') === false || !Module::isModuleEnabled('saml')) {
throw new Error\Error(Error\ErrorCodes::NOACCESS, null, 403);
}
$httpUtils = new Utils\HTTP();
$idpEntityId = $this->mdHandler->getMetaDataCurrentEntityID('saml20-idp-hosted');
$idp = $this->idp::getById('saml2:' . $idpEntityId);
if ($request->query->has('ReturnTo')) {
return new RunnableResponse(
[$idp, 'doLogoutRedirect'],
[$httpUtils->checkURLAllowed($request->query->get('ReturnTo'))],
);
} elseif ($request->request->has('ReturnTo')) {
return $idp->doLogoutRedirect(
$httpUtils->checkURLAllowed($request->request->get('ReturnTo')),
);
}
try {
return new RunnableResponse([Module\saml\IdP\SAML2::class, 'receiveLogoutMessage'], [$idp]);
} catch (UnsupportedBindingException $e) {
throw new Error\Error(Error\ErrorCodes::SLOSERVICEPARAMS, $e, 400);
}
}
/**
* This endpoint will initialize the SLO flow at the SAML 2.0 IdP.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function initSingleLogout(Request $request): RunnableResponse
{
Logger::info('SAML2.0 - IdP.initSLO: Accessing SAML 2.0 IdP endpoint init Single Logout');
if ($this->config->getBoolean('enable.saml20-idp') === false || !Module::isModuleEnabled('saml')) {
throw new Error\Error(Error\ErrorCodes::NOACCESS, null, 403);
}
$idpEntityId = $this->mdHandler->getMetaDataCurrentEntityID('saml20-idp-hosted');
$idp = $this->idp::getById('saml2:' . $idpEntityId);
if (!$request->query->has('RelayState')) {
throw new Error\Error(Error\ErrorCodes::NORELAYSTATE);
}
$httpUtils = new Utils\HTTP();
return new RunnableResponse(
[$idp, 'doLogoutRedirect'],
[$httpUtils->checkURLAllowed($request->query->get('RelayState'))],
);
}
}

View File

@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Controller;
use Exception;
use SAML2\ArtifactResolve;
use SAML2\ArtifactResponse;
use SAML2\DOMDocumentFactory;
use SAML2\Exception\Protocol\UnsupportedBindingException;
use SAML2\SOAP;
use SAML2\XML\saml\Issuer;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Configuration;
use SimpleSAML\Error;
use SimpleSAML\HTTP\RunnableResponse;
use SimpleSAML\IdP;
use SimpleSAML\Logger;
use SimpleSAML\Metadata;
use SimpleSAML\Module;
use SimpleSAML\Store\StoreFactory;
/**
* Controller class for the Web Browser Single Sign On profile.
*
* This class serves the different views available.
*
* @package simplesamlphp/simplesamlphp
*/
class WebBrowserSingleSignOn
{
/**
* Controller constructor.
*
* It initializes the global configuration for the controllers implemented here.
*
* @param \SimpleSAML\Configuration $config The configuration to use by the controllers.
*/
public function __construct(
protected Configuration $config,
) {
}
/**
* The ArtifactResolutionService receives the samlart from the sp.
* And when the artifact is found, it sends a \SAML2\ArtifactResponse.
*
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function artifactResolutionService(): RunnableResponse
{
if ($this->config->getBoolean('enable.saml20-idp') === false || !Module::isModuleEnabled('saml')) {
throw new Error\Error(Error\ErrorCodes::NOACCESS, null, 403);
}
$metadata = Metadata\MetaDataStorageHandler::getMetadataHandler();
$idpEntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
$idpMetadata = $metadata->getMetaDataConfig($idpEntityId, 'saml20-idp-hosted');
if (!$idpMetadata->getOptionalBoolean('saml20.sendartifact', false)) {
throw new Error\Error(Error\ErrorCodes::NOACCESS);
}
$storeType = $this->config->getOptionalString('store.type', 'phpsession');
$store = StoreFactory::getInstance($storeType);
if ($store === false) {
throw new Exception('Unable to send artifact without a datastore configured.');
}
$binding = new SOAP();
try {
$request = $binding->receive();
} catch (UnsupportedBindingException $e) {
throw new Error\Error(Error\ErrorCodes::ARSPARAMS, $e, 400);
}
if (!($request instanceof ArtifactResolve)) {
throw new Exception("Message received on ArtifactResolutionService wasn't a ArtifactResolve request.");
}
$issuer = $request->getIssuer();
/** @psalm-assert \SAML2\XML\saml\Issuer $issuer */
Assert::notNull($issuer);
$issuer = $issuer->getValue();
$spMetadata = $metadata->getMetaDataConfig($issuer, 'saml20-sp-remote');
$artifact = $request->getArtifact();
$responseData = $store->get('artifact', $artifact);
$store->delete('artifact', $artifact);
if ($responseData !== null) {
$document = DOMDocumentFactory::fromString($responseData);
$responseXML = $document->documentElement;
} else {
$responseXML = null;
}
$artifactResponse = new ArtifactResponse();
$issuer = new Issuer();
$issuer->setValue($idpEntityId);
$artifactResponse->setIssuer($issuer);
$artifactResponse->setInResponseTo($request->getId());
$artifactResponse->setAny($responseXML);
Module\saml\Message::addSign($idpMetadata, $spMetadata, $artifactResponse);
return new RunnableResponse([$binding, 'send'], [$artifactResponse]);
}
/**
* The SSOService is part of the SAML 2.0 IdP code, and it receives incoming Authentication Requests
* from a SAML 2.0 SP, parses, and process it, and then authenticates the user and sends the user back
* to the SP with an Authentication Response.
*
* @return \SimpleSAML\HTTP\RunnableResponse
*/
public function singleSignOnService(): RunnableResponse
{
Logger::info('SAML2.0 - IdP.SSOService: Accessing SAML 2.0 IdP endpoint SSOService');
if ($this->config->getBoolean('enable.saml20-idp') === false || !Module::isModuleEnabled('saml')) {
throw new Error\Error(Error\ErrorCodes::NOACCESS, null, 403);
}
$metadata = Metadata\MetaDataStorageHandler::getMetadataHandler();
$idpEntityId = $metadata->getMetaDataCurrentEntityID('saml20-idp-hosted');
$idp = IdP::getById('saml2:' . $idpEntityId);
try {
return new RunnableResponse([Module\saml\IdP\SAML2::class, 'receiveAuthnRequest'], [$idp]);
} catch (UnsupportedBindingException $e) {
throw new Error\Error(Error\ErrorCodes::SSOPARAMS, $e, 400);
}
}
}

View File

@@ -0,0 +1,159 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml;
use SAML2\Constants;
use Throwable;
/**
* Class for representing a SAML 2 error.
*
* @package SimpleSAMLphp
*/
class Error extends \SimpleSAML\Error\Exception
{
/**
* Create a SAML 2 error.
*
* @param string $status The top-level status code.
* @param string|null $subStatus The second-level status code.
* Can be NULL, in which case there is no second-level status code.
* @param string|null $statusMessage The status message.
* Can be NULL, in which case there is no status message.
* @param \Throwable|null $cause The cause of this exception. Can be NULL.
*/
public function __construct(
private string $status,
private ?string $subStatus = null,
private ?string $statusMessage = null,
Throwable $cause = null,
) {
$st = self::shortStatus($status);
if ($subStatus !== null) {
$st .= '/' . self::shortStatus($subStatus);
}
if ($statusMessage !== null) {
$st .= ': ' . $statusMessage;
}
parent::__construct($st, 0, $cause);
}
/**
* Get the top-level status code.
*
* @return string The top-level status code.
*/
public function getStatus(): string
{
return $this->status;
}
/**
* Get the second-level status code.
*
* @return string|null The second-level status code or NULL if no second-level status code is present.
*/
public function getSubStatus(): ?string
{
return $this->subStatus;
}
/**
* Get the status message.
*
* @return string|null The status message or NULL if no status message is present.
*/
public function getStatusMessage(): ?string
{
return $this->statusMessage;
}
/**
* Create a SAML2 error from an exception.
*
* This function attempts to create a SAML2 error with the appropriate
* status codes from an arbitrary exception.
*
* @param \Throwable $e The original exception.
* @return \SimpleSAML\Error\Exception The new exception.
*/
public static function fromException(Throwable $e): \SimpleSAML\Error\Exception
{
if ($e instanceof \SimpleSAML\Module\saml\Error) {
// Return the original exception unchanged
return $e;
} else {
$e = new self(
Constants::STATUS_RESPONDER,
null,
$e::class . ': ' . $e->getMessage(),
$e,
);
}
return $e;
}
/**
* Create a normal exception from a SAML2 error.
*
* This function attempts to reverse the operation of the fromException() function.
* If it is unable to create a more specific exception, it will return the current
* object.
*
* @see \SimpleSAML\Module\saml\Error::fromException()
*
* @return \SimpleSAML\Error\Exception An exception representing this error.
*/
public function toException(): \SimpleSAML\Error\Exception
{
$e = null;
switch ($this->status) {
case Constants::STATUS_RESPONDER:
switch ($this->subStatus) {
case Constants::STATUS_NO_PASSIVE:
$e = new \SimpleSAML\Module\saml\Error\NoPassive(
Constants::STATUS_RESPONDER,
$this->statusMessage,
);
break;
}
break;
}
if ($e === null) {
return $this;
}
return $e;
}
/**
* Create a short version of the status code.
*
* Remove the 'urn:oasis:names:tc:SAML:2.0:status:'-prefix of status codes
* if it is present.
*
* @param string $status The status code.
* @return string A shorter version of the status code.
*/
private static function shortStatus(string $status): string
{
$t = 'urn:oasis:names:tc:SAML:2.0:status:';
if (substr($status, 0, strlen($t)) === $t) {
return substr($status, strlen($t));
}
return $status;
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Error;
use SAML2\Constants;
use Throwable;
/**
* A SAML error indicating that none of the requested Authentication Contexts can be used.
*
* @package SimpleSAMLphp
*/
class NoAuthnContext extends \SimpleSAML\Module\saml\Error
{
/**
* NoAuthnContext error constructor.
*
* @param string $responsible A string telling who is responsible for this error. Can be one of the following:
* - \SAML2\Constants::STATUS_RESPONDER: in case the error is caused by this SAML responder.
* - \SAML2\Constants::STATUS_REQUESTER: in case the error is caused by the SAML requester.
* @param string|null $message A short message explaining why this error happened.
* @param \Throwable|null $cause An exception that caused this error.
*/
public function __construct(string $responsible, string $message = null, Throwable $cause = null)
{
parent::__construct($responsible, Constants::STATUS_NO_AUTHN_CONTEXT, $message, $cause);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Error;
use SAML2\Constants;
use Throwable;
/**
* A SAML error indicating that none of the requested IdPs can be used.
*
* @package SimpleSAMLphp
*/
class NoAvailableIDP extends \SimpleSAML\Module\saml\Error
{
/**
* NoAvailableIDP error constructor.
*
* @param string $responsible A string telling who is responsible for this error. Can be one of the following:
* - \SAML2\Constants::STATUS_RESPONDER: in case the error is caused by this SAML responder.
* - \SAML2\Constants::STATUS_REQUESTER: in case the error is caused by the SAML requester.
* @param string|null $message A short message explaining why this error happened.
* @param \Throwable|null $cause An exception that caused this error.
*/
public function __construct(string $responsible, string $message = null, Throwable $cause = null)
{
parent::__construct($responsible, Constants::STATUS_NO_AVAILABLE_IDP, $message, $cause);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Error;
use SAML2\Constants;
use Throwable;
/**
* A SAML error indicating that passive authentication cannot be used.
*
* @package SimpleSAMLphp
*/
class NoPassive extends \SimpleSAML\Module\saml\Error
{
/**
* NoPassive error constructor.
*
* @param string $responsible A string telling who is responsible for this error. Can be one of the following:
* - \SAML2\Constants::STATUS_RESPONDER: in case the error is caused by this SAML responder.
* - \SAML2\Constants::STATUS_REQUESTER: in case the error is caused by the SAML requester.
* @param string|null $message A short message explaining why this error happened.
* @param \Throwable|null $cause An exception that caused this error.
*/
public function __construct(string $responsible, string $message = null, Throwable $cause = null)
{
parent::__construct($responsible, Constants::STATUS_NO_PASSIVE, $message, $cause);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Error;
use SAML2\Constants;
use Throwable;
/**
* A SAML error indicating that none of the IdPs requested are supported.
*
* @package SimpleSAMLphp
*/
class NoSupportedIDP extends \SimpleSAML\Module\saml\Error
{
/**
* NoSupportedIDP error constructor.
*
* @param string $responsible A string telling who is responsible for this error. Can be one of the following:
* - \SAML2\Constants::STATUS_RESPONDER: in case the error is caused by this SAML responder.
* - \SAML2\Constants::STATUS_REQUESTER: in case the error is caused by the SAML requester.
* @param string|null $message A short message explaining why this error happened.
* @param \Throwable|null $cause An exception that caused this error.
*/
public function __construct(string $responsible, string $message = null, Throwable $cause = null)
{
parent::__construct($responsible, Constants::STATUS_NO_SUPPORTED_IDP, $message, $cause);
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\Error;
use SAML2\Constants;
use Throwable;
/**
* A SAML error indicating that the maximum amount of proxies traversed has been reached.
*
* @package SimpleSAMLphp
*/
class ProxyCountExceeded extends \SimpleSAML\Module\saml\Error
{
/**
* ProxyCountExceeded error constructor.
*
* @param string $responsible A string telling who is responsible for this error. Can be one of the following:
* - \SAML2\Constants::STATUS_RESPONDER: in case the error is caused by this SAML responder.
* - \SAML2\Constants::STATUS_REQUESTER: in case the error is caused by the SAML requester.
* @param string|null $message A short message explaining why this error happened.
* @param \Throwable|null $cause An exception that caused this error.
*/
public function __construct(string $responsible, string $message = null, Throwable $cause = null)
{
parent::__construct($responsible, Constants::STATUS_PROXY_COUNT_EXCEEDED, $message, $cause);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,295 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\IdP;
use Exception;
use PDO;
use PDOStatement;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Configuration;
use SimpleSAML\Database;
use SimpleSAML\Error;
use SimpleSAML\Logger;
use SimpleSAML\Store;
use SimpleSAML\Store\StoreFactory;
/**
* Helper class for working with persistent NameIDs stored in SQL datastore.
*
* @package SimpleSAMLphp
*/
class SQLNameID
{
public const TABLE_VERSION = 1;
public const DEFAULT_TABLE_PREFIX = '';
public const TABLE_SUFFIX = '_saml_PersistentNameID';
/**
* @param string $query
* @param array $params Parameters
* @param array $config
* @return \PDOStatement object
*/
private static function read(string $query, array $params = [], array $config = []): PDOStatement
{
if (!empty($config)) {
$database = Database::getInstance(Configuration::loadFromArray($config));
$stmt = $database->read($query, $params);
} else {
$store = self::getStore();
$stmt = $store->pdo->prepare($query);
$stmt->execute($params);
}
return $stmt;
}
/**
* @param string $query
* @param array $params Parameters
* @param array $config
* @return int|false The number of rows affected by the query or false on error.
*/
private static function write(string $query, array $params = [], array $config = [])
{
if (!empty($config)) {
$database = Database::getInstance(Configuration::loadFromArray($config));
$res = $database->write($query, $params);
} else {
$store = self::getStore();
$query = $store->pdo->prepare($query);
$res = $query->execute($params);
if ($res) {
$res = $query->rowCount();
}
}
return $res;
}
/**
* @param array $config
* @return string
*/
private static function tableName(array $config = []): string
{
$store = empty($config) ? self::getStore() : null;
$prefix = $store === null ? self::DEFAULT_TABLE_PREFIX : $store->prefix;
$table = $prefix . self::TABLE_SUFFIX;
return $table;
}
/**
* @param array $config
*/
private static function create(array $config = []): void
{
$store = empty($config) ? self::getStore() : null;
$table = self::tableName($config);
if ($store === null) {
try {
self::createTable($table, $config);
} catch (Exception $e) {
Logger::debug('SQL persistent NameID table already exists.');
}
} elseif ($store->getTableVersion('saml_PersistentNameID') !== self::TABLE_VERSION) {
self::createTable($table);
$store->setTableVersion('saml_PersistentNameID', self::TABLE_VERSION);
}
}
/**
* @param string $query
* @param array $params
* @param array $config
* @return \PDOStatement
*/
private static function createAndRead(string $query, array $params = [], array $config = []): PDOStatement
{
self::create($config);
return self::read($query, $params, $config);
}
/**
* @param string $query
* @param array $params
* @param array $config
* @return int|false The number of rows affected by the query or false on error.
*/
private static function createAndWrite(string $query, array $params = [], array $config = [])
{
self::create($config);
return self::write($query, $params, $config);
}
/**
* Create NameID table in SQL.
*
* @param string $table The table name.
* @param array $config
*/
private static function createTable(string $table, array $config = []): void
{
$query = 'CREATE TABLE ' . $table . ' (
_idp VARCHAR(256) NOT NULL,
_sp VARCHAR(256) NOT NULL,
_user VARCHAR(256) NOT NULL,
_value VARCHAR(40) NOT NULL,
UNIQUE (_idp, _sp, _user)
)';
self::write($query, [], $config);
$query = 'CREATE INDEX ' . $table . '_idp_sp ON ';
$query .= $table . ' (_idp, _sp)';
self::write($query, [], $config);
}
/**
* Retrieve the SQL datastore.
*
* @return \SimpleSAML\Store\SQLStore SQL datastore.
*/
private static function getStore(): Store\SQLStore
{
$config = Configuration::getInstance();
$storeType = $config->getOptionalString('store.type', 'phpsession');
$store = StoreFactory::getInstance($storeType);
Assert::isInstanceOf(
$store,
Store\SQLStore::class,
'SQL NameID store requires SimpleSAMLphp to be configured with a SQL datastore.',
Error\Exception::class,
);
return $store;
}
/**
* Add a NameID into the database.
*
* @param string $idpEntityId The IdP entityID.
* @param string $spEntityId The SP entityID.
* @param string $user The user's unique identificator (e.g. username).
* @param string $value The NameID value.
* @param array $config
*/
public static function add(
string $idpEntityId,
string $spEntityId,
string $user,
string $value,
array $config = [],
): void {
$params = [
'_idp' => $idpEntityId,
'_sp' => $spEntityId,
'_user' => $user,
'_value' => $value,
];
$query = 'INSERT INTO ' . self::tableName($config);
$query .= ' (_idp, _sp, _user, _value) VALUES(:_idp, :_sp, :_user, :_value)';
self::createAndWrite($query, $params, $config);
}
/**
* Retrieve a NameID into from database.
*
* @param string $idpEntityId The IdP entityID.
* @param string $spEntityId The SP entityID.
* @param string $user The user's unique identificator (e.g. username).
* @param array $config
* @return string|null $value The NameID value, or NULL of no NameID value was found.
*/
public static function get(
string $idpEntityId,
string $spEntityId,
string $user,
array $config = [],
): ?string {
$params = [
'_idp' => $idpEntityId,
'_sp' => $spEntityId,
'_user' => $user,
];
$query = 'SELECT _value FROM ' . self::tableName($config);
$query .= ' WHERE _idp = :_idp AND _sp = :_sp AND _user = :_user';
$query = self::createAndRead($query, $params, $config);
$row = $query->fetch(PDO::FETCH_ASSOC);
if ($row === false) {
// No NameID found
return null;
}
return strval($row['_value']);
}
/**
* Delete a NameID from the database.
*
* @param string $idpEntityId The IdP entityID.
* @param string $spEntityId The SP entityID.
* @param string $user The user's unique identificator (e.g. username).
* @param array $config
*/
public static function delete(
string $idpEntityId,
string $spEntityId,
string $user,
array $config = [],
): void {
$params = [
'_idp' => $idpEntityId,
'_sp' => $spEntityId,
'_user' => $user,
];
$query = 'DELETE FROM ' . self::tableName($config);
$query .= ' WHERE _idp = :_idp AND _sp = :_sp AND _user = :_user';
self::createAndWrite($query, $params, $config);
}
/**
* Retrieve all federated identities for an IdP-SP pair.
*
* @param string $idpEntityId The IdP entityID.
* @param string $spEntityId The SP entityID.
* @param array $config
* @return array Array of userid => NameID.
*/
public static function getIdentities(string $idpEntityId, string $spEntityId, array $config = []): array
{
$params = [
'_idp' => $idpEntityId,
'_sp' => $spEntityId,
];
$query = 'SELECT _user, _value FROM ' . self::tableName($config);
$query .= ' WHERE _idp = :_idp AND _sp = :_sp';
$query = self::createAndRead($query, $params, $config);
$res = [];
while (($row = $query->fetch(PDO::FETCH_ASSOC)) !== false) {
$user = strval($row['_user']);
$value = strval($row['_value']);
$res[$user] = $value;
}
return $res;
}
}

View File

@@ -0,0 +1,893 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml;
use RobRichards\XMLSecLibs\XMLSecurityKey;
use SAML2\Assertion;
use SAML2\AuthnRequest;
use SAML2\Constants;
use SAML2\EncryptedAssertion;
use SAML2\LogoutRequest;
use SAML2\LogoutResponse;
use SAML2\Response;
use SAML2\SignedElement;
use SAML2\StatusResponse;
use SAML2\XML\ds\KeyInfo;
use SAML2\XML\ds\X509Certificate;
use SAML2\XML\ds\X509Data;
use SAML2\XML\saml\Issuer;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Configuration;
use SimpleSAML\Error as SSP_Error;
use SimpleSAML\Logger;
use SimpleSAML\Utils;
/**
* Common code for building SAML 2 messages based on the available metadata.
*
* @package SimpleSAMLphp
*/
class Message
{
/**
* Add signature key and sender certificate to an element (Message or Assertion).
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
* @param \SAML2\SignedElement $element The element we should add the data to.
*/
public static function addSign(
Configuration $srcMetadata,
Configuration $dstMetadata,
SignedElement $element,
): void {
$dstPrivateKey = $dstMetadata->getOptionalString('signature.privatekey', null);
$cryptoUtils = new Utils\Crypto();
if ($dstPrivateKey !== null) {
/** @var array $keyArray */
$keyArray = $cryptoUtils->loadPrivateKey($dstMetadata, true, 'signature.');
$certArray = $cryptoUtils->loadPublicKey($dstMetadata, false, 'signature.');
} else {
/** @var array $keyArray */
$keyArray = $cryptoUtils->loadPrivateKey($srcMetadata, true);
$certArray = $cryptoUtils->loadPublicKey($srcMetadata, false);
}
$algo = $dstMetadata->getOptionalString('signature.algorithm', null);
if ($algo === null) {
$algo = $srcMetadata->getOptionalString('signature.algorithm', XMLSecurityKey::RSA_SHA256);
}
$privateKey = new XMLSecurityKey($algo, ['type' => 'private']);
if (array_key_exists('password', $keyArray)) {
$privateKey->passphrase = $keyArray['password'];
}
$privateKey->loadKey($keyArray['PEM'], false);
$element->setSignatureKey($privateKey);
if ($certArray === null) {
// we don't have a certificate to add
return;
}
if (!array_key_exists('PEM', $certArray)) {
// we have a public key with only a fingerprint
return;
}
$element->setCertificates([$certArray['PEM']]);
}
/**
* Add signature key and and senders certificate to message.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
* @param \SAML2\Message $message The message we should add the data to.
*/
private static function addRedirectSign(
Configuration $srcMetadata,
Configuration $dstMetadata,
\SAML2\Message $message,
): void {
$signingEnabled = null;
if ($message instanceof LogoutRequest || $message instanceof LogoutResponse) {
$signingEnabled = $srcMetadata->getOptionalBoolean('sign.logout', null);
if ($signingEnabled === null) {
$signingEnabled = $dstMetadata->getOptionalBoolean('sign.logout', null);
}
} elseif ($message instanceof AuthnRequest) {
$signingEnabled = $srcMetadata->getOptionalBoolean('sign.authnrequest', null);
if ($signingEnabled === null) {
$signingEnabled = $dstMetadata->getOptionalBoolean('sign.authnrequest', null);
}
}
if ($signingEnabled === null) {
$signingEnabled = $dstMetadata->getOptionalBoolean('redirect.sign', null);
if ($signingEnabled === null) {
$signingEnabled = $srcMetadata->getOptionalBoolean('redirect.sign', false);
}
}
if (!$signingEnabled) {
return;
}
self::addSign($srcMetadata, $dstMetadata, $message);
}
/**
* Check the signature on a SAML2 message or assertion.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
* @param \SAML2\SignedElement $element Either a \SAML2\Response or a \SAML2\Assertion.
* @return bool True if the signature is correct, false otherwise.
*
* @throws \SimpleSAML\Error\Exception if there is not certificate in the metadata for the entity.
* @throws \Exception if the signature validation fails with an exception.
*/
public static function checkSign(Configuration $srcMetadata, SignedElement $element): bool
{
// find the public key that should verify signatures by this entity
$keys = $srcMetadata->getPublicKeys('signing');
if (!empty($keys)) {
$pemKeys = [];
foreach ($keys as $key) {
switch ($key['type']) {
case 'X509Certificate':
$pemKeys[] = "-----BEGIN CERTIFICATE-----\n" .
chunk_split($key['X509Certificate'], 64) .
"-----END CERTIFICATE-----\n";
break;
default:
Logger::debug('Skipping unknown key type: ' . $key['type']);
}
}
} else {
throw new SSP_Error\Exception(
'Missing certificate in metadata for ' .
var_export($srcMetadata->getString('entityid'), true),
);
}
Logger::debug('Has ' . count($pemKeys) . ' candidate keys for validation.');
$lastException = null;
foreach ($pemKeys as $i => $pem) {
$key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'public']);
$key->loadKey($pem);
try {
// make sure that we have a valid signature on either the response or the assertion
$res = $element->validate($key);
if ($res) {
Logger::debug('Validation with key #' . $i . ' succeeded.');
return true;
}
Logger::debug('Validation with key #' . $i . ' failed without exception.');
} catch (\Exception $e) {
Logger::debug('Validation with key #' . $i . ' failed with exception: ' . $e->getMessage());
$lastException = $e;
}
}
// we were unable to validate the signature with any of our keys
if ($lastException !== null) {
throw $lastException;
} else {
return false;
}
}
/**
* Check signature on a SAML2 message if enabled.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
* @param \SAML2\Message $message The message we should check the signature on.
* @return bool Whether or not the message was validated.
*
* @throws \SimpleSAML\Error\Exception if message validation is enabled, but there is no signature in the message.
*/
public static function validateMessage(
Configuration $srcMetadata,
Configuration $dstMetadata,
\SAML2\Message $message,
): bool {
$enabled = null;
if ($message instanceof LogoutRequest || $message instanceof LogoutResponse) {
$enabled = $srcMetadata->getOptionalBoolean('validate.logout', null);
if ($enabled === null) {
$enabled = $dstMetadata->getOptionalBoolean('validate.logout', null);
}
} elseif ($message instanceof AuthnRequest) {
$enabled = $srcMetadata->getOptionalBoolean('validate.authnrequest', null);
if ($enabled === null) {
$enabled = $dstMetadata->getOptionalBoolean('validate.authnrequest', null);
}
}
// If not specifically set to false, the signature must be checked to conform to SAML2INT
if (
(isset($_REQUEST['Signature'])
|| $message->isMessageConstructedWithSignature() === true)
&& ($enabled !== false)
) {
$enabled = true;
} elseif ($enabled === null) {
$enabled = $srcMetadata->getOptionalBoolean('redirect.validate', null);
if ($enabled === null) {
$enabled = $dstMetadata->getOptionalBoolean('redirect.validate', false);
}
}
if (!$enabled) {
return false;
} elseif (!self::checkSign($srcMetadata, $message)) {
throw new SSP_Error\Exception(
'Validation of received messages enabled, but no signature found on message.',
);
}
return true;
}
/**
* Retrieve the decryption keys from metadata.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender (IdP).
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient (SP).
* @param string|null $encryptionMethod
* The EncryptionMethod from the assertion.
*
* @return array Array of decryption keys.
*/
public static function getDecryptionKeys(
Configuration $srcMetadata,
Configuration $dstMetadata,
$encryptionMethod = null,
): array {
$sharedKey = $srcMetadata->getOptionalString('sharedkey', null);
if ($sharedKey !== null) {
if ($encryptionMethod !== null) {
// @TODO: for saml2v5 replace this line
//$algo = $encryptionMethod->getAlgorithm();
$algo = $encryptionMethod;
} else {
$algo = $srcMetadata->getOptionalString('sharedkey_algorithm', null);
if ($algo === null) {
// If no algorithm is supplied or configured, use a sane default as a last resort
$algo = $dstMetadata->getOptionalString('sharedkey_algorithm', XMLSecurityKey::AES128_GCM);
}
}
$key = new XMLSecurityKey($algo);
$key->loadKey($sharedKey);
return [$key];
}
$keys = [];
$cryptoUtils = new Utils\Crypto();
// load the new private key if it exists
$keyArray = $cryptoUtils->loadPrivateKey($dstMetadata, false, 'new_');
if ($keyArray !== null) {
assert::keyExists($keyArray, 'PEM');
$key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, ['type' => 'private']);
if (array_key_exists('password', $keyArray)) {
$key->passphrase = $keyArray['password'];
}
$key->loadKey($keyArray['PEM']);
$keys[] = $key;
}
/**
* find the existing private key
*
* @var array $keyArray Because the second param is true
*/
$keyArray = $cryptoUtils->loadPrivateKey($dstMetadata, true);
Assert::keyExists($keyArray, 'PEM');
$key = new XMLSecurityKey(XMLSecurityKey::RSA_1_5, ['type' => 'private']);
if (array_key_exists('password', $keyArray)) {
$key->passphrase = $keyArray['password'];
}
$key->loadKey($keyArray['PEM']);
$keys[] = $key;
return $keys;
}
/**
* Retrieve blacklisted algorithms.
*
* Remote configuration overrides local configuration.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
*
* @return array Array of blacklisted algorithms.
*/
public static function getBlacklistedAlgorithms(
Configuration $srcMetadata,
Configuration $dstMetadata,
): array {
$blacklist = $srcMetadata->getOptionalArray('encryption.blacklisted-algorithms', null);
if ($blacklist === null) {
$blacklist = $dstMetadata->getOptionalArray('encryption.blacklisted-algorithms', [XMLSecurityKey::RSA_1_5]);
}
return $blacklist;
}
/**
* Decrypt an assertion.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender (IdP).
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient (SP).
* @param \SAML2\Assertion|\SAML2\EncryptedAssertion $assertion The assertion we are decrypting.
*
* @return \SAML2\Assertion The assertion.
*
* @throws \SimpleSAML\Error\Exception if encryption is enabled but the assertion is not encrypted, or if we cannot
* get the decryption keys.
* @throws \Exception if decryption fails for whatever reason.
*/
private static function decryptAssertion(
Configuration $srcMetadata,
Configuration $dstMetadata,
Assertion|EncryptedAssertion $assertion,
): Assertion {
if ($assertion instanceof Assertion) {
$encryptAssertion = $srcMetadata->getOptionalBoolean('assertion.encryption', null);
if ($encryptAssertion === null) {
$encryptAssertion = $dstMetadata->getOptionalBoolean('assertion.encryption', false);
}
if ($encryptAssertion) {
/* The assertion was unencrypted, but we have encryption enabled. */
throw new \Exception('Received unencrypted assertion, but encryption was enabled.');
}
return $assertion;
}
try {
// @todo Enable this code for saml2v5 to automatically determine encryption algorithm
//$encryptionMethod = $assertion->getEncryptedData()->getEncryptionMethod();
//$keys = self::getDecryptionKeys($srcMetadata, $dstMetadata, $encryptionMethod);
$encryptionMethod = null;
$keys = self::getDecryptionKeys($srcMetadata, $dstMetadata, $encryptionMethod);
} catch (\Exception $e) {
throw new SSP_Error\Exception('Error decrypting assertion: ' . $e->getMessage());
}
$blacklist = self::getBlacklistedAlgorithms($srcMetadata, $dstMetadata);
$lastException = null;
foreach ($keys as $i => $key) {
try {
$ret = $assertion->getAssertion($key, $blacklist);
Logger::debug('Decryption with key #' . $i . ' succeeded.');
return $ret;
} catch (\Exception $e) {
Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage());
$lastException = $e;
}
}
/**
* The annotation below is not working - See vimeo/psalm#1909
* @psalm-suppress InvalidThrow
* @var \Exception $lastException
*/
throw $lastException;
}
/**
* Decrypt any encrypted attributes in an assertion.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender (IdP).
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient (SP).
* @param \SAML2\Assertion|\SAML2\Assertion $assertion The assertion containing any possibly encrypted attributes.
*
*
* @throws \SimpleSAML\Error\Exception if we cannot get the decryption keys or decryption fails.
*/
private static function decryptAttributes(
Configuration $srcMetadata,
Configuration $dstMetadata,
Assertion &$assertion,
): void {
if (!$assertion->hasEncryptedAttributes()) {
return;
}
try {
$keys = self::getDecryptionKeys($srcMetadata, $dstMetadata);
} catch (\Exception $e) {
throw new SSP_Error\Exception('Error decrypting attributes: ' . $e->getMessage());
}
$blacklist = self::getBlacklistedAlgorithms($srcMetadata, $dstMetadata);
$error = true;
foreach ($keys as $i => $key) {
try {
$assertion->decryptAttributes($key, $blacklist);
Logger::debug('Attribute decryption with key #' . $i . ' succeeded.');
$error = false;
break;
} catch (\Exception $e) {
Logger::debug('Attribute decryption failed with exception: ' . $e->getMessage());
}
}
if ($error) {
throw new SSP_Error\Exception('Could not decrypt the attributes');
}
}
/**
* Retrieve the status code of a response as a \SimpleSAML\Module\saml\Error.
*
* @param \SAML2\StatusResponse $response The response.
*
* @return \SimpleSAML\Module\saml\Error The error.
*/
public static function getResponseError(StatusResponse $response): \SimpleSAML\Module\saml\Error
{
$status = $response->getStatus();
return new \SimpleSAML\Module\saml\Error($status['Code'], $status['SubCode'], $status['Message']);
}
/**
* Build an authentication request based on information in the metadata.
*
* @param \SimpleSAML\Configuration $spMetadata The metadata of the service provider.
* @param \SimpleSAML\Configuration $idpMetadata The metadata of the identity provider.
* @return \SAML2\AuthnRequest An authentication request object.
*/
public static function buildAuthnRequest(
Configuration $spMetadata,
Configuration $idpMetadata,
): AuthnRequest {
$ar = new AuthnRequest();
// get the NameIDPolicy to apply. IdP metadata has precedence.
$nameIdPolicy = null;
if ($idpMetadata->hasValue('NameIDPolicy')) {
$nameIdPolicy = $idpMetadata->getValue('NameIDPolicy');
} elseif ($spMetadata->hasValue('NameIDPolicy')) {
$nameIdPolicy = $spMetadata->getValue('NameIDPolicy');
}
$policy = Utils\Config\Metadata::parseNameIdPolicy($nameIdPolicy);
// empty array signals not to set any NameIdPolicy element
if ($policy !== []) {
$ar->setNameIdPolicy($policy);
}
$ar->setForceAuthn($spMetadata->getOptionalBoolean('ForceAuthn', false));
$ar->setIsPassive($spMetadata->getOptionalBoolean('IsPassive', false));
$protbind = $spMetadata->getOptionalValueValidate('ProtocolBinding', [
Constants::BINDING_HTTP_POST,
Constants::BINDING_HOK_SSO,
Constants::BINDING_HTTP_ARTIFACT,
Constants::BINDING_HTTP_REDIRECT,
], Constants::BINDING_HTTP_POST);
// Shoaib: setting the appropriate binding based on parameter in sp-metadata defaults to HTTP_POST
$ar->setProtocolBinding($protbind);
$issuer = new Issuer();
$issuer->setValue($spMetadata->getString('entityID'));
$ar->setIssuer($issuer);
$ar->setAssertionConsumerServiceIndex(
$spMetadata->getOptionalInteger('AssertionConsumerServiceIndex', null),
);
$ar->setAttributeConsumingServiceIndex(
$spMetadata->getOptionalInteger('AttributeConsumingServiceIndex', null),
);
if ($spMetadata->hasValue('AuthnContextClassRef')) {
$accr = $spMetadata->getArrayizeString('AuthnContextClassRef');
$comp = $spMetadata->getOptionalValueValidate('AuthnContextComparison', [
Constants::COMPARISON_EXACT,
Constants::COMPARISON_MINIMUM,
Constants::COMPARISON_MAXIMUM,
Constants::COMPARISON_BETTER,
], Constants::COMPARISON_EXACT);
$ar->setRequestedAuthnContext(['AuthnContextClassRef' => $accr, 'Comparison' => $comp]);
}
self::addRedirectSign($spMetadata, $idpMetadata, $ar);
return $ar;
}
/**
* Build a logout request based on information in the metadata.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
* @return \SAML2\LogoutRequest A logout request object.
*/
public static function buildLogoutRequest(
Configuration $srcMetadata,
Configuration $dstMetadata,
): LogoutRequest {
$lr = new LogoutRequest();
$issuer = new Issuer();
$issuer->setValue($srcMetadata->getString('entityid'));
$issuer->setFormat(Constants::NAMEID_ENTITY);
$lr->setIssuer($issuer);
self::addRedirectSign($srcMetadata, $dstMetadata, $lr);
return $lr;
}
/**
* Build a logout response based on information in the metadata.
*
* @param \SimpleSAML\Configuration $srcMetadata The metadata of the sender.
* @param \SimpleSAML\Configuration $dstMetadata The metadata of the recipient.
* @return \SAML2\LogoutResponse A logout response object.
*/
public static function buildLogoutResponse(
Configuration $srcMetadata,
Configuration $dstMetadata,
): LogoutResponse {
$lr = new LogoutResponse();
$issuer = new Issuer();
$issuer->setValue($srcMetadata->getString('entityid'));
$issuer->setFormat(Constants::NAMEID_ENTITY);
$lr->setIssuer($issuer);
self::addRedirectSign($srcMetadata, $dstMetadata, $lr);
return $lr;
}
/**
* Process a response message.
*
* If the response is an error response, we will throw a \SimpleSAML\Module\saml\Error exception with the error.
*
* @param \SimpleSAML\Configuration $spMetadata The metadata of the service provider.
* @param \SimpleSAML\Configuration $idpMetadata The metadata of the identity provider.
* @param \SAML2\Response $response The response.
*
* @return array Array with \SAML2\Assertion objects, containing valid assertions from the response.
*
* @throws \SimpleSAML\Error\Exception if there are no assertions in the response.
* @throws \Exception if the destination of the response does not match the current URL.
*/
public static function processResponse(
Configuration $spMetadata,
Configuration $idpMetadata,
Response $response,
): array {
if (!$response->isSuccess()) {
throw self::getResponseError($response);
}
// validate Response-element destination
$httpUtils = new \SimpleSAML\Utils\HTTP();
$currentURL = $httpUtils->getSelfURLNoQuery();
$msgDestination = $response->getDestination();
if ($msgDestination !== null && $msgDestination !== $currentURL) {
throw new \Exception('Destination in response doesn\'t match the current URL. Destination is "' .
$msgDestination . '", current URL is "' . $currentURL . '".');
}
$responseSigned = self::checkSign($idpMetadata, $response);
/*
* When we get this far, the response itself is valid.
* We only need to check signatures and conditions of the response.
*/
$assertion = $response->getAssertions();
if (empty($assertion)) {
throw new SSP_Error\Exception('No assertions found in response from IdP.');
}
$ret = [];
foreach ($assertion as $a) {
$ret[] = self::processAssertion($spMetadata, $idpMetadata, $response, $a, $responseSigned);
}
return $ret;
}
/**
* Process an assertion in a response.
*
* @param \SimpleSAML\Configuration $spMetadata The metadata of the service provider.
* @param \SimpleSAML\Configuration $idpMetadata The metadata of the identity provider.
* @param \SAML2\Response $response The response containing the assertion.
* @param \SAML2\Assertion|\SAML2\EncryptedAssertion $assertion The assertion.
* @param bool $responseSigned Whether the response is signed.
*
* @return \SAML2\Assertion The assertion, if it is valid.
*
* @throws \SimpleSAML\Error\Exception if an error occurs while trying to validate the assertion, or if a assertion
* is not signed and it should be, or if we are unable to decrypt the NameID due to a local failure (missing or
* invalid decryption key).
* @throws \Exception if we couldn't decrypt the NameID for unexpected reasons.
*/
private static function processAssertion(
Configuration $spMetadata,
Configuration $idpMetadata,
Response $response,
Assertion|EncryptedAssertion $assertion,
bool $responseSigned,
): Assertion {
$assertion = self::decryptAssertion($idpMetadata, $spMetadata, $assertion);
self::decryptAttributes($idpMetadata, $spMetadata, $assertion);
if (!self::checkSign($idpMetadata, $assertion)) {
if (!$responseSigned) {
throw new SSP_Error\Exception('Neither the assertion nor the response was signed.');
}
} // at least one valid signature found
$httpUtils = new Utils\HTTP();
$currentURL = $httpUtils->getSelfURLNoQuery();
// check various properties of the assertion
$config = Configuration::getInstance();
$allowed_clock_skew = $config->getOptionalInteger('assertion.allowed_clock_skew', 180);
$options = [
'options' => [
'default' => 180,
'min_range' => 180,
'max_range' => 300,
],
];
$allowed_clock_skew = filter_var($allowed_clock_skew, FILTER_VALIDATE_INT, $options);
$notBefore = $assertion->getNotBefore();
if ($notBefore !== null && $notBefore > time() + $allowed_clock_skew) {
throw new SSP_Error\Exception(
'Received an assertion that is valid in the future. Check clock synchronization on IdP and SP.',
);
}
$notOnOrAfter = $assertion->getNotOnOrAfter();
if ($notOnOrAfter !== null && $notOnOrAfter <= time() - $allowed_clock_skew) {
throw new SSP_Error\Exception(
'Received an assertion that has expired. Check clock synchronization on IdP and SP.',
);
}
$sessionNotOnOrAfter = $assertion->getSessionNotOnOrAfter();
if ($sessionNotOnOrAfter !== null && $sessionNotOnOrAfter <= time() - $allowed_clock_skew) {
throw new SSP_Error\Exception(
'Received an assertion with a session that has expired. Check clock synchronization on IdP and SP.',
);
}
$validAudiences = $assertion->getValidAudiences();
if ($validAudiences !== null) {
$spEntityId = $spMetadata->getString('entityid');
if (!in_array($spEntityId, $validAudiences, true)) {
$candidates = '[' . implode('], [', $validAudiences) . ']';
throw new SSP_Error\Exception(
'This SP [' . $spEntityId .
'] is not a valid audience for the assertion. Candidates were: ' . $candidates,
);
}
}
$found = false;
$lastError = 'No SubjectConfirmation element in Subject.';
$validSCMethods = [Constants::CM_BEARER, Constants::CM_HOK, Constants::CM_VOUCHES];
foreach ($assertion->getSubjectConfirmation() as $sc) {
$method = $sc->getMethod();
if (!in_array($method, $validSCMethods, true)) {
$lastError = 'Invalid Method on SubjectConfirmation: ' . var_export($method, true);
continue;
}
// is SSO with HoK enabled? IdP remote metadata overwrites SP metadata configuration
$hok = $idpMetadata->getOptionalBoolean('saml20.hok.assertion', null);
if ($hok === null) {
$hok = $spMetadata->getOptionalBoolean('saml20.hok.assertion', false);
}
if ($method === Constants::CM_BEARER && $hok) {
$lastError = 'Bearer SubjectConfirmation received, but Holder-of-Key SubjectConfirmation needed';
continue;
}
if ($method === Constants::CM_HOK && !$hok) {
$lastError = 'Holder-of-Key SubjectConfirmation received, ' .
'but the Holder-of-Key profile is not enabled.';
continue;
}
$scd = $sc->getSubjectConfirmationData();
if ($method === Constants::CM_HOK) {
// check HoK Assertion
if ($httpUtils->isHTTPS() === false) {
$lastError = 'No HTTPS connection, but required for Holder-of-Key SSO';
continue;
}
if (isset($_SERVER['SSL_CLIENT_CERT']) && empty($_SERVER['SSL_CLIENT_CERT'])) {
$lastError = 'No client certificate provided during TLS Handshake with SP';
continue;
}
// extract certificate data (if this is a certificate)
$clientCert = $_SERVER['SSL_CLIENT_CERT'];
$pattern = '/^-----BEGIN CERTIFICATE-----([^-]*)^-----END CERTIFICATE-----/m';
if (!preg_match($pattern, $clientCert, $matches)) {
$lastError = 'Error while looking for client certificate during TLS handshake with SP, ' .
'the client certificate does not have the expected structure';
continue;
}
// we have a valid client certificate from the browser
$clientCert = str_replace(["\r", "\n", " "], '', $matches[1]);
$keyInfo = [];
foreach ($scd->getInfo() as $thing) {
if ($thing instanceof KeyInfo) {
$keyInfo[] = $thing;
}
}
if (count($keyInfo) != 1) {
$lastError = 'Error validating Holder-of-Key assertion: Only one <ds:KeyInfo> element in ' .
'<SubjectConfirmationData> allowed';
continue;
}
$x509data = [];
foreach ($keyInfo[0]->getInfo() as $thing) {
if ($thing instanceof X509Data) {
$x509data[] = $thing;
}
}
if (count($x509data) != 1) {
$lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Data> element in ' .
'<ds:KeyInfo> within <SubjectConfirmationData> allowed';
continue;
}
$x509cert = [];
foreach ($x509data[0]->getData() as $thing) {
if ($thing instanceof X509Certificate) {
$x509cert[] = $thing;
}
}
if (count($x509cert) != 1) {
$lastError = 'Error validating Holder-of-Key assertion: Only one <ds:X509Certificate> element in ' .
'<ds:X509Data> within <SubjectConfirmationData> allowed';
continue;
}
$HoKCertificate = $x509cert[0]->getCertificate();
if ($HoKCertificate !== $clientCert) {
$lastError = 'Provided client certificate does not match the certificate bound to the ' .
'Holder-of-Key assertion';
continue;
}
}
// if no SubjectConfirmationData then don't do anything.
if ($scd === null) {
$lastError = 'No SubjectConfirmationData provided';
continue;
}
$notBefore = $scd->getNotBefore();
if (is_int($notBefore) && $notBefore > time() + 60) {
$lastError = 'NotBefore in SubjectConfirmationData is in the future: ' . $notBefore;
continue;
}
$notOnOrAfter = $scd->getNotOnOrAfter();
if (is_int($notOnOrAfter) && $notOnOrAfter <= time() - 60) {
$lastError = 'NotOnOrAfter in SubjectConfirmationData is in the past: ' . $notOnOrAfter;
continue;
}
$recipient = $scd->getRecipient();
if ($recipient !== null && $recipient !== $currentURL) {
$lastError = 'Recipient in SubjectConfirmationData does not match the current URL. Recipient is ' .
var_export($recipient, true) . ', current URL is ' . var_export($currentURL, true) . '.';
continue;
}
$inResponseTo = $scd->getInResponseTo();
if (
$inResponseTo !== null
&& $response->getInResponseTo() !== null
&& $inResponseTo !== $response->getInResponseTo()
) {
$lastError = 'InResponseTo in SubjectConfirmationData does not match the Response. Response has ' .
var_export($response->getInResponseTo(), true) .
', SubjectConfirmationData has ' . var_export($inResponseTo, true) . '.';
continue;
}
$found = true;
break;
}
if (!$found) {
throw new SSP_Error\Exception('Error validating SubjectConfirmation in Assertion: ' . $lastError);
}
// as far as we can tell, the assertion is valid
// decrypt the NameID element if it is encrypted
if ($assertion->isNameIdEncrypted()) {
try {
$keys = self::getDecryptionKeys($idpMetadata, $spMetadata);
} catch (\Exception $e) {
throw new SSP_Error\Exception('Error decrypting NameID: ' . $e->getMessage());
}
$blacklist = self::getBlacklistedAlgorithms($idpMetadata, $spMetadata);
$lastException = null;
foreach ($keys as $i => $key) {
try {
$assertion->decryptNameId($key, $blacklist);
Logger::debug('Decryption with key #' . $i . ' succeeded.');
$lastException = null;
break;
} catch (\Exception $e) {
Logger::debug('Decryption with key #' . $i . ' failed with exception: ' . $e->getMessage());
$lastException = $e;
}
}
if ($lastException !== null) {
throw $lastException;
}
}
return $assertion;
}
/**
* Retrieve the encryption key for the given entity.
*
* @param \SimpleSAML\Configuration $metadata The metadata of the entity.
*
* @return \RobRichards\XMLSecLibs\XMLSecurityKey The encryption key.
*
* @throws \SimpleSAML\Error\Exception if there is no supported encryption key in the metadata of this entity.
*/
public static function getEncryptionKey(Configuration $metadata): XMLSecurityKey
{
$sharedKey = $metadata->getOptionalString('sharedkey', null);
if ($sharedKey !== null) {
$key = new XMLSecurityKey(XMLSecurityKey::AES128_CBC);
$key->loadKey($sharedKey);
return $key;
}
$keys = $metadata->getPublicKeys('encryption', true);
foreach ($keys as $key) {
switch ($key['type']) {
case 'X509Certificate':
$pemKey = "-----BEGIN CERTIFICATE-----\n" .
chunk_split($key['X509Certificate'], 64) .
"-----END CERTIFICATE-----\n";
$key = new XMLSecurityKey(XMLSecurityKey::RSA_OAEP_MGF1P, ['type' => 'public']);
$key->loadKey($pemKey);
return $key;
}
}
throw new SSP_Error\Exception('No supported encryption key in ' .
var_export($metadata->getString('entityid'), true));
}
}

View File

@@ -0,0 +1,396 @@
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\saml\SP;
use Exception;
use PDO;
use SAML2\XML\saml\NameID;
use SimpleSAML\Assert\Assert;
use SimpleSAML\Configuration;
use SimpleSAML\Logger;
use SimpleSAML\Session;
use SimpleSAML\Store;
use SimpleSAML\Store\StoreFactory;
use SimpleSAML\Store\StoreInterface;
use SimpleSAML\Utils;
/**
* A directory over logout information.
*
* @package SimpleSAMLphp
*/
class LogoutStore
{
/**
* Create logout table in SQL, if it is missing.
*
* @param \SimpleSAML\Store\SQLStore $store The datastore.
*/
private static function createLogoutTable(Store\SQLStore $store): void
{
$tableVer = $store->getTableVersion('saml_LogoutStore');
if ($tableVer === 5) {
return;
} elseif ($tableVer === 4) {
// The _authSource index is being changed from UNIQUE to PRIMARY KEY for table version 5.
switch ($store->driver) {
case 'pgsql':
// Drop old index and add primary key
$update = [
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore DROP CONSTRAINT IF EXISTS ' .
$store->prefix . '_saml_LogoutStore___authSource__nameId__sessionIndex_key',
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore ADD PRIMARY KEY ' .
'(_authSource, _nameId, _sessionIndex)',
];
break;
case 'sqlsrv':
/**
* Drop old index and add primary key.
* NOTE: We get the name of the index by looking for the only unique index with a default name.
*/
$update = [
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore DROP INDEX IF EXISTS SELECT CONSTRAINT_NAME ' .
'FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE TABLE_NAME=' . $store->prefix . '_saml_LogoutStore ' .
'AND CONSTRAINT_NAME LIKE \'UQ__%"\'',
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore ADD CONSTRAINT _authSource ' .
'PRIMARY KEY CLUSTERED (_authSource, _nameId, _sessionIndex)',
];
break;
case 'sqlite':
/**
* Because SQLite does not support field alterations, the approach is to:
* Create a new table with the primary key
* Copy the current data to the new table
* Drop the old table
* Rename the new table correctly
*/
$update = [
'CREATE TABLE ' . $store->prefix .
'_saml_LogoutStore_new (_authSource VARCHAR(255) NOT NULL,' .
'_nameId VARCHAR(40) NOT NULL, _sessionIndex VARCHAR(50) NOT NULL, ' .
'_expire TIMESTAMP NOT NULL, _sessionId VARCHAR(50) NOT NULL, PRIMARY KEY' .
'(_authSource, _nameID, _sessionIndex))',
'INSERT INTO ' . $store->prefix . '_saml_LogoutStore_new SELECT * FROM ' .
$store->prefix . '_saml_LogoutStore',
'DROP TABLE ' . $store->prefix . '_saml_LogoutStore',
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore_new RENAME TO ' .
$store->prefix . '_saml_LogoutStore',
'CREATE INDEX ' . $store->prefix . '_saml_LogoutStore_expire ON ' .
$store->prefix . '_saml_LogoutStore (_expire)',
'CREATE INDEX ' . $store->prefix . '_saml_LogoutStore_nameId ON ' .
$store->prefix . '_saml_LogoutStore (_authSource, _nameId)',
];
break;
case 'mysql':
// Drop old index and add primary key
$update = [
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore DROP INDEX _authSource',
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore ADD PRIMARY KEY ' .
'(_authSource(191), _nameId, _sessionIndex)',
];
break;
default:
// Drop old index and add primary key
$update = [
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore DROP INDEX _authSource',
'ALTER TABLE ' . $store->prefix . '_saml_LogoutStore ADD PRIMARY KEY ' .
'(_authSource, _nameId, _sessionIndex)',
];
break;
}
try {
foreach ($update as $query) {
$store->pdo->exec($query);
}
} catch (Exception $e) {
Logger::warning('Database error: ' . var_export($store->pdo->errorInfo(), true));
return;
}
$store->setTableVersion('saml_LogoutStore', 5);
return;
} elseif ($tableVer < 4 && $tableVer > 0) {
throw new Exception(
'No upgrade path available. Please migrate to the latest 1.18+ '
. 'version of SimpleSAMLphp first before upgrading to 2.x.',
);
}
$query = 'CREATE TABLE ' . $store->prefix . '_saml_LogoutStore (
_authSource VARCHAR(' . ($store->driver === 'mysql' ? '191' : '255') . ') NOT NULL,
_nameId VARCHAR(40) NOT NULL,
_sessionIndex VARCHAR(50) NOT NULL,
_expire ' . ($store->driver === 'pgsql' ? 'TIMESTAMP' : 'DATETIME') . ' NOT NULL,
_sessionId VARCHAR(50) NOT NULL,
PRIMARY KEY (_authSource' . ($store->driver === 'mysql' ? '(191)' : '') . ', _nameId, _sessionIndex)
)';
$store->pdo->exec($query);
$query = 'CREATE INDEX ' . $store->prefix . '_saml_LogoutStore_expire ON ';
$query .= $store->prefix . '_saml_LogoutStore (_expire)';
$store->pdo->exec($query);
$query = 'CREATE INDEX ' . $store->prefix . '_saml_LogoutStore_nameId ON ';
$query .= $store->prefix . '_saml_LogoutStore (_authSource' . ($store->driver === 'mysql' ? '(191)' : '') .
', _nameId)';
$store->pdo->exec($query);
$store->setTableVersion('saml_LogoutStore', 5);
}
/**
* Clean the logout table of expired entries.
*
* @param \SimpleSAML\Store\SQLStore $store The datastore.
*/
private static function cleanLogoutStore(Store\SQLStore $store): void
{
Logger::debug('saml.LogoutStore: Cleaning logout store.');
$query = 'DELETE FROM ' . $store->prefix . '_saml_LogoutStore WHERE _expire < :now';
$params = ['now' => gmdate('Y-m-d H:i:s')];
$query = $store->pdo->prepare($query);
$query->execute($params);
}
/**
* Register a session in the SQL datastore.
*
* @param \SimpleSAML\Store\SQLStore $store The datastore.
* @param string $authId The authsource ID.
* @param string $nameId The hash of the users NameID.
* @param string $sessionIndex The SessionIndex of the user.
* @param int $expire Unix timestamp when this session expires.
* @param string $sessionId
*/
private static function addSessionSQL(
Store\SQLStore $store,
string $authId,
string $nameId,
string $sessionIndex,
int $expire,
string $sessionId,
): void {
self::createLogoutTable($store);
if (rand(0, 1000) < 10) {
self::cleanLogoutStore($store);
}
$data = [
'_authSource' => $authId,
'_nameId' => $nameId,
'_sessionIndex' => $sessionIndex,
'_expire' => gmdate('Y-m-d H:i:s', $expire),
'_sessionId' => $sessionId,
];
$store->insertOrUpdate(
$store->prefix . '_saml_LogoutStore',
['_authSource', '_nameId', '_sessionIndex'],
$data,
);
}
/**
* Retrieve sessions from the SQL datastore.
*
* @param \SimpleSAML\Store\SQLStore $store The datastore.
* @param string $authId The authsource ID.
* @param string $nameId The hash of the users NameID.
* @return array Associative array of SessionIndex => SessionId.
*/
private static function getSessionsSQL(Store\SQLStore $store, string $authId, string $nameId): array
{
self::createLogoutTable($store);
$params = [
'_authSource' => $authId,
'_nameId' => $nameId,
'now' => gmdate('Y-m-d H:i:s'),
];
// We request the columns in lowercase in order to be compatible with PostgreSQL
$query = 'SELECT _sessionIndex AS _sessionindex, _sessionId AS _sessionid FROM ' . $store->prefix;
$query .= '_saml_LogoutStore WHERE _authSource = :_authSource AND _nameId = :_nameId AND _expire >= :now';
$query = $store->pdo->prepare($query);
$query->execute($params);
$res = [];
while (($row = $query->fetch(PDO::FETCH_ASSOC)) !== false) {
$res[$row['_sessionindex']] = $row['_sessionid'];
}
/** @var array $res */
return $res;
}
/**
* Retrieve all session IDs from a key-value store.
*
* @param \SimpleSAML\Store\StoreInterface $store The datastore.
* @param string $nameId The hash of the users NameID.
* @param array $sessionIndexes The session indexes.
* @return array Associative array of SessionIndex => SessionId.
*/
private static function getSessionsStore(
StoreInterface $store,
string $nameId,
array $sessionIndexes,
): array {
$res = [];
foreach ($sessionIndexes as $sessionIndex) {
$sessionId = $store->get('saml.LogoutStore', $nameId . ':' . $sessionIndex);
if ($sessionId === null) {
continue;
}
Assert::string($sessionId);
$res[$sessionIndex] = $sessionId;
}
return $res;
}
/**
* Register a new session in the datastore.
*
* @param string $authId The authsource ID.
* @param \SAML2\XML\saml\NameID $nameId The NameID of the user.
* @param string|null $sessionIndex The SessionIndex of the user.
* @param int $expire Unix timestamp when this session expires.
*/
public static function addSession(string $authId, NameID $nameId, ?string $sessionIndex, int $expire): void
{
$session = Session::getSessionFromRequest();
if ($session->isTransient()) {
// transient sessions are useless for this purpose, nothing to do
return;
}
if ($sessionIndex === null) {
/* This IdP apparently did not include a SessionIndex, and thus probably does not
* support SLO. We still want to add the session to the data store just in case
* it supports SLO, but we don't want an LogoutRequest with a specific
* SessionIndex to match this session. We therefore generate our own session index.
*/
$randomUtils = new Utils\Random();
$sessionIndex = $randomUtils->generateID();
}
$config = Configuration::getInstance();
$storeType = $config->getOptionalString('store.type', 'phpsession');
$store = StoreFactory::getInstance($storeType);
if ($store === false) {
// We don't have a datastore.
return;
}
// serialize and anonymize the NameID
$strNameId = serialize($nameId);
$strNameId = sha1($strNameId);
// Normalize SessionIndex
if (strlen($sessionIndex) > 50) {
$sessionIndex = sha1($sessionIndex);
}
/** @var string $sessionId */
$sessionId = $session->getSessionId();
if ($store instanceof Store\SQLStore) {
self::addSessionSQL($store, $authId, $strNameId, $sessionIndex, $expire, $sessionId);
} else {
$store->set('saml.LogoutStore', $strNameId . ':' . $sessionIndex, $sessionId, $expire);
}
}
/**
* Log out of the given sessions.
*
* @param string $authId The authsource ID.
* @param \SAML2\XML\saml\NameID $nameId The NameID of the user.
* @param array $sessionIndexes The SessionIndexes we should log out of. Logs out of all if this is empty.
* @return int|false Number of sessions logged out, or FALSE if not supported.
*/
public static function logoutSessions(string $authId, NameID $nameId, array $sessionIndexes)
{
$config = Configuration::getInstance();
$storeType = $config->getOptionalString('store.type', 'phpsession');
$store = StoreFactory::getInstance($storeType);
if ($store === false) {
// We don't have a datastore
return false;
}
// serialize and anonymize the NameID
$strNameId = serialize($nameId);
$strNameId = sha1($strNameId);
// Normalize SessionIndexes
foreach ($sessionIndexes as &$sessionIndex) {
Assert::string($sessionIndex);
if (strlen($sessionIndex) > 50) {
$sessionIndex = sha1($sessionIndex);
}
}
// Remove reference
unset($sessionIndex);
if ($store instanceof Store\SQLStore) {
$sessions = self::getSessionsSQL($store, $authId, $strNameId);
} else {
if (empty($sessionIndexes)) {
// We cannot fetch all sessions without a SQL store
return false;
}
$sessions = self::getSessionsStore($store, $strNameId, $sessionIndexes);
}
if (empty($sessionIndexes)) {
$sessionIndexes = array_keys($sessions);
}
$numLoggedOut = 0;
foreach ($sessionIndexes as $sessionIndex) {
if (!isset($sessions[$sessionIndex])) {
Logger::info('saml.LogoutStore: Logout requested for unknown SessionIndex.');
continue;
}
$sessionId = $sessions[$sessionIndex];
$session = Session::getSession($sessionId);
if ($session === null) {
Logger::info('saml.LogoutStore: Skipping logout of missing session.');
continue;
}
if (!$session->isValid($authId)) {
Logger::info(
'saml.LogoutStore: Skipping logout of session because it isn\'t authenticated.',
);
continue;
}
Logger::info(
'saml.LogoutStore: Logging out of session with trackId [' . $session->getTrackID() . '].',
);
$session->doLogout($authId);
$numLoggedOut += 1;
}
return $numLoggedOut;
}
}

View File

@@ -0,0 +1,12 @@
{% set pagetitle = 'SimpleSAMLphp'|trans %}
{% extends "base.twig" %}
{% block content %}
<h2>{{ 'Invalid Identity Provider'|trans }}</h2>
<p>{{ 'You already have a valid session with an identity provider (<em>%IDP%</em>) that is not accepted by <em>%SP%</em>. Would you like to log out from your existing session and log in again with another identity provider?'|trans({"%IDP%": entity_idp|entityDisplayName, "%SP%": entity_sp|entityDisplayName})|raw }}</p>
<form class="pure-form" method="post" action="?">
<input type="hidden" name="AuthState" value="{{ AuthState|escape('html') }}">
<input type="submit" class="pure-button pure-button-red" name="continue" value="{{ 'Yes, continue'|trans }}">
<input type="submit" class="pure-button" name="cancel" value="{{ 'No, cancel'|trans }}">
</form>
{% endblock %}

View File

@@ -0,0 +1,7 @@
{% set pagetitle = 'SimpleSAMLphp'|trans %}
{% extends "base.twig" %}
{% block content %}
<h2>{{ 'Wrong authentication context'|trans }}</h2>
<p>{{ 'Your authentication context is not accepted at this service. Probably too weak or not two-factor.'|trans }}</p>
{% endblock %}