Skip to content

Commit 1225ea1

Browse files
authored
Add support for Metadata Deployment Profile for errorURL (#1841)
Add support for Metadata Deployment Profile for errorURL
1 parent f4636c6 commit 1225ea1

6 files changed

Lines changed: 279 additions & 1 deletion

File tree

docs/simplesamlphp-reference-idp-hosted.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ entry matches.
8888
],
8989
```
9090

91+
`errorURL`
92+
: Overrides the errorURL in the IDP's published metadata.
93+
9194
`host`
9295
: The hostname for this IdP. One IdP can also have the `host`-option
9396
set to `__DEFAULT__`, and that IdP will be used when no other

modules/core/routing/routes/routes.yml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ core-loginuserpassorg:
3636
}
3737
methods: [GET, POST]
3838

39+
core-error:
40+
path: /error/{code}
41+
defaults: {
42+
_controller: 'SimpleSAML\Module\core\Controller\Exception::error'
43+
}
44+
methods: [GET]
45+
3946
core-error-nocookie:
4047
path: /error/nocookie
4148
defaults: {

modules/core/src/Controller/Exception.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44

55
namespace SimpleSAML\Module\core\Controller;
66

7+
use DATE_W3C;
8+
use SimpleSAML\Assert\Assert;
79
use SimpleSAML\{Auth, Configuration, Error, Logger, Module, Session, Utils};
810
use SimpleSAML\XHTML\Template;
911
use Symfony\Component\HttpFoundation\{RedirectResponse, Request, Response};
1012

1113
use function array_keys;
14+
use function date;
1215
use function implode;
16+
use function intval;
17+
use function strval;
1318
use function urlencode;
1419

1520
/**
@@ -21,6 +26,9 @@
2126
*/
2227
class Exception
2328
{
29+
public const CODES = ['IDENTIFICATION_FAILURE', 'AUTHENTICATION_FAILURE', 'AUTHORIZATION_FAILURE', 'OTHER_ERROR'];
30+
31+
2432
/**
2533
* Controller constructor.
2634
*
@@ -38,6 +46,65 @@ public function __construct(
3846
}
3947

4048

49+
/**
50+
* Show Service Provider error.
51+
*
52+
* @param Request $request The request that lead to this login operation.
53+
* @param string $code The error code
54+
* @return \SimpleSAML\XHTML\Template An HTML template
55+
*/
56+
public function error(Request $request, string $code): Response
57+
{
58+
if ($code !== 'ERRORURL_CODE') {
59+
Assert::oneOf($code, self::CODES);
60+
}
61+
62+
$ts = $request->query->get('ts');
63+
$rp = $request->query->get('rp');
64+
$tid = $request->query->get('tid');
65+
$ctx = $request->query->get('ctx');
66+
67+
if ($ts !== 'ERRORURL_TS') {
68+
Assert::integerish($ts);
69+
$ts = date(DATE_W3C, intval($ts));
70+
} else {
71+
$ts = null;
72+
}
73+
74+
if ($rp === 'ERRORURL_RP') {
75+
$rp = null;
76+
}
77+
78+
if ($tid === 'ERRORURL_TID') {
79+
$tid = null;
80+
}
81+
82+
if ($ctx === 'ERRORURL_CTX') {
83+
$ctx = null;
84+
}
85+
86+
Logger::notice(sprintf(
87+
"A Service Provider reported the following error during authentication: "
88+
. "Code: %s; Timestamp: %s; Relying party: %s; Transaction ID: %s; Context: [%s]",
89+
$code,
90+
$ts ?? 'null',
91+
$rp ?? 'null',
92+
$tid ?? 'null',
93+
$ctx,
94+
));
95+
96+
$t = new Template($this->config, 'core:error.twig');
97+
98+
$t->data['code'] = $code;
99+
$t->data['ts'] = $ts;
100+
$t->data['rp'] = $rp;
101+
$t->data['tid'] = $tid;
102+
$t->data['ctx'] = $ctx;
103+
104+
return $t;
105+
}
106+
107+
41108
/**
42109
* Show cardinality error.
43110
*

modules/core/templates/error.twig

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{% set pagetitle = 'An error has occurred'|trans %}
2+
{% extends "base.twig" %}
3+
4+
{% block content %}
5+
<h2>{{ pagetitle }}</h2>
6+
7+
{% if code is same as ('IDENTIFICATION_FAILURE') %}
8+
<p>{{ 'The identification procesd has failed.' |trans }}</p>
9+
{% elseif code is same as ('AUTHENTICATION_FAILURE') %}
10+
<p>{{ 'The authentication procesd has failed.' |trans }}</p>
11+
{% elseif code is same as ('AUTHORIZATION_FAILURE') %}
12+
<p>{{ 'The authorization procesd has failed.' |trans }}</p>
13+
{% else %}
14+
<p>{{ 'There was an issue while signing you in.' |trans }}</p>
15+
{% endif %}
16+
17+
{% if ts is defined or rp is defined or tid is defined or ctx is defined %}
18+
<table id="table_with_attributes" class="attributes pure-table pure-table-striped pure-table-attributes" summary="attribute overview">
19+
{% if ts is defined %}
20+
<tr><td>Timestamp</td><td>{{ ts }}</td></tr>
21+
{% endif %}
22+
{% if rp is defined %}
23+
<tr><td>Service Provider</td><td>{{ rp }}</td></tr>
24+
{% endif %}
25+
{% if tid is defined %}
26+
<tr><td>Reference code</td><td>{{ tid }}</td></tr>
27+
{% endif %}
28+
{% if ctx is defined %}
29+
<tr><td>Context</td><td>{{ ctx }}</td></tr>
30+
{% endif %}
31+
</table>
32+
{% endif %}
33+
{% endblock %}

src/SimpleSAML/Metadata/SAMLBuilder.php

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace SimpleSAML\Metadata;
66

77
use DOMElement;
8-
use SimpleSAML\{Configuration, Logger, Utils};
8+
use SimpleSAML\{Configuration, Module, Logger, Utils};
99
use SimpleSAML\Assert\{Assert, AssertionFailedException};
1010
use SimpleSAML\Module\adfs\SAML2\XML\fed\SecurityTokenServiceType;
1111
use SimpleSAML\SAML2\Constants as C;
@@ -476,6 +476,14 @@ public function addMetadataIdP20(array $metadata): void
476476
$e->setWantAuthnRequestsSigned($metadata->getBoolean('redirect.sign'));
477477
}
478478

479+
if ($metadata->hasValue('errorURL')) {
480+
$e->setErrorURL($metadata->getString('errorURL'));
481+
} else {
482+
$e->setErrorURL(Module::getModuleURL(
483+
'core/error/ERRORURL_CODE?ts=ERRORURL_TS&rp=ERRORURL_RP&tid=ERRORURL_TID&ctx=ERRORURL_CTX',
484+
));
485+
}
486+
479487
$this->addExtensions($metadata, $e);
480488

481489
$this->addCertificate($e, $metadata);
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\Test\Module\core\Controller;
6+
7+
use DATE_W3C;
8+
use PHPUnit\Framework\TestCase;
9+
use SimpleSAML\Assert\AssertionFailedException;
10+
use SimpleSAML\{Configuration, Logger, Session};
11+
use SimpleSAML\Module\core\Controller;
12+
use SimpleSAML\TestUtils\ArrayLogger;
13+
use SimpleSAML\XHTML\Template;
14+
use Symfony\Component\HttpFoundation\Request;
15+
16+
use function date;
17+
use function sprintf;
18+
use function time;
19+
20+
/**
21+
* Set of tests for the controllers in the "core" module.
22+
*
23+
* @covers \SimpleSAML\Module\core\Controller\Exception
24+
* @package SimpleSAML\Test
25+
*/
26+
class ExceptionTest extends TestCase
27+
{
28+
/** @var \SimpleSAML\Configuration */
29+
protected Configuration $config;
30+
31+
/** @var \SimpleSAML\Session */
32+
protected Session $session;
33+
34+
35+
/**
36+
* Set up for each test.
37+
*/
38+
protected function setUp(): void
39+
{
40+
parent::setUp();
41+
42+
$this->config = Configuration::loadFromArray(
43+
[
44+
'logging.handler' => ArrayLogger::class,
45+
'errorreporting' => false,
46+
'module.enable' => ['core' => true],
47+
],
48+
'[ARRAY]',
49+
'simplesaml'
50+
);
51+
52+
Configuration::setPreLoadedConfig($this->config, 'config.php');
53+
54+
$this->session = Session::getSessionFromRequest();
55+
}
56+
57+
58+
/**
59+
* Clean up after each test
60+
*/
61+
protected function tearDown(): void
62+
{
63+
Logger::clearCapturedLog();
64+
}
65+
66+
67+
/**
68+
* @dataProvider codeProvider
69+
* @param string $code
70+
* Test that we are presented with an 'error was reported' page
71+
*/
72+
public function testErrorURL(string $code, string $ts, string $rp, string $tid, string $ctx): void
73+
{
74+
$request = Request::create(
75+
sprintf('/error/%s?ts=%s&rp=%s&tid=%s&ctx=%s', $code, $ts, $rp, $tid, $ctx),
76+
'GET',
77+
);
78+
79+
$c = new Controller\Exception($this->config, $this->session);
80+
81+
Logger::setCaptureLog();
82+
$response = $c->error($request, $code);
83+
Logger::setCaptureLog(false);
84+
85+
$log = Logger::getCapturedLog();
86+
self::assertCount(5, $log);
87+
88+
self::assertStringContainsString(
89+
"A Service Provider reported the following error during authentication: "
90+
. sprintf(
91+
"Code: %s; Timestamp: %s; Relying party: %s; Transaction ID: %s; Context: [%s]",
92+
$code,
93+
($ts === 'ERRORURL_TS') ? 'null' : date(DATE_W3C, intval($ts)),
94+
($rp === 'ERRORURL_RP') ? 'null' : $rp,
95+
($tid === 'ERRORURL_TID') ? 'null' : $tid,
96+
($ctx === 'ERRORURL_CTX') ? '' : urldecode($ctx),
97+
),
98+
$log[0],
99+
);
100+
101+
$this->assertInstanceOf(Template::class, $response);
102+
$this->assertTrue($response->isSuccessful());
103+
$this->assertEquals('core:error.twig', $response->getTemplateName());
104+
}
105+
106+
107+
/**
108+
*/
109+
public static function codeProvider(): array
110+
{
111+
$codes = [
112+
'ERRORURL_CODE',
113+
'IDENTIFICATION_FAILURE',
114+
'AUTHENTICATION_FAILURE',
115+
'AUTHORIZATION_FAILURE',
116+
'OTHER_ERROR',
117+
];
118+
119+
$tss = ['ERRORURL_TS', strval(time())];
120+
$rps = ['ERRORURL_RP', 'phpunit'];
121+
$tids = ['ERRORURL_TID', '123456'];
122+
$ctxs = ['ERRORURL_CTX', urlencode("phpunit did it's job")];
123+
124+
$matrix = [];
125+
foreach ($codes as $code) {
126+
foreach ($tss as $ts) {
127+
foreach ($rps as $rp) {
128+
foreach ($tids as $tid) {
129+
foreach ($ctxs as $ctx) {
130+
$matrix[] = [$code, $ts, $rp, $tid, $ctx];
131+
}
132+
}
133+
}
134+
}
135+
}
136+
137+
return $matrix;
138+
}
139+
140+
141+
/**
142+
* Test that an exception was thrown when an invalid error code was used.
143+
*/
144+
public function testErrorURLInvalidCode(): void
145+
{
146+
$request = Request::create(
147+
'/error/doesNotExist',
148+
);
149+
150+
$c = new Controller\Exception($this->config, $this->session);
151+
152+
$this->expectException(AssertionFailedException::class);
153+
$this->expectExceptionMessage(
154+
'Expected one of: "IDENTIFICATION_FAILURE", "AUTHENTICATION_FAILURE",'
155+
. ' "AUTHORIZATION_FAILURE", "OTHER_ERROR". Got: "doesNotExist"'
156+
);
157+
158+
$c->error($request, 'doesNotExist');
159+
}
160+
}

0 commit comments

Comments
 (0)