aaugustin/django-sesame

Feature: Enforce same session link usage

Opened this issue · 3 comments

I have implemented a variation of django-sesame in my own code. One security enhancement that I implemented was the requierement that the link be used in the same browser session This meant that even if the link was leaked, the person receiving it must be on the same device that issued the request.

I suspect this as an option might be beneficial to enhance the security of this package.

django-sesame is primarily designed to provide secure authentication tokens using only state available in the database (vs. in the session).

If you're using sessions and you're storing them on the server, you don't need the cryptography provided by django-sesame. You can generate a random token with secrets.token_urlsafe, store that token and its expiry date in the session, then check that you're getting the same token before the expiry date from the verification link.

That doesn't work if you're storing sessions in cookies because they are signed but not encrypted, meaning that the user could decode the session and look up the token there, defeating the purpose. In that situation, you could create a random token and store it in the session, then use scoped tokens with scope=f"browser:{your_random_token}.


Can you share your solution? I'm curious to see what it looks like. Depending on how much code and complexity it adds to the library, I may consider it. Specifically, I want to check if you've done it in a way that doesn't cause token generation to know about sessions or cookies.

My implementation was an afterthought. I had already created the whole magic link flow and realized that it would be far too easy to leak out the url to someone that likely should not have it (thinking insecure email transit, sharing links etc - everything you have already mentioned)

So my implementation, rather than just using session alone, it inverted the process. It added the Primary Key of the login link to the session and compared at use time. I am thinking this could be really easy for this repo to implement as it could be put behind a setting on both the generation and the use.

The benefit to creating the code in the DB is purely for analytical purposes - who is completing the process and who is not. If you simply added the code to the session you would not have that data unless you stored it elsewhere.

The one line of code I think you could add (and the most salient from my code) is:

request.session["login_code_pk"] = login_code.pk

Pretty dead simple on the generation side.

The more questionable (for ease of implementation) code for the validation side is something along the lines of:

    pk = request.session.get("login_code_pk")

    expiry = now() - timedelta(hours=1)

    try:
        login_code = models.LoginCode.objects.get(code__iexact=code, used=False, pk=pk, created__gt=expiry)
    except models.LoginCode.DoesNotExist:
        return None

    login_code.used = True
    login_code.used_ip, is_routable = get_client_ip(request)
    login_code.save()

    return login_code.user

Clear - thank you. I think there's a valid use case here.

This would require adding a new API or extending the current API because get_user takes a User in argument, not a Request.