Welcome | Get started | Dive | Contribute | Topics | Reference | Changes | More

Two-Factor Auth, Trusted Devices, and OTP

This page explains the current two-factor authentication flow in lino.modlib.users when enable_two_factor_auth is enabled.

All examples below run on lino_book.projects.cosi5.settings.

>>> from lino import startup
>>> startup('lino_book.projects.cosi5.settings')
>>> from lino.api.doctest import *

Configuration

cosi5 enables two-factor auth in its site config:

>>> dd.plugins.users.enable_two_factor_auth
True

Default timing settings are:

>>> dd.plugins.users.device_verification_code_expires
5
>>> dd.plugins.users.trusted_device_cookie_max_age
7776000

How It Works

  1. The user signs in with username and password.

  2. If no valid trusted-device token is present, the server sends a one-time device verification code (OTP) by email.

  3. The user submits OTP using about.About.trust_device.

  4. The server stores a hash of the device token in users.TrustedDevice and sends the raw token back as an HttpOnly cookie.

  5. Later sign-ins can skip OTP when the cookie token matches an active trusted device entry.

The action is available when two-factor auth is enabled:

>>> About = rt.models.about.About
>>> bool(About.get_action_by_name('trust_device'))
True

Trusted Device Model

The token stored in browser cookie is never stored in clear text in database. Only its SHA-256 hash is persisted.

>>> TrustedDevice = rt.models.users.TrustedDevice
>>> token = TrustedDevice.make_token()
>>> bool(token)
True
>>> len(TrustedDevice.hash_token('abc'))
64

Create a trusted device for the demo user and verify that lookup works:

>>> ses = rt.login('robin')
>>> user = ses.get_user()
>>> td = TrustedDevice.register(user=user, token=token, device_name='Doctest device')
>>> found = TrustedDevice.lookup(user=user, token=token)
>>> found == td
True
>>> found.token_hash == token
False
>>> len(found.token_hash)
64
>>> td.delete()  # cleanup

Lookup with a wrong token returns no match:

>>> TrustedDevice.lookup(user=user, token='this-token-does-not-exist') is None
True