mirror of
https://github.com/simple-login/app.git
synced 2026-04-07 19:27:34 +00:00
Store FIDO information about the registered keys to send the proper keys when authentication
This commit is contained in:
committed by
Adrià Casajús
parent
7a92b60231
commit
8db564963a
+30
-7
@@ -27,6 +27,23 @@ from app.models import User, Fido, MfaBrowser
|
||||
from app.utils import sanitize_next_url
|
||||
|
||||
|
||||
def _backfill_fido_metadata(fido_key: Fido, sk_assertion: dict) -> None:
|
||||
"""Backfill credential metadata that was not stored at registration time."""
|
||||
changed = False
|
||||
if fido_key.credential_type is None:
|
||||
credential_type = sk_assertion.get("type")
|
||||
if credential_type:
|
||||
fido_key.credential_type = credential_type
|
||||
changed = True
|
||||
if fido_key.authenticator_attachment is None:
|
||||
authenticator_attachment = sk_assertion.get("authenticatorAttachment")
|
||||
if authenticator_attachment:
|
||||
fido_key.authenticator_attachment = authenticator_attachment
|
||||
changed = True
|
||||
if changed:
|
||||
LOG.d(f"Backfilled metadata for fido credential_id={fido_key.credential_id}")
|
||||
|
||||
|
||||
class FidoTokenForm(FlaskForm):
|
||||
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
|
||||
remember = BooleanField(
|
||||
@@ -105,6 +122,7 @@ def fido():
|
||||
auto_activate = False
|
||||
else:
|
||||
user.fido_sign_count = new_sign_count
|
||||
_backfill_fido_metadata(fido_key, sk_assertion)
|
||||
Session.commit()
|
||||
del session[MFA_USER_ID]
|
||||
|
||||
@@ -155,13 +173,18 @@ def fido():
|
||||
webauthn_users, challenge
|
||||
)
|
||||
webauthn_assertion_options = webauthn_assertion_options.assertion_dict
|
||||
try:
|
||||
# HACK: We need to upgrade to webauthn > 1 so it can support specifying the transports
|
||||
for credential in webauthn_assertion_options["allowCredentials"]:
|
||||
del credential["transports"]
|
||||
except KeyError:
|
||||
# Should never happen but...
|
||||
pass
|
||||
# Inject stored transports per credential, falling back to removing the field
|
||||
# if none are stored (keys registered before metadata collection).
|
||||
fido_by_credential_id = {fido.credential_id: fido for fido in fidos}
|
||||
for credential in webauthn_assertion_options.get("allowCredentials", []):
|
||||
fido = fido_by_credential_id.get(credential.get("id"))
|
||||
if fido and fido.transports:
|
||||
try:
|
||||
credential["transports"] = json.loads(fido.transports)
|
||||
except Exception:
|
||||
del credential["transports"]
|
||||
else:
|
||||
credential.pop("transports", None)
|
||||
|
||||
return render_template(
|
||||
"auth/fido.html",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import json
|
||||
import secrets
|
||||
import struct
|
||||
import uuid
|
||||
|
||||
import cbor2
|
||||
import webauthn
|
||||
from flask import render_template, flash, redirect, url_for, session
|
||||
from flask_login import login_required, current_user
|
||||
@@ -17,6 +19,31 @@ from app.models import Fido, RecoveryCode
|
||||
from app.user_settings import regenerate_user_alternative_id
|
||||
|
||||
|
||||
def _extract_aaguid(att_obj_b64: str) -> str | None:
|
||||
"""Extract the AAGUID from the attestation object's authData (bytes 37-53)."""
|
||||
try:
|
||||
# webauthn.py uses _webauthn_b64_decode which handles urlsafe base64
|
||||
padding = 4 - len(att_obj_b64) % 4
|
||||
if padding != 4:
|
||||
att_obj_b64 += "=" * padding
|
||||
import base64
|
||||
|
||||
raw = base64.urlsafe_b64decode(att_obj_b64)
|
||||
att_obj = cbor2.loads(raw)
|
||||
auth_data = att_obj.get("authData", b"")
|
||||
if len(auth_data) < 53:
|
||||
return None
|
||||
flags = struct.unpack("!B", auth_data[32:33])[0]
|
||||
# AT flag (bit 6) indicates attested credential data is present
|
||||
if not (flags & 0x40):
|
||||
return None
|
||||
aaguid_bytes = auth_data[37:53]
|
||||
return str(uuid.UUID(bytes=aaguid_bytes))
|
||||
except Exception as e:
|
||||
LOG.w(f"Failed to extract AAGUID: {e}")
|
||||
return None
|
||||
|
||||
|
||||
class FidoTokenForm(FlaskForm):
|
||||
key_name = StringField("key_name", validators=[validators.DataRequired()])
|
||||
sk_assertion = HiddenField("sk_assertion", validators=[validators.DataRequired()])
|
||||
@@ -64,6 +91,7 @@ def fido_setup():
|
||||
current_user.fido_uuid = fido_uuid
|
||||
Session.flush()
|
||||
|
||||
transports = sk_assertion.get("transports")
|
||||
Fido.create(
|
||||
credential_id=str(fido_credential.credential_id, "utf-8"),
|
||||
uuid=fido_uuid,
|
||||
@@ -71,6 +99,10 @@ def fido_setup():
|
||||
sign_count=fido_credential.sign_count,
|
||||
name=fido_token_form.key_name.data,
|
||||
user_id=current_user.id,
|
||||
credential_type=sk_assertion.get("type"),
|
||||
authenticator_attachment=sk_assertion.get("authenticatorAttachment"),
|
||||
transports=transports if isinstance(transports, list) else None,
|
||||
aaguid=_extract_aaguid(sk_assertion.get("attObj", "")),
|
||||
)
|
||||
regenerate_user_alternative_id(current_user)
|
||||
Session.commit()
|
||||
|
||||
@@ -361,6 +361,13 @@ class Fido(Base, ModelMixin):
|
||||
sign_count = sa.Column(sa.BigInteger(), nullable=False)
|
||||
name = sa.Column(sa.String(128), nullable=False, unique=False)
|
||||
user_id = sa.Column(sa.ForeignKey("users.id", ondelete="cascade"), nullable=True)
|
||||
# Credential metadata for debugging and proper authentication routing
|
||||
credential_type = sa.Column(sa.String(32), nullable=True)
|
||||
authenticator_attachment = sa.Column(sa.String(32), nullable=True)
|
||||
transports = sa.Column(sa.JSON(), nullable=True) # JSON array, e.g. ["usb","nfc"]
|
||||
aaguid = sa.Column(
|
||||
sa.String(36), nullable=True
|
||||
) # UUID format, identifies device model
|
||||
|
||||
__table_args__ = (sa.Index("ix_fido_user_id", "user_id"),)
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Add credential metadata columns to fido table
|
||||
|
||||
Revision ID: 4a9f8c2e1b3d
|
||||
Revises: 3ee37864eb67
|
||||
Create Date: 2026-03-19 09:00:00.000000
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '4a9f8c2e1b3d'
|
||||
down_revision = '3ee37864eb67'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.add_column('fido', sa.Column('credential_type', sa.String(length=32), nullable=True))
|
||||
op.add_column('fido', sa.Column('authenticator_attachment', sa.String(length=32), nullable=True))
|
||||
op.add_column('fido', sa.Column('transports', sa.Text(), nullable=True))
|
||||
op.add_column('fido', sa.Column('aaguid', sa.String(length=36), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column('fido', 'aaguid')
|
||||
op.drop_column('fido', 'transports')
|
||||
op.drop_column('fido', 'authenticator_attachment')
|
||||
op.drop_column('fido', 'credential_type')
|
||||
Vendored
+7
@@ -104,6 +104,10 @@ const transformNewAssertionForServer = (newAssertion) => {
|
||||
|
||||
const registrationClientExtensions = newAssertion.getClientExtensionResults();
|
||||
|
||||
const transports = (typeof newAssertion.response.getTransports === 'function')
|
||||
? newAssertion.response.getTransports()
|
||||
: [];
|
||||
|
||||
return {
|
||||
id: newAssertion.id,
|
||||
rawId: b64enc(rawId),
|
||||
@@ -111,6 +115,8 @@ const transformNewAssertionForServer = (newAssertion) => {
|
||||
attObj: b64enc(attObj),
|
||||
clientData: b64enc(clientDataJSON),
|
||||
registrationClientExtensions: JSON.stringify(registrationClientExtensions),
|
||||
authenticatorAttachment: newAssertion.authenticatorAttachment || null,
|
||||
transports: transports,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -134,5 +140,6 @@ const transformAssertionForServer = (newAssertion) => {
|
||||
clientData: b64RawEnc(clientDataJSON),
|
||||
signature: hexEncode(sig),
|
||||
assertionClientExtensions: JSON.stringify(assertionClientExtensions),
|
||||
authenticatorAttachment: newAssertion.authenticatorAttachment || null,
|
||||
};
|
||||
};
|
||||
@@ -31,6 +31,7 @@
|
||||
{{ fido_token_form.sk_assertion(class="form-control", placeholder="") }}
|
||||
<div class="text-center mt-3">
|
||||
<button id="btnVerifyKey"
|
||||
type="button"
|
||||
class="btn btn-lg btn-danger"
|
||||
onclick="verifyAdminKey();">Use your security key</button>
|
||||
</div>
|
||||
@@ -58,7 +59,6 @@
|
||||
}
|
||||
|
||||
const skAssertion = transformAssertionForServer(assertion);
|
||||
skAssertion.authenticatorAttachment = assertion.authenticatorAttachment || null;
|
||||
$('#sk_assertion').val(JSON.stringify(skAssertion));
|
||||
$('#formVerifyKey').submit();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user