Skip to content
This repository was archived by the owner on Dec 14, 2018. It is now read-only.

Commit 8290ea1

Browse files
committed
Merge pull request #126 from stormpath/feature/issue_116
Adds support for SAML in the PHP SDK! This fixes #116
2 parents 0866631 + f4e1e05 commit 8290ea1

25 files changed

+1468
-85
lines changed

.travis.yml

+11-11
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,26 @@
11
language: php
2-
32
php:
43
- 5.6
54
- 5.5
65
- 5.4
7-
86
sudo: false
9-
107
services:
11-
- redis-server
12-
- memcached
13-
8+
- redis-server
9+
- memcached
1410
before_script:
1511
- composer self-update
1612
- travis_retry composer install --prefer-dist --no-interaction
1713
- mkdir -p ~/.phpenv/versions/$(phpenv version-name)/etc
1814
- echo "extension = memcached.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
1915
- echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini
20-
2116
script:
22-
- travis_retry vendor/bin/phpunit --coverage-clover build/logs/clover.xml
23-
17+
- travis_retry vendor/bin/phpunit --coverage-clover build/logs/clover.xml
2418
after_success:
25-
- bash <(curl -s https://codecov.io/bash)
26-
19+
- bash <(curl -s https://codecov.io/bash)
20+
notifications:
21+
hipchat:
22+
rooms:
23+
secure: DN61iUJL9kBtBfPqdHZk67IvadLdSR8+X2dV79qxx/OAFjhu+rW/K0PQp2VEgpUwHwROJxPhxiwXO8OLaA8v7rD7yp9diYwG8gjrmbOcLOphPhMuRlxSfDddWS7Eo7C177KfB2/WEbFhXhav4rDeso8xRUvGJwxqe1TYLQ7cwZo=
24+
template:
25+
- '%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message} (<a href="%{build_url}">Details</a>/<a href="%{compare_url}">Change view</a>)'
26+
format: html

README.md

+161
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,167 @@ $authenticationRequest = new UsernamePasswordRequest('usernameOrEmail', 'passwor
933933
$result = $application->authenticateAccount($authenticationRequest);
934934
```
935935

936+
937+
### Authenticating Against a SAML Directory
938+
939+
940+
SAML is an XML-based standard for exchanging authentication and authorization data between security domains. Stormpath enables you to allow customers to log-in by authenticating with an external SAML Identity Provider.
941+
942+
#### Stormpath as a Service Provider
943+
944+
The specific use case that Stormpath supports is user-initiated single sign-on. In this scenario, a user requests a protected resource (e.g. your application). Your application, with the help of Stormpath, then confirms the users identity in order to determine whether they are able to access the resource. In SAML terminology, the user is the **User Agent**, your application (along with Stormpath) is the **Service Provider**, and the third-party SAML authentication site is the **Identity Provider** or **IdP**.
945+
946+
The broad strokes of the process are as follows:
947+
948+
- User Agent requests access from Service Provider
949+
- Service Provider responds with redirect to Identity Provider
950+
- Identity Provider authenticates the user
951+
- Identity provider redirects user back to Service Provider along with SAML assertions.
952+
- Service Provider receives SAML assertions and either creates or retrieves Account information
953+
954+
Just like with Mirror and Social Directories, the user information that is returned from the IdP is used by Stormpath to either identify an existing Account resource, or create a new one. In the case of new Account creation, Stormpath will map the information in the response onto its own resources. In this section we will walk you through the process of configuring your SAML Directory, as well as giving you an overview of how the SAML Authentication process works.
955+
956+
957+
#### Configuring Stormpath as a Service Provider
958+
959+
Configuration is stored in the Directory's `Provider resource `. Both of these resources must also be linked with an `AccountStoreMapping`. Here we will explain to you the steps that are required to configure Stormpath as a SAML Service Provider.
960+
961+
##### Step 1: Gather IDP Data
962+
963+
You will need the following information from your IdP:
964+
965+
- **SSO Login URL** - The URL at the IdP to which SAML authentication requests should be sent. This is often called an "SSO URL", "Login URL" or "Sign-in URL".
966+
- **SSO Logout URL** - The URL at the IdP to which SAML logout requests should be sent. This is often called a "Logout URL", "Global Logout URL" or "Single Logout URL".
967+
- **Signing Cert** - The IdP will digitally sign auth assertions and Stormpath will need to validate the signature. This will usually be in .pem or .crt format, but Stormpath requires the text value.
968+
- **Signing Algorithm** - You will need the name of the signing algorithm that your IdP uses. It will be either "RSA-SHA256" or "RSA-SHA1".
969+
970+
##### Step 2: Configure Your SAML Directory
971+
972+
Input the data you gathered in Step 1 above into your Directory's Provider resource, and then pass that along as part of the Directory creation HTTP POST:
973+
974+
```
975+
$samlProvider = \Stormpath\Resource\SamlProvider::instantiate([
976+
'ssoLoginUrl' => 'http://google.com/login',
977+
'ssoLogoutUrl' => 'http://google.com/logout',
978+
'encodedX509SigningCert' => $this->getDummyCertForSaml(),
979+
'requestSignatureAlgorithm' => 'RSA-SHA1'
980+
]);
981+
982+
$directory = \Stormpath\Resource\Directory::create([
983+
'name' => makeUniqueName('DirectoryTest samlProvider'),
984+
'provider' => $samlProvider
985+
]);
986+
```
987+
988+
989+
990+
> Notice that new lines in the certificate are separated with a ``\n`` character.
991+
992+
993+
##### Retrieve Your Service Provider Metadata
994+
995+
Next you will have to configure your Stormpath-powered application as a Service Provider in your Identity Provider. This means that you will need to retrieve the correct metadata from Stormpath.
996+
997+
In order to retrieve the required values, start by sending a GET to the Directory's Provider:
998+
999+
```
1000+
$provider = Stormpath\Resource\SamlProvider::get(self::$directory->provider->href);
1001+
$providerMetaData = $provider->serviceProviderMetadata
1002+
```
1003+
1004+
From this metadata, you will need two values:
1005+
1006+
- **Assertion Consumer Service URL**: This is the location the IdP will send its response to.
1007+
- **X509 Signing Certificate**: The certificate that is used to sign the requests sent to the IdP. If you retrieve XML, the certificate will be embedded. If you retrieve JSON, you'll have to follow a further ``/x509certificates`` link to retrieve it.
1008+
1009+
You will also need two other values, which will always be the same:
1010+
1011+
- **SAML Request Binding:** Set to ``HTTP-Redirect``.
1012+
- **SAML Response Binding:** Set to ``HTTP-Post``.
1013+
1014+
##### Step 4: Configure Your Service Provider in Your Identity Provider
1015+
1016+
Log-in to your Identity Provider (Salesforce, OneLogin, etc) and enter the information you retrieved in the previous step into the relevant application configuration fields. The specific steps to follow here will depend entirely on what Identity Provider you use, and for more information you should consult your Identity Provider's SAML documentation.
1017+
1018+
##### Step 5: Configure Your Application
1019+
1020+
The Stormpath `Application` Resource has two parts that are relevant to SAML:
1021+
1022+
- an ``authorizedCallbackUri`` Array that defines the authorized URIs that the IdP can return your user to. These should be URIs that you host yourself.
1023+
- an embedded ``samlPolicy`` object that contains information about the SAML flow configuration and endpoints.
1024+
1025+
```
1026+
$application->setAuthorizedCallbackUris([
1027+
'http://myapplication.com/whatever/callback',
1028+
'http://myapplication.com/whatever/callback2'
1029+
]);
1030+
1031+
$application->save();
1032+
1033+
```
1034+
1035+
##### Step 6: Add the SAML Directory as an Account Store
1036+
1037+
Now you last thing you have to do is map the new Directory to your Application with an Account Store Mapping.
1038+
1039+
1040+
##### Step 7: Configure SAML Assertion Mapping
1041+
1042+
The Identity Provider's SAML response contains assertions about the user's identity, which Stormpath can use to create and populate a new Account resource.
1043+
1044+
``` xml
1045+
1046+
<saml:AttributeStatement>
1047+
<saml:Attribute Name="uid" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
1048+
<saml:AttributeValue xsi:type="xs:string">test</saml:AttributeValue>
1049+
</saml:Attribute>
1050+
<saml:Attribute Name="mail" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
1051+
<saml:AttributeValue xsi:type="xs:string">[email protected]</saml:AttributeValue>
1052+
</saml:Attribute>
1053+
<saml:Attribute Name="location" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
1054+
<saml:AttributeValue xsi:type="xs:string">Tampa, FL</saml:AttributeValue>
1055+
</saml:Attribute>
1056+
</saml:AttributeStatement>
1057+
1058+
The Attribute Assertions (`<saml:AttributeStatement>`) are brought into Stormpath and become Account and customData attributes.
1059+
1060+
SAML Assertion mapping is defined in an **attributeStatementMappingRules** object found inside the Directory's Provider object, or directly: `/v1/attributeStatementMappingRules/$RULES_ID`.
1061+
1062+
##### Mapping Rules
1063+
1064+
The rules have three different components:
1065+
1066+
- **name**: The SAML Attribute name
1067+
- **nameFormat**: The name format for this SAML Attribute, expressed as a Uniform Resource Name (URN).
1068+
- **accountAttributes**: This is an array of Stormpath Account or customData (`customData.$KEY_NAME`) attributes that will map to this SAML Attribute.
1069+
1070+
1071+
1072+
In order to create the mapping rules, we simply send the following:
1073+
1074+
```
1075+
$provider = \Stormpath\Resource\SamlProvider::get($directory->provider->href);
1076+
1077+
$ruleBuilder = new \Stormpath\Saml\AttributeStatementMappingRuleBuilder();
1078+
$rule = $ruleBuilder->setName('test1')
1079+
->setAccountAttributes(['customData.test1'])
1080+
->build();
1081+
1082+
$rule2 = $ruleBuilder->setName('test2')
1083+
->setAccountAttributes(['customData.test2'])
1084+
->build();
1085+
1086+
1087+
$rulesBuilder = new \Stormpath\Saml\AttributeStatementMappingRulesBuilder();
1088+
$rulesBuilder->setAttributeStatementMappingRules([$rule, $rule2]);
1089+
$rules = $rulesBuilder->build();
1090+
1091+
$provider->setAttributeStatementMappingRules($rules);
1092+
1093+
$provider->save();
1094+
```
1095+
1096+
9361097
### Verify an Account's email address
9371098
9381099
This workflow allows you to send a welcome email to a newly registered account and optionally verify that they own the

src/DataStore/DefaultDataStore.php

+20-3
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ public function save(Resource $resource, $returnType = null)
151151

152152
if (!strlen($href))
153153
{
154-
throw new InvalidArgumentException('save may only be called on objects that have already been persisted (i.e. they have an existing href).');
154+
throw new \InvalidArgumentException('save may only be called on objects that have already been persisted (i.e. they have an existing href).');
155155
}
156156

157157
if ($this->needsToBeFullyQualified($href))
@@ -298,7 +298,6 @@ private function applyDefaultRequestHeaders(Request $request)
298298
->setPhpVersion(phpversion())
299299
->build();
300300

301-
302301
if ($body = $request->getBody())
303302
{
304303
$headers['Content-Type'] = 'application/json';
@@ -351,12 +350,30 @@ private function toStdClass(Resource $resource, $customData = false)
351350
$property = $this->toStdClass($property);
352351
}
353352

353+
$properties->$name = $property;
354+
}
354355

356+
return $properties;
357+
}
355358

356-
$properties->$name = $property;
359+
private function objectArrayToStdClass($property)
360+
{
361+
$properties = new \stdClass();
362+
363+
$class = new \ReflectionClass($property);
364+
365+
$classProperties = $class->getProperties();
366+
367+
foreach($classProperties as $prop) {
368+
$method = 'get'.ucfirst($prop->name);
369+
if(method_exists($property, $method)) {
370+
371+
$properties->{$prop->name} = $property->$method();
372+
}
357373
}
358374

359375
return $properties;
376+
360377
}
361378

362379
private function toSimpleReference($propertyName, \stdClass $properties)

src/DataStore/DefaultResourceFactory.php

+33
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
* See the License for the specific language governing permissions and
1818
* limitations under the License.
1919
*/
20+
use Stormpath\Saml\AttributeStatementMappingRuleBuilder;
21+
use Stormpath\Saml\AttributeStatementMappingRulesBuilder;
22+
2023
class DefaultResourceFactory implements ResourceFactory
2124
{
2225

@@ -43,11 +46,19 @@ public function instantiate($className, array $constructorArgs)
4346
$newClass->customData;
4447
}
4548

49+
if($newClass instanceof \Stormpath\Resource\SamlProvider) {
50+
$newClass = $this->convertSamlAttributeStatementMappingRules($newClass);
51+
}
52+
4653
return $newClass;
4754
}
4855

