En el Dockerfile de creación de la imagen hay que hacer un pequeño cambio para copiar el logo de passkeys con fondo negro al directorio de instalación del módulo de passkeys dentro del contenedor.
Esto es necesario porque el logo que viene por defecto es blanco y si el botón de entrar con passkey es también blanco, pues no se ve.
COPY ./src/config/FIDO_Passkey_mark_A_black.jpg /root/.local/lib/python3.14/site-packages/passkeys/static/passkeys/imgs/
Para ello, en el directorio ./src/config hay que dejar el logo correcto. Lo descargué de la página de la FIDO Alliance.
settings.py`En settings.py` hay que hacer los siguientes cambios:
Añadir 'passkeys', a INSTALLED_APPS:
INSTALLED_APPS = [
...
...
'passkeys',
...
...
]
Añadir el backend de autenticación:
AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend']
Añadir las siguientes variables:
FIDO_SERVER_ID="finanzas.reymota.es" # Server rp id for FIDO2, it the full domain of your project
FIDO_SERVER_NAME="Finanzas"
KEY_ATTACHMENT = None # Set None to allow all authenticator attachment
en
FIDO_SERVER_IDhay que poner el nombre del dominio. Este de aquí es un ejemplo.
urls.py del proyecto.En el fichero urls.py del proyecto, hay que añadir una entrada para passkeys.
urlpatterns = [
...
...
path('passkeys/', include('passkeys.urls')),
...
...
]
No olvidar importar include:
from django.urls import path, include
reymotausersEn reymotausers hay que añadir un nuevo fichero llamado auth.py con el siguiente contenido:
import json
from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.shortcuts import render, redirect
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework.serializers import Serializer, CharField
from rest_framework.views import APIView
from passkeys.api.token_backends import get_token_response
def loginView(request):
if request.method == "POST":
passkeys_raw = request.POST.get("passkeys", "")
if passkeys_raw:
user = authenticate(request)
if user:
login(request, user)
return redirect(request.POST.get("next", "") or "principal")
form = AuthenticationForm(request)
form.add_error(None, "Passkey authentication failed. Try again.")
else:
form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
user = form.get_user()
login(request, user)
return redirect(request.POST.get("next", "") or "principal")
else:
form = AuthenticationForm(request)
return render(request, "registration/login.html", {"form": form})
# def create_session(request,username):
# user=User.objects.get(username=username)
# user.backend='django.contrib.auth.backends.ModelBackend'
# login(request, user)
# return HttpResponseRedirect(reverse('home'))
def logoutView(request):
logout(request) # pragma: no cover
return render(request,"logout.html",{}) # pragma: no cover
class LoginSerializer(Serializer):
username = CharField()
password = CharField()
class LoginAPIView(APIView):
"""
Username + password login API.
Returns the same token format as passkey authentication
(JWT, DRF Token, or session depending on project config).
Use this to get an initial token before registering passkeys.
"""
permission_classes = [AllowAny]
serializer_class = LoginSerializer
def post(self, request):
serializer = LoginSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = authenticate(
request,
username=serializer.validated_data['username'],
password=serializer.validated_data['password'],
)
if user is None:
raise AuthenticationFailed("Invalid username or password")
token_data = get_token_response(user, request)
return Response({
'user_id': user.pk,
'username': user.get_username(),
**token_data,
})
A continuación, hay que modificar urls.py de accounts añadiendo
from reymotausers.auth import loginView
y en urlpatterns cambiar el path de login por:
path('login/', loginView, name='login'),
En el directorio templates hay que añadir una carpeta passkeys con un fichero manage.html cuyo contenido es el siguiente:
{% extends "passkeys/base.html" %}
{% load static %}
{% block head %}
{{block.super}}
<script type="application/javascript" src="{% static 'passkeys/js/base64url.js'%}"></script>
<script type="application/javascript" src="{% static 'passkeys/js/helpers.js'%}"></script>
<script type="application/javascript">
var MakeCredReq = (makeCredReq) => {
makeCredReq.publicKey.challenge = base64url.decode(makeCredReq.publicKey.challenge);
makeCredReq.publicKey.user.id = base64url.decode(makeCredReq.publicKey.user.id);
for(let excludeCred of makeCredReq.publicKey.excludeCredentials) {
excludeCred.id = base64url.decode(excludeCred.id);
}
return makeCredReq
}
function begin_reg(){
fetch('{% url 'passkeys:reg_begin' %}',{}).then(function(response) {
if(response.ok)
{
return response.json().then(function (req){
return MakeCredReq(req)
});
}
throw new Error('Error getting registration data!');
}).then(function(options) {
if (!navigator.credentials) {
throw new Error('WebAuthn is not available. Ensure the page is served over HTTPS (secure context).');
}
return navigator.credentials.create(options);
}).then(function(attestation) {
attestation["key_name"] = $("#key_name").val();
return fetch('{% url 'passkeys:reg_complete' %}', {
headers:{'X-CSRFToken': "{{csrf_token}}"},
method: 'POST',
body: JSON.stringify(publicKeyCredentialToJSON(attestation))
});
}).then(function(response) {
var stat = response.ok ? 'successful' : 'unsuccessful';
return response.json()
}).then(function (res)
{
if (res["status"] =='OK')
$("#res").html("<div class='alert alert-success'>Registered Successfully, <a href='{% url "passkeys:home"%}'> Refresh</a></div>")
else
$("#res").html("<div class='alert alert-danger'>Registration Failed as " + res["message"] + ", <a href='javascript:void(0)' onclick='begin_reg()'> try again </a> </div>")
}, function(reason) {
$("#res").html("<div class='alert alert-danger'>Registration Failed as " +reason +", <a href='javascript:void(0)' onclick='begin_reg()'> try again </a> </div>")
})
}
function confirmDel(id) {
$.ajax({
url:"{% url 'passkeys:delKey' %}",
method:"POST",
data:{"id":id,"csrfmiddlewaretoken":"{{ csrf_token }}"},
success:function (data) {
alert(data)
window.location= "{%url 'passkeys:home'%}";
}
})
}
function start(){
if (!navigator.credentials) {
alert("WebAuthn is not available. This feature requires a secure context (HTTPS).");
return;
}
$("#modal-title").html("Enter a token name")
$("#modal-body").html(`<p>Please enter a name for your new token</p>
<input type="text" placeholder="e.g Laptop, PC" id="key_name" class="form-control"/><br/>
<div id="res"></div>
`)
$("#actionBtn").remove();
$("#modal-footer").prepend(`<button id='actionBtn' class='btn btn-success' onclick="begin_reg()">Start</button>`)
$("#popUpModal").modal('show')
}
function deleteKey(id,name)
{
$("#modal-title").html("Confirm Delete")
$("#modal-body").html(`Are you sure you want to delete '${name}'? you may lose access to your system if this your only 2FA.`);
$("#actionBtn").remove()
$("#modal-footer").prepend("<button id='actionBtn' class='btn btn-danger' onclick='confirmDel("+id+")'>Confirm Deletion</button>")
$("#popUpModal").modal('show')
}
function toggleKey(id) {
$.ajax({
url:"{% url 'passkeys:toggle' %}",
data:{"id":id,"csrfmiddlewaretoken":"{{ csrf_token }}"},
method:"POST",
success:function (data) {
if (data == "Error")
$("#toggle_"+id).toggle()
},
error:function (data) {
$("#toggle_"+id).toggle()
}
})
}
{%if enroll%}
$(document).ready(start)
{%endif%}
</script>
<link href="{% static 'passkeys/css/bootstrap-toggle.min.css' %}" rel="stylesheet">
<script src="{% static 'passkeys/js/bootstrap-toggle.min.js'%}"></script>
{% endblock %}
{% block content %}
{{block.super}}
<br/>
<br/>
<div class="container">
<div class="row">
<div class="offset-5 col-2" style="text-align: center">
<div class="btn-group">
<button class="btn btn-success" onclick='start()'> Add Key</button>
</div>
</div>
</div>
<br/>
<table class="table table-striped">
<tr>
<th>Name</th>
<th>Date Added</th>
<th>Platform</th>
<th>Last Used</th>
<th>Status</th>
<th>Delete</th>
</tr>
{% if keys %}
{% for key in keys %}
<tr>
<td>{{ key.name }}</td>
<td>{{ key.added_on }}</td>
<td>{{ key.platform }}</td>
<td>{% if key.last_used %}{{ key.last_used }}{% else %}Never{% endif %}</td>
<td><input type="checkbox" id="toggle_{{ key.id }}" {% if key.enabled %}checked{% endif %} data-onstyle="success" data-offstyle="danger" onchange="toggleKey({{ key.id }})" data-toggle="toggle" class="status_chk"></td>
<td><a href="javascript:void(0)" onclick="deleteKey({{ key.id }},'{{ key.name }}')"> <span class="fa fa-trash fa-solid fa-trash-can bi bi-trash-fill"></span></a></td>
</tr>
{% endfor %}
{% else %}
<tr>
<td colspan="7" align="center">You didn't have any keys yet.</td>
</tr>
{% endif %}
</table>
</div>
{% include "passkeys/modal.html" %}
{% endblock %}
en la template login.html hay que añadir los componentes necesarios para ofrecer la posibilidad de entrar con passkey.
Para ello, antes del final del form (</form>) y después del botón de login propiamente dicho, hay que añadir otro botón para ofrecer la posibilidad de entrar con passkeys:
<div class="text-center">
<input type="hidden" name="passkeys" id="passkeys"/>
<button class="btn app-btn-secondary w-100 theme-btn mx-auto" style="margin-top: 10px" type="button" onclick="authn('login-form')"><img src="{% static 'passkeys/imgs/FIDO_Passkey_mark_A_black.jpg' %}" style="width: 24px"/> Entrar con clave de paso</button>
</div>
La clave están en el tag button en cual tiene una opción onclick que hace que se invoque a la funcionalidad de passkeys.
Aquí es donde he metido un nuevo logo de FIDO Passkey que es mejor (para mis web) que el que viene por defecto que está en blanco total.
También he añadido un estilo específico para que se separe un poco del botón de login que tiene justo encima:
style="margin-top: 10px"
Por último, antes del cierre de body hay que añadir:
{%include 'passkeys/passkeys.js' with allow_password=True %}
base.htmlEn base.html hay que añadir una opción al menú de usuario para que este pueda gestionar las llaves de paso.
En el dropdown, se añade la línea resaltada:
<ul class="dropdown-menu" aria-labelledby="user-dropdown-toggle">
{% if user.is_authenticated %}
<li>
<a class="dropdown-item">{{ user.nombre }}</a>
</li>
<!-- Esto es para el menú de gestión de llaves de paso -->
<li>
<a class="dropdown-item" href="{% url 'passkeys:home' %}">Passkeys</a><
</li>
<li>
<a class="dropdown-item">
<form method="post" action="{% url 'logout' %}" >
{% csrf_token %}
<button
style="background: none!important;
border: none;
padding: 0!important;
/*optional*/
font-family: arial, sans-serif;
/*input has OS specific font-family*/
/*color: #069;
text-decoration: underline;*/
cursor: pointer;"
type="submit">Salir</button>
</form>
</a>
</li>
{% else %}
<li>
<a class="dropdown-item" href="{% url 'login' %}">Entrar</a>
</li>
{% endif %}
</ul>
requirements.txtEn requirements.txt hay que añadir estos dos requisitos para que sean instalados en la imagen:
django-passkeys>=2.1
djangorestframework-simplejwt>=5.0
Cuando se lanza la aplicación, en entrypoing.sh se llama a migrate y a collectstatic.
Esto hace que se cree la tabla de llaves de paso y también que se copien los componentes estáticos de passkeys en el directorio correspondiente.
El primer proyecto que modifiqué fue el de ReyMotaAppsOC. Luego seguí por misfinanzas. En este último no estaban bien configuradas las templates en lo que se refiere principalmente al bloque head.
Lo dejé igual que en ReyMotaAppsOC. Básicamente, _head.html no cierra el head con </head> y se cierra en las plantillas que la incluyen. Solo en base.html se include {% block head %}{% endblock %}. Con esto la template passkeys/manage.html incluye el código necesario para la función de añadir llaves.
On branch passkeys Your branch is up to date with 'origin/passkeys'.
Changes not staged for commit: (use "git add ..." to update what will be committed) (use "git restore ..." to discard changes in working directory)
modified: .current_version
modified: .kube/config
modified: .version
modified: ../passkeys-django.md
modified: ../src/accounts/urls.py
modified: ../src/reymotausers/auth.py
modified: ../src/templates/registration/login.html