frontend support for password

This commit is contained in:
2026-01-17 10:57:57 -05:00
parent 4a543f45ca
commit b61400085a
7 changed files with 85 additions and 25 deletions
+3
View File
@@ -3,6 +3,7 @@ twig:
strict_variables: true
globals:
env:
allow_password: '%env(STATIC_SECRET_ENABLED)%'
title: '%env(TITLE)%'
bg_color: '%env(BG_COLOR)%'
fg_color: '%env(FG_COLOR)%'
@@ -10,8 +11,10 @@ twig:
return_field: '%env(QUERY_PREFIX)%return'
id_field: '%env(QUERY_PREFIX)%id'
token_field: '%env(QUERY_PREFIX)%token'
password_field: '%env(QUERY_PREFIX)%password'
id_name: '%env(ID_NAME)%'
token_name: '%env(TOKEN_NAME)%'
password_name: '%env(PASSWORD_NAME)%'
submit_name: '%env(SUBMIT_NAME)%'
error_message: '%env(ERROR_MESSAGE)%'
teapot_title: '%env(TEAPOT_TITLE)%'
+4 -3
View File
@@ -26,12 +26,12 @@ parameters:
# --- extra variables ---
# query parameter prefix to prevent collisions
env(QUERY_PREFIX): '_preauth_'
# allow files in /app/public/assets directory to be served, false to disable
env(ASSETS): '1' # default enabled, boolean
# how long do we allow all traffic from an ip address after successful login
# could be useful if you have a system which does not handle cookies
env(IP_TTL): '0' # default disabled, time in seconds
# if desired, in addition to supporting a TOTP, you can set a static password
# TODO rely on checking enabled, instead of the secret directly throughout the code
env(STATIC_SECRET_ENABLED): '0' # boolean
env(STATIC_SECRET): '' # default disabled
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
env(TEAPOT): '1' # boolean
@@ -43,6 +43,7 @@ parameters:
env(ERROR_COLOR): '#ffb16d'
env(ID_NAME): 'Session ID'
env(TOKEN_NAME): 'Authentication Token'
env(PASSWORD_NAME): 'Authentication Password'
env(SUBMIT_NAME): 'Submit'
env(ERROR_MESSAGE): 'Unsuccessful login attempt'
# title and message to use on block page, if teapot is true
@@ -59,8 +60,8 @@ parameters:
app.query_prefix: '%env(QUERY_PREFIX)%'
app.totp_uri: '%env(TOTP_URI)%'
app.assets: '%env(ASSETS)%'
app.ip_ttl: '%env(IP_TTL)%'
app.static_secret_enabled: '%env(STATIC_SECRET_ENABLED)%'
app.static_secret: '%env(STATIC_SECRET)%'
app.teapot: '%env(TEAPOT)%'
+2 -1
View File
@@ -27,7 +27,8 @@
#IP_TTL=0 # default disabled, time in seconds
# if desired, in addition to supporting a TOTP, you can set a static password
#STATIC_SECRET='' # deafult disabled
#STATIC_SECRET_ENABLED='0' # boolean, disabled by default
#STATIC_SECRET='' # default disabled
# once blocked, do we respond with "I'm a teapot", false to use "Too many requests"
#TEAPOT=true # default enabled, boolean
+2 -9
View File
@@ -15,7 +15,6 @@ final readonly class ConfigBag {
private int $limitTtl;
private string $queryPrefix;
private string $totpUri;
private ?string $assetsDir;
private ?int $ipTtl;
private ?string $staticSecret;
private bool $teapot;
@@ -33,9 +32,8 @@ final readonly class ConfigBag {
#[Autowire('%app.limit_ttl%')] int $limitTtl,
#[Autowire('%app.query_prefix%')] string $queryPrefix,
#[Autowire('%app.totp_uri%')] string $totpUri,
#[Autowire('%app.assets%')] bool $assets,
#[Autowire('%kernel.project_dir%/public/assets/')] string $assetsDir,
#[Autowire('%app.ip_ttl%')] ?int $ipTtl,
#[Autowire('%app.static_secret_enabled%')] bool $staticSecretEnabled,
#[Autowire('%app.static_secret%')] ?string $staticSecret,
#[Autowire('%app.teapot%')] bool $teapot,
#[Autowire('%app.error_message%')] string $errorMessage,
@@ -49,9 +47,8 @@ final readonly class ConfigBag {
$this->limitTtl = ($limitTtl >= 1) ? $limitTtl : 86400;
$this->queryPrefix = $queryPrefix;
$this->totpUri = $totpUri ?: $utilities->loadTotp();
$this->assetsDir = $assets ? $assetsDir : null;
$this->ipTtl = $ipTtl ?: null;
$this->staticSecret = $staticSecret ?: null;
$this->staticSecret = $staticSecretEnabled ? ($staticSecret ?: null) : null;
$this->teapot = $teapot;
$this->errorMessage = $errorMessage;
$this->teapotTitle = $teapotTitle;
@@ -86,10 +83,6 @@ final readonly class ConfigBag {
return $this->totpUri;
}
public function assetsDir(): ?string {
return $this->assetsDir;
}
public function ipTtl(): ?int {
return $this->ipTtl;
}
+6 -1
View File
@@ -5,8 +5,13 @@ html { background-color: {{ env.bg_color }}; color: {{ env.fg_color }}; display:
body { display: table-cell; vertical-align: middle; }
h1 { font-size: 2.5em; font-weight: normal; text-align: center; }
p { color: {{ env.error_color }}; text-align: center; }
form { display: flex; flex-wrap: wrap; justify-content: center; }
form { align-items: baseline; display: flex; flex-wrap: wrap; justify-content: center; }
form div { width: 45%; }
div.right { text-align: right; }
div.center { text-align: center; }
div.hidden { display: none; }
span { cursor: pointer; font-size: 0.75em; text-decoration: underline; }
button { background-color: #cccccc; }
input { background-color: #ffffff; max-width: 100%; }
button, input { border: 0.0625em solid #333333; border-radius: 0.25em; color: #333333; font-size: 0.9em; }
</style>
+1 -1
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<title>{{ env.title }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
{{ include('_style.html.twig') }}
</head>
<body>
+67 -10
View File
@@ -8,25 +8,82 @@
<div class="right"><label for="preauth-id">{{ env.id_name }}:</label></div>
<div><input type="text" name="preauth_id" id="preauth-id"
autocomplete="username" required="required" autofocus="autofocus"></div>
<div class="right"><label for="preauth-token">{{ env.token_name }}:</label></div>
<div><input type="text" name="preauth_token" id="preauth-token"
autocomplete="one-time-code" required="required"></div>
{% if env.allow_password %}
<div class="right boxTk">
<label for="preauth-token">{{ env.token_name }}:</label><br>
<span id="preauth-use-pw">🔃 {{ env.password_name }}</span></div>
<div class="boxTk"><input type="text" name="preauth_token" id="preauth-token"
autocomplete="one-time-code" required="required"></div>
<div class="right boxPw hidden">
<label for="preauth-password">{{ env.password_name }}:</label><br>
<span id="preauth-use-tk">🔃 {{ env.token_name }}</span></div>
<div class="boxPw hidden"><input type="password" name="preauth_password" id="preauth-password"
autocomplete="password" disabled="disabled" required="required"></div>
{% else %}
<div class="right"><label for="preauth-token">{{ env.token_name }}:</label></div>
<div><input type="text" name="preauth_token" id="preauth-token"
autocomplete="one-time-code" required="required"></div>
{% endif %}
<div class="center"><button type="submit">{{ env.submit_name }}</button></div>
</form>
<script>
const form = document.getElementById('preauth-form');
const message = document.getElementById('preauth-message');
{% if env.allow_password %}
const usePw = document.getElementById('preauth-use-pw');
const useTk = document.getElementById('preauth-use-tk');
const boxPw = document.querySelectorAll('.boxPw');
const boxTk = document.querySelectorAll('.boxTk');
usePw.addEventListener('click', (event) => {
form.preauth_token.value = '';
form.preauth_token.disabled = true;
form.preauth_password.disabled = false;
boxTk.forEach((element) => {
element.classList.add('hidden');
});
boxPw.forEach((element) => {
element.classList.remove('hidden');
});
});
useTk.addEventListener('click', (event) => {
form.preauth_password.value = '';
form.preauth_password.disabled = true;
form.preauth_token.disabled = false;
boxPw.forEach((element) => {
element.classList.add('hidden');
});
boxTk.forEach((element) => {
element.classList.remove('hidden');
});
});
{% endif %}
form.addEventListener('submit', (event) => {
event.preventDefault();
/* make bas64url string containing our payload json object */
const data = btoa(JSON.stringify({
id: form.preauth_id.value,
token: form.preauth_token.value,
nonce: form.preauth_nonce.value,
json: true
})).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
{% if env.allow_password %}
/* make base64url string containing our payload json object */
/* payload will contain either token or password */
const data = btoa(JSON.stringify({
id: form.preauth_id.value,
...( form.preauth_token.value && { token: form.preauth_token.value }),
...(( ! form.preauth_token.value) && { password: form.preauth_password.value }),
nonce: form.preauth_nonce.value,
json: true
})).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
{% else %}
/* make base64url string containing our payload json object */
const data = btoa(JSON.stringify({
id: form.preauth_id.value,
token: form.preauth_token.value,
nonce: form.preauth_nonce.value,
json: true
})).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
{% endif %}
/* send our request to the server */
fetch(window.location.href, {