Skip to content
This repository was archived by the owner on Sep 19, 2022. It is now read-only.

Commit fd3dd41

Browse files
author
Dominik František Bučík
authored
Merge pull request #226 from BaranekD/privacyIdea_template
feat: Custom privacyIDEA login template
2 parents 5520751 + 15359e0 commit fd3dd41

File tree

3 files changed

+388
-0
lines changed

3 files changed

+388
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"alternate_login_options": {
3+
"en": "Alternate login options",
4+
"cs": "Další možnosti přihlášení"
5+
},
6+
"introduction_notice": {
7+
"en": "The service you are authenticating to requires MFA (Multi Factor Authentication). To complete the authentication, select one from the options below.",
8+
"cs": "Služba, ke které chcete přistoupit, vyžaduje vícefaktorovou autentizaci. Pro dokončení přihlášení zvolte jednu z možností níže."
9+
},
10+
"webauthn": {
11+
"en": "WebAuthn",
12+
"cs": "WebAuthn"
13+
},
14+
"push": {
15+
"en": "Push",
16+
"cs": "Push"
17+
},
18+
"otp": {
19+
"en": "OTP",
20+
"cs": "OTP"
21+
},
22+
"u2f": {
23+
"en": "U2F",
24+
"cs": "U2F"
25+
}
26+
}
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
<?php declare(strict_types=1);
2+
3+
// Set default scenario if isn't set
4+
use SimpleSAML\Configuration;
5+
use SimpleSAML\Logger;
6+
use SimpleSAML\Module;
7+
8+
try {
9+
$config = Configuration::getConfig('module_perun.php')
10+
->getArray('privacyidea_dictionaries', [])
11+
;
12+
} catch (\Exception $ex) {
13+
Logger::error(
14+
"perun:loginform: missing or invalid 'module_perun.php[privacyidea]' configuration file. " .
15+
'Default configuration will be used.'
16+
);
17+
}
18+
19+
if (!empty($this->data['authProcFilterScenario'])) {
20+
if (empty($this->data['username'])) {
21+
$this->data['username'] = null;
22+
}
23+
} else {
24+
$this->data['authProcFilterScenario'] = 0;
25+
}
26+
27+
// Set the right text shown in otp/pass field(s)
28+
if (!empty($this->data['otpFieldHint'])) {
29+
$otpHint = $this->data['otpFieldHint'];
30+
} else {
31+
$otpHint = $this->t('{privacyidea:privacyidea:otp}');
32+
}
33+
if (!empty($this->data['passFieldHint'])) {
34+
$passHint = $this->data['passFieldHint'];
35+
} else {
36+
$passHint = $this->t('{privacyidea:privacyidea:password}');
37+
}
38+
39+
// Call u2f.js and u2f-api.js if u2f token is triggered
40+
/*$head = '';
41+
if ($this->data['u2fSignRequest']) {
42+
// Add javascript for U2F support before including the header.
43+
$head .= '<script type="text/javascript" src="' . htmlspecialchars(SimpleSAML_Module::getModuleUrl('privacyidea/js/u2f-api.js'), ENT_QUOTES) . '"></script>';
44+
}*/
45+
46+
$this->data['header'] = '';
47+
48+
// Prepare next settings
49+
if (!empty($this->data['username'])) {
50+
$this->data['autofocus'] = 'password';
51+
} else {
52+
$this->data['autofocus'] = 'username';
53+
}
54+
55+
$this->data['head'] = '<link rel="stylesheet" href="'
56+
. htmlspecialchars(Module::getModuleUrl('perun/res/css/privacyidea.css'), ENT_QUOTES)
57+
. '" media="screen" />' . PHP_EOL;
58+
59+
$this->includeAtTemplateBase('includes/header.php');
60+
?>
61+
<div>
62+
<?php echo getTranslation($config, 'introduction_notice', '', $this); ?>
63+
</div>
64+
65+
<div class="login">
66+
<div class="loginlogo"></div>
67+
68+
<?php
69+
if ($this->data['authProcFilterScenario']) {
70+
echo '<h3 class="text-center">' . htmlspecialchars(
71+
getTranslation($config, 'login_title_challenge', 'One Time Password', $this)
72+
) . '</h3>';
73+
} else {
74+
if ($this->data['step'] < 2) {
75+
echo '<h3>' . htmlspecialchars($this->t('{privacyidea:privacyidea:login_title}')) . '</h3>';
76+
}
77+
}
78+
?>
79+
80+
<form action="" method="POST" id="piLoginForm" name="piLoginForm" class="loginForm">
81+
<div class="form-panel first valid" id="gaia_firstform">
82+
<div class="slide-out ">
83+
<div class="input-wrapper focused">
84+
<div class="identifier-shown">
85+
<?php
86+
if ($this->data['forceUsername']) {
87+
if (!empty(htmlspecialchars($this->data['username'] ?? ''))) {
88+
?>
89+
<h3 class="text-center"><?php echo htmlspecialchars($this->data['username'] ?? ''); ?></h3>
90+
<?php
91+
} ?>
92+
<input type="hidden" id="username" name="username"
93+
value="<?php echo htmlspecialchars($this->data['username'] ?? '', ENT_QUOTES); ?>"/>
94+
<?php
95+
} else {
96+
?>
97+
<label for="username"></label>
98+
<input type="text" id="username" tabindex="1" name="username"
99+
value="<?php echo htmlspecialchars($this->data['username'], ENT_QUOTES); ?>"
100+
placeholder="<?php echo htmlspecialchars(getTranslation($config, 'username', 'Username', $this), ENT_QUOTES); ?>"
101+
/>
102+
<br>
103+
<?php
104+
}
105+
106+
// Remember username in authproc
107+
if (!$this->data['authProcFilterScenario']) {
108+
if ($this->data['rememberUsernameEnabled'] || $this->data['rememberMeEnabled']) {
109+
$rowspan = 1;
110+
} elseif (array_key_exists('organizations', $this->data)) {
111+
$rowspan = 3;
112+
} else {
113+
$rowspan = 2;
114+
}
115+
if ($this->data['rememberUsernameEnabled'] || $this->data['rememberMeEnabled']) {
116+
if ($this->data['rememberUsernameEnabled']) {
117+
echo str_repeat("\t", 4);
118+
echo '<input type="checkbox" id="rememberUsername" tabindex="4" name="rememberUsername"
119+
value="Yes" ';
120+
echo $this->data['rememberUsernameChecked'] ? 'checked="Yes" /> ' : '/> ';
121+
echo htmlspecialchars(
122+
getTranslation($config, 'remember_username', 'Remember username', $this)
123+
);
124+
}
125+
if ($this->data['rememberMeEnabled']) {
126+
echo str_repeat("\t", 4);
127+
echo '<input type="checkbox" id="rememberMe" tabindex="4" name="rememberMe" value="Yes" ';
128+
echo $this->data['rememberMeChecked'] ? 'checked="Yes" /> ' : '/> ';
129+
echo htmlspecialchars(
130+
getTranslation($config, 'remember_me', 'Remember me', $this)
131+
);
132+
}
133+
}
134+
} ?>
135+
136+
<!-- Pass and OTP fields -->
137+
<label for="password"></label>
138+
<input id="password" name="password" tabindex="1" type="password" value="" class="text"
139+
placeholder="<?php echo htmlspecialchars($passHint, ENT_QUOTES); ?>"/>
140+
141+
<?php
142+
if (null !== $this->data['errorCode']) {
143+
?>
144+
<div class="alert alert-danger text-center">
145+
<?php echo htmlspecialchars('Error ' . $this->data['errorCode'] . ': ' . $this->data['errorMessage']); ?>
146+
</div>
147+
<?php
148+
}
149+
?>
150+
151+
<div class="row text-center">
152+
<div class="col-xs-12">
153+
<label for="otp"></label>
154+
<input id="otp" name="otp" tabindex="1" value="" class="col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-8 col-xs-offset-2 col-12 col-offset-0"
155+
autocomplete="one-time-code" type="text" inputmode="numeric" pattern="[0-9]{6,}"
156+
placeholder="<?php echo htmlspecialchars($otpHint, ENT_QUOTES); ?>"/>
157+
</div>
158+
<div class="col-xs-12">
159+
<button id="submitButton" tabindex="1" class="btn btn-primary col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-8 col-xs-offset-2 col-12 col-offset-0" type="submit" name="Submit">
160+
<span><?php echo htmlspecialchars(getTranslation($config, 'login_button', 'Login', $this), ENT_QUOTES); ?></span>
161+
</button>
162+
</div>
163+
</div>
164+
165+
<!-- Hidden input which store the info about changes for future use in backend-->
166+
<input id="mode" type="hidden" name="mode" value="<?php echo $this->data['mode'] ?? 'otp'; ?>"/>
167+
<input id="pushAvailable" type="hidden" name="pushAvailable"
168+
value="<?php echo $this->data['pushAvailable']; ?>"/>
169+
<input id="otpAvailable" type="hidden" name="otpAvailable"
170+
value="<?php echo $this->data['otpAvailable']; ?>"/>
171+
<input id="webAuthnSignRequest" type="hidden" name="webAuthnSignRequest"
172+
value='<?php echo $this->data['webAuthnSignRequest']; ?>'/>
173+
<input id="u2fSignRequest" type="hidden" name="u2fSignRequest"
174+
value='<?php echo $this->data['u2fSignRequest']; ?>'/>
175+
<input id="modeChanged" type="hidden" name="modeChanged" value="0"/>
176+
<input id="step" type="hidden" name="step"
177+
value="<?php echo $this->data['step']; ?>"/>
178+
<input id="webAuthnSignResponse" type="hidden" name="webAuthnSignResponse" value=""/>
179+
<input id="u2fSignResponse" type="hidden" name="u2fSignResponse" value=""/>
180+
<input id="origin" type="hidden" name="origin" value=""/>
181+
<input id="loadCounter" type="hidden" name="loadCounter"
182+
value="<?php echo $this->data['loadCounter']; ?>"/>
183+
184+
<!-- Additional input to persist the message -->
185+
<input type="hidden" name="message"
186+
value="<?php echo $this->data['message']; ?>"/>
187+
188+
<?php
189+
// If enrollToken load QR Code
190+
if (isset($this->data['tokenQR'])) {
191+
echo htmlspecialchars(
192+
getTranslation($config, 'scanTokenQR', 'Scan QR token', $this)
193+
); ?>
194+
<div class="tokenQR">
195+
<?php echo '<img src="' . $this->data['tokenQR'] . '" />'; ?>
196+
</div>
197+
<?php
198+
}
199+
?>
200+
</div>
201+
202+
<?php
203+
// Organizations
204+
if (array_key_exists('organizations', $this->data)) {
205+
?>
206+
<div class="identifier-shown">
207+
<?php echo htmlspecialchars(getTranslation($config, 'organization', 'Organization', $this)); ?>
208+
<label>
209+
<select name="organization" tabindex="3">
210+
211+
<?php
212+
if (array_key_exists('selectedOrg', $this->data)) {
213+
$selectedOrg = $this->data['selectedOrg'];
214+
} else {
215+
$selectedOrg = null;
216+
}
217+
218+
foreach ($this->data['organizations'] as $orgId => $orgDesc) {
219+
if (is_array($orgDesc)) {
220+
$orgDesc = $this->t($orgDesc);
221+
}
222+
223+
if ($orgId === $selectedOrg) {
224+
$selected = 'selected="selected" ';
225+
} else {
226+
$selected = '';
227+
}
228+
229+
echo '<option ' . $selected . 'value="' . htmlspecialchars(
230+
$orgId,
231+
ENT_QUOTES
232+
) . '">' . htmlspecialchars($orgDesc) . '</option>';
233+
} ?>
234+
</select>
235+
</label>
236+
</div>
237+
<?php
238+
} ?>
239+
</div> <!-- focused -->
240+
</div> <!-- slide-out-->
241+
</div> <!-- form-panel -->
242+
243+
<div id="AlternateLoginOptions" class="groupMargin">
244+
<h3 class="text-center"><?php echo getTranslation($config, 'alternate_login_options', 'Alternate login options', $this); ?></h3>
245+
<!-- Alternate Login Options-->
246+
<div class="row text-center">
247+
<div class="col-xs-12">
248+
<button id="useWebAuthnButton" name="useWebAuthnButton" class="alternate-btn btn btn-primary col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-8 col-xs-offset-2 col-12 col-offset-0" type="button">
249+
<span><?php echo getTranslation($config, 'webauthn', 'WebAuthn', $this); ?></span>
250+
</button>
251+
</div>
252+
<div class="col-xs-12">
253+
<button id="usePushButton" name="usePushButton" class="alternate-btn btn btn-primary col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-8 col-xs-offset-2 col-12 col-offset-0" type="button">
254+
<span><?php echo getTranslation($config, 'push', 'Push', $this); ?></span>
255+
</button>
256+
</div>
257+
<div class="col-xs-12">
258+
<button id="useOTPButton" name="useOTPButton" class="alternate-btn btn btn-primary col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-8 col-xs-offset-2 col-12 col-offset-0" type="button">
259+
<span><?php echo getTranslation($config, 'otp', 'OTP', $this); ?></span>
260+
</button>
261+
</div>
262+
<div class="col-xs-12">
263+
<button id="useU2FButton" name="useU2FButton" class="alternate-btn btn btn-primary col-md-4 col-md-offset-4 col-sm-6 col-sm-offset-3 col-xs-8 col-xs-offset-2 col-12 col-offset-0" type="button">
264+
<span><?php echo getTranslation($config, 'u2f', 'U2F', $this); ?></span>
265+
</button>
266+
</div>
267+
</div>
268+
</div>
269+
</form>
270+
</div> <!-- End of login -->
271+
272+
<?php
273+
if (!empty($this->data['links'])) {
274+
echo '<ul class="links">';
275+
foreach ($this->data['links'] as $l) {
276+
echo '<li><a href="' . htmlspecialchars(
277+
$l['href'],
278+
ENT_QUOTES
279+
) . '">' . htmlspecialchars($this->t($l['text'])) . '</a></li>';
280+
}
281+
echo '</ul>';
282+
}
283+
?>
284+
285+
<script src="<?php echo htmlspecialchars(Module::getModuleUrl('privacyidea/js/webauthn.js'), ENT_QUOTES); ?>">
286+
</script>
287+
288+
<script src="<?php echo htmlspecialchars(Module::getModuleUrl('privacyidea/js/u2f-api.js'), ENT_QUOTES); ?>">
289+
</script>
290+
291+
<meta id="privacyidea-step" name="privacyidea-step" content="<?php echo $this->data['step']; ?>">
292+
<meta id="privacyidea-hide-alternate" name="privacyidea-hide-alternate" content="<?php echo (
293+
!$this->data['pushAvailable']
294+
&& (($this->data['u2fSignRequest']) === '')
295+
&& (($this->data['webAuthnSignRequest']) === '')
296+
) ? 'true' : 'false'; ?>">
297+
298+
<meta id="privacyidea-translations" name="privacyidea-translations" content="<?php
299+
$translations = [];
300+
$translation_keys = [
301+
'alert_webauthn_insecure_context', 'alert_webauthn_unavailable', 'alert_webAuthnSignRequest_error',
302+
'alert_u2f_insecure_context', 'alert_u2f_unavailable', 'alert_U2FSignRequest_error',
303+
];
304+
foreach ($translation_keys as $translation_key) {
305+
$translations[$translation_key] = $this->t(sprintf('{privacyidea:privacyidea:%s}', $translation_key));
306+
}
307+
echo htmlspecialchars(json_encode($translations));
308+
?>">
309+
310+
<script src="<?php echo htmlspecialchars(Module::getModuleUrl('privacyidea/js/loginform.js'), ENT_QUOTES); ?>">
311+
</script>
312+
313+
<?php
314+
$this->includeAtTemplateBase('includes/footer.php');
315+
?>
316+
317+
<?php
318+
319+
function getTranslation($config, $key, $fallback, $t)
320+
{
321+
foreach ($config as $dictionary) {
322+
if (!str_contains($t->t('{' . $dictionary . ':' . $key . '}'), 'not translated')) {
323+
return $t->t('{' . $dictionary . ':' . $key . '}');
324+
}
325+
}
326+
327+
return $fallback;
328+
}

www/res/css/privacyidea.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.message--common-error {
2+
margin-left: 18px;
3+
width: 520px;
4+
}
5+
6+
.btn-wrap {
7+
text-align: center;
8+
}
9+
10+
.alternate-btn {
11+
margin-top: 1rem;
12+
}
13+
14+
.identifier-shown {
15+
margin-bottom: 5rem;
16+
}
17+
18+
#otp {
19+
margin-top: 0;
20+
}
21+
22+
#submitButton {
23+
margin-top: 1rem;
24+
}
25+
26+
@media (max-width: 576px) {
27+
.col-12 {
28+
width: 100%;
29+
}
30+
31+
.col-offset-0 {
32+
margin-left: 1%;
33+
}
34+
}

0 commit comments

Comments
 (0)