Save 37% off PRO during our Black Friday Sale! »

2017 - Everything You Ever Wanted to Know About Web Authentication in Python

Db2ee812bdc6fd057f8f4209c08b6f63?s=47 PyBay
August 21, 2017

2017 - Everything You Ever Wanted to Know About Web Authentication in Python

Description

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?

By the end of this talk, you’ll be intimately familiar with web authentication in Python.

Abstract

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
By the end of this talk, you’ll be intimately familiar with web authentication in Python.

Bio

Randall Degges leads Developer Advocacy at Okta, previously Stormpath, where he builds open source security libraries and helps make the internet a little safer. In a prior life, Randall was the CTO of OpenCNAM, the largest Caller ID API service.

In his free time, Randall geeks out on web best practices, explores new technologies, and spends an inordinate amount of time writing Python, Node, and Go projects. As a fun fact, Randall runs ipify.org, one of the largest IP lookup APIs which serves over 25 billion requests per month.

https://www.youtube.com/watch?v=IdysobPKa6g

Db2ee812bdc6fd057f8f4209c08b6f63?s=128

PyBay

August 21, 2017
Tweet

Transcript

  1. Everything You Ever Wanted to Know About Web Authentication in

    Python @rdegges *Almost
  2. I’m Randall Degges Run Developer Advocacy at Okta Python /

    Node / Go Hacker
  3. The Journey

  4. REGISTER LOGIN DASHBOARD

  5. 0x00 - Getting Set Up

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

  7. Install Dependencies $ pip install flask

  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
  9. Create Basic Templates ➔ Home ➔ Register ➔ Login ➔

    Dashboard
  10. <html> <head> <title>Flask Simple Auth: {{ title }}</title> </head> <body>

    {% block body %}{% endblock %} </body> </html> ➔ templates/base.html Home | Login | Register More HTML
  11. {% extends "base.html" %} {% set title = 'Home' %}

    {% block body %} <h1>Flask Simple Auth!</h1> <p> Welcome to Flask Simple Auth! Please <a href="/login">login</a> or <a href="/register">register</a> to continue. </p> {% endblock %} ➔ templates/index.html
  12. {% extends "base.html" %} {% set title = 'Register' %}

    {% block body %} <h1>Create an Account</h1> <form method="post"> <span>First Name:</span> <input type="text" name="firstName" required=true> <br> <span>Last Name:</span> <input type="text" name="lastName" required=true> <br> <span>Email:</span> <input type="email" name="email" required=true> <br> <span>Password:</span> <input type="password" name="password" required=true> <br> <input type="submit"> </form> {% endblock %} ➔ templates/register.html
  13. {% extends "base.html" %} {% set title = 'Login' %}

    {% block body %} <h1>Log Into Your Account</h1> <form method="post"> <span>Email:</span> <input type="email" name="email" required=true> <br> <span>Password:</span> <input type="password" name="password" required=true> <br> <input type="submit"> </form> {% endblock %} ➔ templates/login.html
  14. {% extends "base.html" %} {% set title = 'Dashboard' %}

    {% block body %} <h1>Dashboard</h1> <p>Welcome to your dashboard! You are now logged in.</p> {% endblock %} ➔ templates/dashboard.html
  15. Create Basic Web App ➔ Define Views ➔ Render Templates

    ➔ Start Server
  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", methods=['GET', 'POST']) def login(): return render_template('login.html') @app.route("/register", methods=['GET', 'POST']) 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
  17. $ FLASK_APP=server.py flask run Create Basic Templates

  18. 0x01 - Forms

  19. <form method="post"> First Name: <input type="text" name="firstName" required> Last Name:

    <input type="text" name="lastName" required> Email: <input type="email" name="email" required> Password: <input type="password" name="password" required> <input type="submit"> </form> You know... HTML!
  20. 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
  21. 0x02 - Databases

  22. $ mongo MongoDB shell version: 3.2.11 > Mongo

  23. > use test; switched to db test > show collections;

    > db.users.insert({ email: "r@rdegges.com", password: "woot!" }); WriteResult({ "nInserted" : 1 }) > db.users.find(); { "_id" : ObjectId("59247d0f3dacfc7ef19a41d2"), "email" : "r@rdegges.com", "password" : "woot!" } > The Basics
  24. $ pip install flask-pymongo app = Flask(__name__) app.config['MONGO_DBNAME'] = 'flask-simple-auth'

    mongo = PyMongo(app) flask-pymongo
  25. 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
  26. > db.users.find() { "_id" : ObjectId("59248139c6670c68737612bd"), "firstName" : "Randall", "lastName"

    : "Degges", "email" : "r@rdegges.com", "password" : "woot!", "__v" : 0 } Verifying
  27. @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
  28. 0x03 - Sessions

  29. Home Page Dashboard What If... Billing You had to enter

    your credentials over and over again on every page you visited?
  30. Website The Idea Login Unique ID Hey! 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.
  31. Browser Server Cookies Cookies Authentication Cookies

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

    } <html> … </html> Headers Body
  33. Set-Cookie: session=12345 Body { "Set-Cookie": "session=12345" } Creating Cookies Set-Cookie:

    a=b; c=d; e=f
  34. Body { "User-Agent": "cURL/1.2.3", "Accept": "*/*", "Host": "localhost:3000", "Cookie": "session=12345"

    } Reading Cookies
  35. 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')
  36. Verifying

  37. 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
  38. 0x04 - Storing Passwords

  39. { "_id" : ObjectId("59248139c6670c68737612bd"), "firstName" : "Randall", "lastName" : "Degges",

    "email" : "r@rdegges.com", "password" : "woot!", "__v" : 0 } Current User Data
  40. 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.
  41. Hashing • md5 • sha256 • bcrypt • scrypt •

    argon2 • etc. • SHITTY • NOPE! • SAFE • AWESOME • YES!! • NO
  42. bcrypt (pseudo) >>> password = 'hi' >>> hash = bcrypt(password)

    >>> print(hash) '$2a$10$uS.pE0aS0NlsgbvLd6EruO5VDKllinIZLF3C84OYzWHFiyKYfZVXy'
  43. 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')
  44. 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')
  45. Our Improved User { "_id" : ObjectId("5924ad584f7ddf1eea467408"), "firstName" : "Randall",

    "lastName" : "Degges", "email" : "r@rdegges.com", "password" : "$2a$14$czZ5IJCRsi7TvqwW8InVsOOnzBffT3e19unr1ecFq0XQGiTatN0wm", "__v" : 0 }
  46. Homework! http://plaintextoffenders.com/

  47. 0x05 - Refactoring

  48. 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
  49. 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
  50. @app.route("/dashboard") @login_required def dashboard(): print('hello: {}'.format(user.first_name)) return render_template('dashboard.html') Refactoring

  51. 0x06 - CSRF

  52. Let’s Say... <!-- bank.com/withdraw --> <form> <input type="text" name="amount"> <input

    type="text" name="toAccount"> </form>
  53. Cross Site Request Forgery Hey Randall, Check out this picture

    of my dog! <img src="http://bank.com/withdraw?amount=1000000&amp; toAccount=BadGuy">
  54. CSRF Tokens Browser Server CSRF Cookie GET /login Login Page

    w/ CT POST /login w/ CT NO! >:S
  55. CSRF Protection in HTML <form method="post"> ... <input type="hidden" name="_csrf_token"

    value="{{ csrf_token() }}"> </form>
  56. CSRF Protection $ pip install flask-seasurf from flask_seasurf import SeaSurf

    csrf = SeaSurf(app)
  57. 0x07 - Security Best Practices

  58. User Server secret Always Use SSL

  59. 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)
  60. Don’t Roll Your Own ➔ Flask-Login ➔ Flask-Security ➔ Okta

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

  62. BONUS! JWTs suck for web authentication.

  63. What are JWTs? - JSON data - Cryptographically signed -

    Not fancy - Not encrypted
  64. What’s a Cryptographic Signature? Randall Degges That’s a signature!

  65. What Do JWTs Actually Do? Prove that some data was

    generated by someone else.
  66. How Do People Use JWTs?

  67. NOTE Never store sensitive information in LocalStorage / SessionStorage. NEVER

    EVER.
  68. “… 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)
  69. Why Do JWTs Suck?

  70. You’re Going to Hit the DB Anyway

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

  72. 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.
  73. 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.