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. 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
  2. <html> <head> <title>Flask Simple Auth: {{ title }}</title> </head> <body>

    {% block body %}{% endblock %} </body> </html> ➔ templates/base.html Home | Login | Register More HTML
  3. {% 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
  4. {% 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
  5. {% 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
  6. {% 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
  7. 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
  8. <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!
  9. 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
  10. > 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;
  11. 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”
  12. @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
  13. Home Page Dashboard What If... Billing You had to enter

    your credentials over and over again on every page you visited?
  14. 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.
  15. 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')
  16. 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
  17. 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.
  18. Hashing • md5 • sha256 • bcrypt • scrypt •

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

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

    "lastName" : "Degges", "email" : "[email protected]", "password" : "$2a$14$czZ5IJCRsi7TvqwW8InVsOOnzBffT3e19unr1ecFq0XQGiTatN0wm", "__v" : 0 }
  23. 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
  24. 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!
  25. Cross Site Request Forgery Hey Randall, Check out this picture

    of my dog! <img src="http://bank.com/withdraw?amount=1000000&amp; toAccount=BadGuy">
  26. 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)
  27. “… 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)
  28. 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.
  29. 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.