Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

The Journey

Slide 4

Slide 4 text

REGISTER LOGIN DASHBOARD

Slide 5

Slide 5 text

0x00 - Getting Set Up

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Install Dependencies $ pip install flask

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

Create Basic Templates ➔ Home ➔ Register ➔ Login ➔ Dashboard

Slide 10

Slide 10 text

Flask Simple Auth: {{ title }} {% block body %}{% endblock %} ➔ templates/base.html Home | Login | Register More HTML

Slide 11

Slide 11 text

{% 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

Slide 12

Slide 12 text

{% extends "base.html" %} {% set title = 'Register' %} {% block body %}

Create an Account

First Name:
Last Name:
Email:
Password:
{% endblock %} ➔ templates/register.html

Slide 13

Slide 13 text

{% extends "base.html" %} {% set title = 'Login' %} {% block body %}

Log Into Your Account

Email:
Password:
{% endblock %} ➔ templates/login.html

Slide 14

Slide 14 text

{% extends "base.html" %} {% set title = 'Dashboard' %} {% block body %}

Dashboard

Welcome to your dashboard! You are now logged in.

{% endblock %} ➔ templates/dashboard.html

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

@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

Slide 18

Slide 18 text

$ FLASK_APP=server.py flask run Create Basic Templates

Slide 19

Slide 19 text

0x01 - Forms

Slide 20

Slide 20 text

First Name: Last Name: Email: Password: You know... HTML!

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

0x02 - Databases

Slide 23

Slide 23 text

$ mongo MongoDB shell version: 3.2.11 > Mongo

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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”

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

@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

Slide 29

Slide 29 text

0x03 - Sessions

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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.

Slide 32

Slide 32 text

Browser Server Cookies Cookies Authentication Cookies

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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')

Slide 37

Slide 37 text

Verifying

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

0x04 - Storing Passwords

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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.

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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')

Slide 45

Slide 45 text

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')

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Homework! http://plaintextoffenders.com/

Slide 48

Slide 48 text

0x05 - Refactoring

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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!

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

0x06 - CSRF

Slide 53

Slide 53 text

Let’s Say...

Slide 54

Slide 54 text

Cross Site Request Forgery Hey Randall, Check out this picture of my dog!

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

CSRF Protection in HTML ...

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

0x07 - Security Best Practices

Slide 59

Slide 59 text

User Server secret Always Use SSL

Slide 60

Slide 60 text

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)

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

BONUS! JWTs suck for web authentication.

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

How Do People Use JWTs?

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

“… 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)

Slide 70

Slide 70 text

Why Do JWTs Suck?

Slide 71

Slide 71 text

You’re Going to Hit the DB Anyway

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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.

Slide 74

Slide 74 text

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.