Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Almost Everything You Ever Wanted to Know About Web Authentication in Python

Almost Everything You Ever Wanted to Know About Web Authentication in Python

Want to learn how web authentication works? How your login information is transmitted from a web browser to a web server, and what happens from that point onwards? How authentication protocols work behind the scenes?

In this talk, Randall Degges, Developer Advocate at Okta, will walk you through the entire web authentication flow, covering:

- Credential transmission
- Cookies
- Sessions
- Databases
- Best practices

Randall Degges

October 06, 2017
Tweet

More Decks by Randall Degges

Other Decks in Programming

Transcript

  1. Everything You Ever
    Wanted to Know
    About Web Authentication in Python
    @rdegges
    *Almost

    View Slide

  2. I’m Randall Degges
    Run Developer Advocacy
    at Okta
    Python / Node / Go
    Hacker

    View Slide

  3. The Journey

    View Slide

  4. REGISTER LOGIN DASHBOARD

    View Slide

  5. 0x00 - Getting Set Up

    View Slide

  6. Assumptions
    Python https://www.python.org
    Mongo https://www.mongodb.com/

    View Slide

  7. Install Dependencies
    $ pip install flask

    View Slide

  8. Prepare the App
    $ touch server.py
    $ mkdir templates
    $ touch templates/base.html
    $ touch templates/index.html
    $ touch templates/register.html
    $ touch templates/login.html
    $ touch templates/dashboard.html

    View Slide

  9. Create Basic Templates
    ➔ Home
    ➔ Register
    ➔ Login
    ➔ Dashboard

    View Slide



  10. Flask Simple Auth: {{ title }}


    {% block body %}{% endblock %}


    ➔ templates/base.html
    Home | Login | Register
    More HTML

    View Slide

  11. {% extends "base.html" %}
    {% set title = 'Home' %}
    {% block body %}
    Flask Simple Auth!

    Welcome to Flask Simple Auth! Please login or
    register to continue.

    {% endblock %}
    ➔ templates/index.html
    Use the base template
    Set the title variable
    Insert HTML into the body

    View Slide

  12. {% extends "base.html" %}
    {% set title = 'Register' %}
    {% block body %}
    Create an Account

    First Name:
    required=true>


    Last Name:
    required=true>


    Email:
    required=true>


    Password:
    required=true>




    {% endblock %}
    ➔ templates/register.html

    View Slide

  13. {% extends "base.html" %}
    {% set title = 'Login' %}
    {% block body %}
    Log Into Your Account

    Email:
    required=true>


    Password:
    required=true>




    {% endblock %}
    ➔ templates/login.html

    View Slide

  14. {% extends "base.html" %}
    {% set title = 'Dashboard' %}
    {% block body %}
    Dashboard
    Welcome to your dashboard! You are now logged
    in.
    {% endblock %}
    ➔ templates/dashboard.html

    View Slide

  15. Create Basic Web App
    ➔ Define Views
    ➔ Render Templates
    ➔ Start Server

    View Slide

  16. from flask import Flask
    from flask import render_template
    app = Flask(__name__)
    @app.route("/")
    def index():
    return render_template('index.html')
    @app.route("/login")
    def login():
    return render_template('login.html')
    @app.route("/register")
    def register():
    return render_template('register.html')
    @app.route("/logout")
    def logout():
    pass
    @app.route("/dashboard")
    def dashboard():
    return render_template('dashboard.html')
    ➔ server.py

    View Slide

  17. @app.route("/")
    def index():
    return render_template('index.html')
    Run the function below when
    the user hits the / url
    Return a response to the user

    View Slide

  18. $ FLASK_APP=server.py flask run
    Create Basic Templates

    View Slide

  19. 0x01 - Forms

    View Slide


  20. First Name:
    Last Name:
    Email:
    Password:


    You know... HTML!

    View Slide

  21. from flask import jsonify
    from flask import request
    # ...
    @app.route("/register", methods=['GET', 'POST'])
    def register():
    if request.method == 'POST':
    return jsonify(request.form)
    return render_template('register.html')
    Form Data
    Tell the function to run for
    GET and POST requests

    View Slide

  22. 0x02 - Databases

    View Slide

  23. $ mongo
    MongoDB shell version: 3.2.11
    >
    Mongo

    View Slide

  24. > use test;
    switched to db test
    The Basics
    > show collections;
    >
    Get or create a database
    > db.users.insert({ email: "[email protected]", password: "woot!" });
    WriteResult({ "nInserted" : 1 })
    Show tables
    > db.users.find();
    { "_id" : ObjectId("59247d0f3dacfc7ef19a41d2"), "email" : "[email protected]", "password" :
    "woot!" }
    Create table and insert something
    SELECT * FROM users;

    View Slide

  25. $ pip install flask-pymongo
    app = Flask(__name__)
    app.config['MONGO_DBNAME'] = 'flask-simple-auth'
    mongo = PyMongo(app)
    flask-pymongo

    View Slide

  26. from flask import redirect
    from flask import url_for
    # ...
    @app.route("/register", methods=['GET', 'POST'])
    def register():
    if request.method == 'POST':
    mongo.db.users.insert_one(request.form.to_dict())
    return redirect(url_for('dashboard'))
    return render_template('register.html')
    Creating Users
    Redirect to “/dashboard”

    View Slide

  27. > db.users.find()
    {
    "_id" : ObjectId("59248139c6670c68737612bd"),
    "firstName" : "Randall",
    "lastName" : "Degges",
    "email" : "[email protected]",
    "password" : "woot!",
    "__v" : 0
    }
    Verifying

    View Slide

  28. @app.route("/login", methods=['GET', 'POST'])
    def login():
    if request.method == 'POST':
    user = mongo.db.users.find_one({'email': request.form['email']})
    if user['password'] == request.form['password']:
    return redirect(url_for('dashboard'))
    return render_template('login.html')
    Authenticating Users
    Find the user by email
    Verify the user’s password

    View Slide

  29. 0x03 - Sessions

    View Slide

  30. Home Page Dashboard
    What If...
    Billing
    You had to enter your credentials over and
    over again on every page you visited?

    View Slide

  31. Website
    The Idea
    Login
    Unique ID
    Hey! Browser! Remember this
    value! I need you to send it back
    to me next time you visit so I
    know who you are!
    Browser
    These are
    sessions.

    View Slide

  32. Browser Server
    Cookies
    Cookies
    Authentication
    Cookies

    View Slide

  33. HTTP Requests / Responses
    {
    "User-Agent": "Mozilla/5.0 ...",
    "Content-Type": "text/html"
    }



    Headers
    Body

    View Slide

  34. Set-Cookie: session=12345
    Body
    {
    "Set-Cookie": "session=12345"
    }
    Creating Cookies
    Set-Cookie: a=b; c=d; e=f

    View Slide

  35. Body
    {
    "User-Agent": "cURL/1.2.3",
    "Accept": "*/*",
    "Host": "localhost:3000",
    "Cookie": "session=12345"
    }
    Reading Cookies

    View Slide

  36. Using Sessions
    from flask import session
    # ...
    app.config['SECRET_KEY'] = 'SUPER_SECRET!'
    # ...
    @app.route("/login", methods=['GET', 'POST'])
    def login():
    if request.method == 'POST':
    user = mongo.db.users.find_one({'email': request.form['email']})
    if user['password'] == request.form['password']:
    session['id'] = str(user['_id'])
    return redirect(url_for('dashboard'))
    return render_template('login.html')

    View Slide

  37. Verifying

    View Slide

  38. from bson.objectid import ObjectId
    # ...
    @app.route("/dashboard")
    def dashboard():
    if 'id' not in session:
    return redirect(url_for('login'))
    user = mongo.db.users.find_one({'_id': ObjectId(session['id'])})
    if user:
    return render_template('dashboard.html')
    return redirect(url_for('login'))
    Improving the Dashboard
    If there’s no session, bail
    Find the user by id
    Everything OK? Show the
    dashboard

    View Slide

  39. 0x04 - Storing Passwords

    View Slide

  40. {
    "_id" : ObjectId("59248139c6670c68737612bd"),
    "firstName" : "Randall",
    "lastName" : "Degges",
    "email" : "[email protected]",
    "password" : "woot!",
    "__v" : 0
    }
    Current User Data

    View Slide

  41. Password Hashing!
    Given a hash, you can never retrieve the
    original password that created it. Hashes are
    one-way.
    The same password always generates the
    same hash.

    View Slide

  42. Hashing
    ● md5
    ● sha256
    ● bcrypt
    ● scrypt
    ● argon2
    ● etc.
    ● SHITTY
    ● NOPE!
    ● SAFE
    ● AWESOME
    ● YES!!
    ● NO

    View Slide

  43. bcrypt (pseudo)
    >>> password = 'hi'
    >>> hash = bcrypt(password)
    >>> print(hash)
    '$2a$10$uS.pE0aS0NlsgbvLd6EruO5VDKllinIZLF3C84OYzWHFiyKYfZVXy'

    View Slide

  44. Improving Registration
    $ pip install bcrypt passlib
    from passlib.hash import bcrypt
    # ...
    @app.route("/register", methods=['GET', 'POST'])
    def register():
    if request.method == 'POST':
    user_data = request.form.to_dict()
    user_data['password'] = bcrypt.hash(user_data['password'])
    user = mongo.db.users.insert_one(user_data)
    session['id'] = str(user['_id'])
    return redirect(url_for('dashboard'))
    return render_template('register.html')

    View Slide

  45. Improving Login
    @app.route("/login", methods=['GET', 'POST'])
    def login():
    if request.method == 'POST':
    user = mongo.db.users.find_one({'email': request.form['email']})
    if user and bcrypt.verify(request.form['password'], user['password']):
    session['id'] = str(user['_id'])
    return redirect(url_for('dashboard'))
    return render_template('login.html')

    View Slide

  46. Our Improved User
    {
    "_id" : ObjectId("5924ad584f7ddf1eea467408"),
    "firstName" : "Randall",
    "lastName" : "Degges",
    "email" : "[email protected]",
    "password" : "$2a$14$czZ5IJCRsi7TvqwW8InVsOOnzBffT3e19unr1ecFq0XQGiTatN0wm",
    "__v" : 0
    }

    View Slide

  47. Homework!
    http://plaintextoffenders.com/

    View Slide

  48. 0x05 - Refactoring

    View Slide

  49. from flask import g
    from werkzeug.local import LocalProxy
    user = LocalProxy(lambda: g.user)
    # ...
    @app.before_request
    def load_user():
    if 'id' in session:
    g.user = mongo.db.users.find_one({'_id': ObjectId(session['id'])})
    del g.user['password']
    return
    Smart User Loading

    View Slide

  50. from functools import wraps
    def login_required(func):
    @wraps(func)
    def decorator(*args, **kwargs):
    if not user:
    return redirect(url_for('login'))
    return func(*args, **kwargs)
    return decorator
    Force Authentication
    GTFO
    Welcome
    buddy!

    View Slide

  51. @app.route("/dashboard")
    @login_required
    def dashboard():
    print('hello: {}'.format(user.first_name))
    return render_template('dashboard.html')
    Refactoring

    View Slide

  52. 0x06 - CSRF

    View Slide

  53. Let’s Say...





    View Slide

  54. Cross Site Request Forgery
    Hey Randall,
    Check out this picture of my dog!
    src="http://bank.com/withdraw?amount=1000000&
    toAccount=BadGuy">

    View Slide

  55. CSRF Tokens
    Browser
    Server
    CSRF
    Cookie
    GET /login
    Login Page w/ CT
    POST /login w/
    CT
    NO! >:S

    View Slide

  56. CSRF Protection in HTML

    ...


    View Slide

  57. CSRF Protection
    $ pip install flask-seasurf
    from flask_seasurf import SeaSurf
    csrf = SeaSurf(app)

    View Slide

  58. 0x07 - Security Best Practices

    View Slide

  59. User Server
    secret
    Always Use SSL

    View Slide

  60. Use Cookie Flags
    from datetime import timedelta
    # Don't let Javascript access cookies
    app.config['SESSION_COOKIE_HTTPONLY'] = True
    # Only set the cookie over SSL
    app.config['SESSION_COOKIE_SECURE'] = True
    # How long should the session last?
    app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=30)

    View Slide

  61. Don’t Roll Your Own
    ➔ Flask-Login
    ➔ Flask-Security
    ➔ Okta

    View Slide

  62. Code https://github.com/rdegges
    Slides https://speakerdeck.com/rdegges
    Resources
    @oktadev
    @rdegges

    View Slide

  63. BONUS!
    JWTs suck for web authentication.

    View Slide

  64. What are JWTs?
    - JSON data
    - Cryptographically signed
    - Not fancy
    - Not encrypted

    View Slide

  65. What’s a Cryptographic Signature?
    Randall Degges
    That’s a
    signature!

    View Slide

  66. What Do JWTs Actually Do?
    Prove that some data
    was generated by
    someone else.

    View Slide

  67. How Do People Use JWTs?

    View Slide

  68. NOTE
    Never store sensitive information
    in LocalStorage / SessionStorage.
    NEVER EVER.

    View Slide

  69. “… In other words, any authentication your application requires
    can be bypassed by a user with local privileges to the machine
    on which the data is stored. Therefore, it's recommended not
    to store any sensitive information in local storage.”
    - OWASP (Open Web Application Security Project)

    View Slide

  70. Why Do JWTs Suck?

    View Slide

  71. You’re Going to Hit the DB Anyway

    View Slide

  72. Don’t Sign Things Twice
    Randall Degges
    That’s
    dumb!
    Randall Degges

    View Slide

  73. What ARE JWTs Good For?
    Website
    Reset my password.
    Ok! I’ve emailed
    you a link that has a
    JWT in the URL
    which will expire in
    30 minutes.
    Ok! I clicked
    the link.
    This JWT looks legit. I
    suppose I’ll let you reset
    your password.
    Ok, your PW has
    been reset.

    View Slide

  74. In Summary
    JWTs are not meant to be used
    for persisting state. Making
    them do the same thing as
    cookies in a less efficient and
    secure manner is bad for you.
    Don’t use them unless you
    know how.

    View Slide