Multifactor Authentication


<cyli>

Ying Li

(at) Rackspace

Multifactor Authentication

Alice & Bob

Multifactor authentication

  • something you know
  • something you have
  • something you are
MITM password
phishing keylogging replay password
MITM phishing keylogging replay password MITM theft token
stealing replay biometric

2FA

  • something you know
  • something you have
phishing keylogging replay password theft token MITM
phishing replay keylogging password theft token MITM
theft token MITM phishing keylogging replay password
phishing keylogging replay password theft token MITM
MITM phishing keylogging replay password password
phishing keylogging replay password theft token MITM
... password ... token MITM root phone
OATH (open authentication) TOTP (time-based
one time password)

TOTP(<secret key>,
<time counter>)


(RFC 6238, RFC 4226)

pip install cryptography

or

pip install otpauth

or

...

pip install twilio

pip install yubikey

or

pip install
yubico-client

or

...

  • add 2FA to account
  • validate 2FA on login
  • remove 2FA from account

assuming...


class User(object):
  """Represents a user account"""
  username = ""               # user's name/id
  possession_factors = ()     # in preference order

  def validate_password(self, pwd):
    """Validates the password against
    the stored hash"""

  def email(self, *message):
    """Emails the user"""

  def exists(self):
    """Whether a user exists"""

                    

... assuming...


class Factor(object):
  """Represents a type of factor or device"""
  prompt = ""       # what to ask user

  def start(self):
    """Preps the factor, such as SMS - does
    nothing for other factors"""

  def check_otp(self, otp):
    """Validates that the OTP is valid
    for this factor"""
                    

TOTP

=?

TOTP Factor


from os import urandom
from otpauth import OtpAuth

class TOTPFactor(object):
  prompt = "token?"

  def __init__(self, secret=None):
    self.secret = secret or urandom(40)

  def start(self): pass

  def check_otp(self, otp):
    otpa = OtpAuth(self.secret)
    return otpa.valid_totp(otp)
                    

add 2FA to account: TOTP


from bobcraft.totp_factor import TOTPFactor
from bobcraft.made_up import get_user_input

def add_totp(user):
  totp = TOTPFactor()
  qr_code = generate_qr_code(totp, user.username)

  totp_token = get_user_input(prompt=qr_code)

  if totp.check_otp(totp_token):
    user.possession_factors += (totp,)
                    

otpauth://type/label?
secret=...&issuer=...

label = issuer: account_name

generate_qr_code


from otpauth import OtpAuth
import qrcode

def generate_qr_code(totp, username):
  otpa = OtpAuth(totp.secret)
  uri = otpa.to_uri(
    'totp', 'BobCraft:{0}'.format(username),
    'BobCraft')
  return qrcode.make(uri)
                    

SMS

# 123 456 ( ) =?

SMS Factor


from bobcraft.twilio_creds import (
    account_sid, auth_token, from_number)
from twilio.rest import TwilioRestClient

class SMSFactor(object):
  prompt = "code from text?"

  def __init__(self, user_phone):
    self.user_phone = user_phone
    self.stored = None
    self.client = TwilioRestClient(
        account_sid, auth_token)
  ...
                    

SMS Factor (cont.)


  ...
  def start(self):
    from random import SystemRandom
    self.stored = SystemRandom().randint(1, 999999)
    self.client.sms.messages.create(
      body=str(self.stored),
      to=self.user_phone, from_=from_number)

  def check_otp(self, otp):
    return int(otp) == self.stored
                    

add 2FA to account: SMS


from bobcraft.sms_factor import SMSFactor
from bobcraft.made_up import get_user_input

def add_sms(user):
  user_phone = get_user_input(prompt="phone #")
  sms = SMSFactor(user_phone)

  if sms not in user.possession_factors:
      sms.start()
      sms_token = get_user_input(prompt="token")

      if sms.check_otp(sms_token):
        user.possession_factors += (sms,)
                    

Yubikey

fifjgjgkhchb irdrfdnlnghhfgrtnnlgedjlftrbdeut
public ID OTP
up to 128 bits 128 bits (32 characters)
=? and yubico

Yubikey Factor


from bobcraft.yubikey_creds import (
    client_id, secret_key)
from yubico_client import Yubico

class YubikeyFactor(object):
  prompt = "press yubikey button"

  def __init__(self, yubikey_id):
    self.yubikey_id = yubikey_id

  def start(self): pass
  ...
                    

Yubikey Factor (cont.)


  ...
  def check_otp(self, otp):
    client = Yubico(client_id, secret_key)
    return (get_public_id(otp) == self.yubikey_id
            and client.verify(otp))

