# Añadir Passkeys a mis aplicaciones Django

## Cambios en Dockerfile

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](https://fidoalliance.org/get-the-passkey-icon/).


## Cambios en ```settings.py````

En ```settings.py```` hay que hacer los siguientes cambios:

1. Añadir ```'passkeys',``` a ```INSTALLED_APPS```:

        INSTALLED_APPS = [
            ...
            ...
            'passkeys',
            ...
            ...
        ]

2. Añadir el backend de autenticación:

        AUTHENTICATION_BACKENDS = ['passkeys.backend.PasskeyModelBackend']

3. 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_ID``` hay que poner el nombre del dominio. Este de aquí es un ejemplo.

## Cambios en ```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

## Cambios en ```reymotausers```

En ```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'),




## Templates para passkeys.

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 %}

## Template de login.

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 %}

## Cambios en ```base.html```

En ```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>

## Cambios en ```requirements.txt```

En ```requirements.txt``` hay que añadir estos dos requisitos para que sean instalados en la imagen:

    django-passkeys>=2.1
    djangorestframework-simplejwt>=5.0

## Migraciones.

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.

## Otras consideraciones.

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.



## cambios para hacer que si hay passkey no pida password

On branch passkeys
Your branch is up to date with 'origin/passkeys'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." 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

no changes added to commit (use "git add" and/or "git commit -a")
=======
