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

56badf521701d4f9b3a394d3ef6e90c4?s=128

Randall Degges

October 06, 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 Use the base template Set the title variable Insert HTML into the body
  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") 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
  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
  18. $ FLASK_APP=server.py flask run Create Basic Templates

  19. 0x01 - Forms

  20. <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!
  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
  22. 0x02 - Databases

  23. $ mongo MongoDB shell version: 3.2.11 > Mongo

  24. > use test; switched to db test The Basics >

    show collections; > Get or create a database > db.users.insert({ email: "r@rdegges.com", password: "woot!" }); WriteResult({ "nInserted" : 1 }) Show tables > db.users.find(); { "_id" : ObjectId("59247d0f3dacfc7ef19a41d2"), "email" : "r@rdegges.com", "password" : "woot!" } Create table and insert something SELECT * FROM users;
  25. $ pip install flask-pymongo app = Flask(__name__) app.config['MONGO_DBNAME'] = 'flask-simple-auth'

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

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

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

    your credentials over and over again on every page you visited?
  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.
  32. Browser Server Cookies Cookies Authentication Cookies

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

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

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

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

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

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

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

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

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

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

  48. 0x05 - Refactoring

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

  52. 0x06 - CSRF

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

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

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

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

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

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

  59. User Server secret Always Use SSL

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

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

  63. BONUS! JWTs suck for web authentication.

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

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

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

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

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

    EVER.
  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)
  70. Why Do JWTs Suck?

  71. You’re Going to Hit the DB Anyway

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

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