def get_public_id(otp):  # module level function
  return otp[:-32]
                    

add 2FA to account: Yubikey


from bobcraft.yubikey_factor import (
  YubikeyFactor, get_public_id)
from bobcraft.made_up import get_user_input

def add_yubikey(user):
  otp = get_user_input(prompt="Yubikey OTP")

  yubikey = YubikeyFactor(get_public_id(otp))

  if (yubikey not in user.possession_factors and
      yubikey.check_otp(otp)):
    user.possession_factors += (yubikey,)
                    

validate 2FA on login

user pass ( ) and ?

login


from bobcraft.user import User

def login(username, password, post_login):
  user = User(username)
  if (user.exists() and
      user.validate_password(password) and
      check_possession_factor(user)):
    post_login(user)
  else:
    raise InvalidLogin(username)
                    

validate preferred token
on login


from bobcraft.made_up import get_user_input

def check_possession_factor(user):
  if len(user.possession_factors) == 0:
    return True

  factor = user.possession_factors[0]
  factor.start()
  otp = get_user_input(prompt=factor.prompt)
  return factor.check_otp(otp)
                    

validate backup token
on login


from bobcraft.made_up import get_user_choice

def check_possession_factor(user):
  if len(user.possession_factors) == 0:
    return True

  factor = get_user_choice(user.possession_factors)
  factor.start()
  otp = get_user_input(prompt=factor.prompt)

  if factor != user.possession_factors[0]:
    user.email("Login with backup factor", factor)

  return factor.check_otp(otp)
                    

remove 2FA from account

remove factor


def remove_factor(user, factor):
  user.possession_factors = (
    f for f in user.possession_factors
    if f != factor)

  user.email("A factor has been removed", factor)
                    

secure remove factor


from functools import partial
from bobcraft.login import login

def secure_remove_factor(user, factor):
  password = get_user_input(prompt="password")

  login(user.username, password,
        partial(remove_factor, factor=factor))
                    

https://github.com/cyli/bobcraft

https://www.twilio.com/blog/
2013/04/add-two-factor-authentication-to-your-website-with-google-authenticator-and-twilio-sms.html

pip install django-otp

pip install django-otp-twilio

pip install django-otp-yubikey

settings.py


MIDDLEWARE_CLASSES = [
    ...
    'django_otp.middleware.OTPMiddleware'
]

INSTALLED_APPS = [
    ...
    'django_otp',
    'django_otp.plugins.otp_totp',
    'django_otp.plugins.otp_static',
    'otp_twilio',   # optional
    'otp_yubikey'   # optional
]
                    

pip install
django-two-factor-auth

(no yubikey support)

settings.py


from django.core.urlresolvers import reverse_lazy
LOGIN_URL = reverse_lazy('two_factor:login')

INSTALLED_APPS = [
    ...
    'django_otp',
    ...
    'two_factor'
]

...
                    

settings.py


...

TWO_FACTOR_CALL_GATEWAY = TWO_FACTOR_SMS_GATEWAY = (
    'two_factor.gateways.twilio.Twilio')

OTP_TWILIO_ACCOUNT = ''  # Twilio account sid
OTP_TWILIO_AUTH = ''     # Twilio account token
OTP_TWILIO_FROM = ''     # phone number with Twilio
                    

urls.py


from two_factor.urls import urlpatterns as tf_urls
from two_factor.gateways.twilio.urls import (
    urlpatterns as tf_twilio_urls)

urlpatterns = patterns(
    ...
    url(r'', include(tf_urls + tf_twilio_urls,
                     'two_factor')),
)
                    

django-otp

django-two-factor-auth

http://django-two-factor-auth.readthedocs.org

http://example-two-factor-auth.herokuapp.com

  • add 2FA to account
  • validate 2FA on login
  • remove 2FA from account

Happily Ever After?

login session key api key
session key
TLS tls certificate

goto fail



password token MITM
password token MITM
MITM TLS tls certificate api key

API keys should be:

  • revokable and auditable
  • limited access (least privilege)


@otp_required

django-otp

Password/Factor changes:

  • require full credentials
  • warn user
MITM TLS tls certificate

Recovery should be:

  • auditable
  • rare
  • difficult

Standard recovery methods:

  • backup devices
  • email reset
  • 2FA
  • backup factors
  • passwords
  • protect email
  • protect phone
  • system updates
  • 2FA
  • account reset
  • TLS
  • limit privilege
  • system updates

Fin


github: cyli
twitter: cyli