4956
private function qualifyClassName($className)
5057
{
58+
if (class_exists($className) && strstr($className, 'Stormpath')) {
59+
return $className;
60+
}
61+
5162
if (strpos($className, self::RESOURCE_PATH) === false)
5263
{
5364
return self::RESOURCE_PATH .$className;
@@ -57,4 +68,26 @@ private function qualifyClassName($className)
5768

5869
}
5970

71+
private function convertSamlAttributeStatementMappingRules($newClass)
72+
{
73+
$mappingRules = $newClass->getAttributeStatementMappingRules();
74+
if(null === $mappingRules) {
75+
return $newClass;
76+
}
77+
78+
$items = $mappingRules->getItems();
79+
$newItems = [];
80+
81+
$itemBuilder = new AttributeStatementMappingRuleBuilder();
82+
foreach($items as $item) {
83+
$newItems[] = $itemBuilder->setName($item->name)
84+
->setNameFormat($item->nameFormat)
85+
->setAccountAttributes($item->accountAttributes)
86+
->build();
87+
}
88+
89+
$newClass->getAttributeStatementMappingRules()->items = $newItems;
90+
return $newClass;
91+
}
92+
6093
}

src/Http/HttpClientRequestExecutor.php

+1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public function __construct(RequestSigner $signer = null)
4242
public function executeRequest(Request $request, $redirectsLimit = 10)
4343
{
4444
$requestHeaders = $request->getHeaders();
45+
4546
$apiKey = $request->getApiKey();
4647

4748
if ($apiKey)

src/Resource/Application.php

+27
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ class Application extends InstanceResource implements Deletable
4646
const LOGIN_ATTEMPTS = "loginAttempts";
4747
const OAUTH_POLICY = "oAuthPolicy";
4848
const CUSTOM_DATA = "customData";
49+
const AUTHORIZED_CALLBACK_URIS = "authorizedCallbackUris";
4950

5051
const PATH = "applications";
5152

@@ -112,6 +113,32 @@ public function setStatus($status)
112113
}
113114
}
114115

116+
/**
117+
* Array that defines the authorized URIs that the IdP can return your
118+
* user to. These should be URIs that you host yourself.
119+
*
120+
* @since 1.13.0
121+
* @param array $uris
122+
* @return self
123+
*/
124+
public function setAuthorizedCallbackUris(array $uris = [])
125+
{
126+
$this->setProperty(self::AUTHORIZED_CALLBACK_URIS, $uris);
127+
return $this;
128+
}
129+
130+
/**
131+
* Returns Array that defines the authorized URIs that the IdP can return
132+
* your user to. These should be URIs that you host yourself.
133+
*
134+
* @since 1.13.0
135+
* @return array
136+
*/
137+
public function getAuthorizedCallbackUris()
138+
{
139+
return (array) $this->getProperty(self::AUTHORIZED_CALLBACK_URIS);
140+
}
141+
115142
public function getTenant(array $options = array())
116143
{
117144
return $this->getResourceProperty(self::TENANT, Stormpath::TENANT, $options);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?php
2+
3+
namespace Stormpath\Resource;
4+
5+
class AssertionConsumerServicePostEndpoint extends Resource {}

0 commit comments

Comments
 (0)