Skip to content

Commit 2c52c4a

Browse files
committed
Add example code by @jorissteyn (closes: #93)
1 parent f6e59d1 commit 2c52c4a

2 files changed

Lines changed: 202 additions & 0 deletions

File tree

EXAMPLES.md

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
Here are two example controllers here in the hope it might still be useful for the reader.
2+
3+
I'm assuming your application is the service provider, and you want to
4+
authenticate users with a remote identity provider. This means you need two
5+
endpoints:
6+
7+
- an endpoint to start authentication, I call this the acs init endpoint
8+
- an endpoint to process a SAML response, I call this the acs respond endpoint
9+
10+
Your init endpoint might look something like this:
11+
12+
```php
13+
<?php
14+
15+
namespace YourApp\Controller\Saml;
16+
17+
use YourApp\Saml\AcsContextInterface;
18+
use Psr\Log\LoggerInterface;
19+
use Surfnet\SamlBundle\Entity\HostedEntities;
20+
use Surfnet\SamlBundle\Entity\IdentityProvider;
21+
use Surfnet\SamlBundle\SAML2\AuthnRequestFactory;
22+
use Symfony\Component\HttpFoundation\RedirectResponse;
23+
use Symfony\Component\HttpFoundation\Request;
24+
use Symfony\Component\HttpFoundation\Response;
25+
use Symfony\Component\Routing\Annotation\Route;
26+
27+
class AcsInitController
28+
{
29+
/**
30+
* @Route(
31+
* "/saml/acs/init",
32+
* name="saml_acs_init",
33+
* methods={"GET"},
34+
* requirements={
35+
* "_format": "xml",
36+
* },
37+
* )
38+
*/
39+
public function __invoke(
40+
Request $httpRequest,
41+
HostedEntities $hostedEntities,
42+
IdentityProvider $idp,
43+
AcsContextInterface $context,
44+
LoggerInterface $logger
45+
): Response {
46+
$request = AuthnRequestFactory::createNewRequest(
47+
$hostedEntities->getServiceProvider(),
48+
$idp
49+
);
50+
51+
$logger->info(
52+
sprintf(
53+
'Starting SSO request with ID %s to IDP %s',
54+
$request->getRequestId(),
55+
$idp->getEntityId()
56+
),
57+
['request' => $request->getUnsignedXML()]
58+
);
59+
60+
// Store the request so we can validate the response on acs respond.
61+
$context->setAuthnRequest($request);
62+
63+
// That's it, we're good to go!
64+
return new RedirectResponse(
65+
sprintf(
66+
'%s?%s',
67+
$idp->getSsoUrl(),
68+
$request->buildRequestQuery()
69+
)
70+
);
71+
}
72+
}
73+
```
74+
75+
The `$idp` argument should be wired to the
76+
`surfnet_saml.hosted.identity_provider` service. The `$context` argument is an
77+
object from your own application where you store some state in the session,
78+
like the request or request ID that was sent.
79+
80+
The second endpoint is the actual ACS endpoint that receives and validates the
81+
SAML response and redirects to your application:
82+
83+
```php
84+
namespace YourApp\Controller\Saml;
85+
86+
use Exception;
87+
use SAML2\Assertion;
88+
use Surfnet\SamlBundle\Entity\IdentityProvider;
89+
use YourApp\Saml\AcsContextInterface;
90+
use Psr\Log\LoggerInterface;
91+
use Surfnet\SamlBundle\Entity\HostedEntities;
92+
use Surfnet\SamlBundle\Http\PostBinding;
93+
use Symfony\Component\HttpFoundation\RedirectResponse;
94+
use Symfony\Component\HttpFoundation\Request;
95+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
96+
use Symfony\Component\Routing\Annotation\Route;
97+
98+
class AcsRespondController
99+
{
100+
/**
101+
* @Route(
102+
* "/saml/acs",
103+
* name="saml_acs_respond",
104+
* methods={"POST"},
105+
* requirements={
106+
* "_format": "xml",
107+
* },
108+
* )
109+
*/
110+
public function __invoke(
111+
HostedEntities $hostedEntities,
112+
AcsContextInterface $context,
113+
IdentityProvider $idp,
114+
PostBinding $binding,
115+
Request $httpRequest,
116+
LoggerInterface $logger
117+
): RedirectResponse {
118+
$response = $httpRequest->request->get('SAMLResponse');
119+
120+
if (!$response) {
121+
throw new BadRequestHttpException(
122+
'No SAMLResponse parameter found in request to ACS respond endpoint'
123+
);
124+
}
125+
126+
$logger->info(
127+
'Received HTTP request on ACS endpoint',
128+
[
129+
'SAMLResponse' => base64_decode($response),
130+
]
131+
);
132+
133+
if (!$context->hasAuthnRequest()) {
134+
$logger->error('Received assertion but no authn request found in context: session lost?');
135+
136+
throw new BadRequestHttpException('Received an assertion but SSO was not initiated here');
137+
}
138+
139+
try {
140+
$assertion = $binding->processResponse(
141+
$httpRequest,
142+
$idp,
143+
$hostedEntities->getServiceProvider()
144+
);
145+
} catch (Exception $e) {
146+
$logger->error(
147+
'Error processing ACS request: ' . $e->getMessage(),
148+
[
149+
'exception' => $e,
150+
]
151+
);
152+
153+
throw new BadRequestHttpException('Error processing ACS request');
154+
}
155+
156+
$logger->info(
157+
'Processed ACS authn request',
158+
[
159+
'attributes' => $assertion->getAttributes(),
160+
]
161+
);
162+
163+
$logger->debug(
164+
'Full assertion in received authn response',
165+
[
166+
'assertion' => $assertion->toXML()->ownerDocument->saveXML(),
167+
]
168+
);
169+
170+
$inResponseTo = $this->getInResponseTo($assertion);
171+
$requestId = $context->getAuthnRequest()->getRequestId();
172+
if ($inResponseTo !== $requestId) {
173+
throw new BadRequestException(
174+
"InResponseTo of asssertion {$inResponseTo} does not match request ID {$requestId}"
175+
);
176+
}
177+
178+
// You should clear the authn request from your session state, and set the user as logged
179+
// in based on the attributes found
180+
$context->clearAuthnRequest();
181+
182+
return new RedirectResponse(
183+
'/redirect-to-somewhere'
184+
);
185+
}
186+
187+
private function getInResponseTo(Assertion $assertion): ?string
188+
{
189+
/** @var \SAML2\XML\saml\SubjectConfirmation $subjectConfirmation */
190+
$subjectConfirmation = $assertion->getSubjectConfirmation()[0];
191+
192+
return $subjectConfirmation->SubjectConfirmationData->InResponseTo;
193+
}
194+
}
195+
```
196+
197+
That is more than a few lines of code, but most of it is just logging. The SAML
198+
bundle does not check the InResponseTo of the assertion, so that too is included
199+
in this example.

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ class MetadataController extends Controller
123123
}
124124
```
125125

126+
See more examples in [EXAMPLES.md](EXAMPLES.md).
127+
128+
126129
## Release strategy
127130

128131
### CHANGELOG.md

0 commit comments

Comments
 (0)