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

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", 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

Slide 17

Slide 17 text

$ FLASK_APP=server.py flask run Create Basic Templates

Slide 18

Slide 18 text

0x01 - Forms

Slide 19

Slide 19 text

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

Slide 20

Slide 20 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

Slide 21

Slide 21 text

0x02 - Databases

Slide 22

Slide 22 text

$ mongo MongoDB shell version: 3.2.11 > Mongo

Slide 23

Slide 23 text

> use test; switched to db test > show collections; > db.users.insert({ email: "[email protected]", password: "woot!" }); WriteResult({ "nInserted" : 1 }) > db.users.find(); { "_id" : ObjectId("59247d0f3dacfc7ef19a41d2"), "email" : "[email protected]", "password" : "woot!" } > The Basics

Slide 24

Slide 24 text

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

Slide 25

Slide 25 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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 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

Slide 28

Slide 28 text

0x03 - Sessions

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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.

Slide 31

Slide 31 text

Browser Server Cookies Cookies Authentication Cookies

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 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 36

Slide 36 text

Verifying

Slide 37

Slide 37 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

Slide 38

Slide 38 text

0x04 - Storing Passwords

Slide 39

Slide 39 text

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

Slide 40

Slide 40 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 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 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 44

Slide 44 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 45

Slide 45 text

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

Slide 46

Slide 46 text

Homework! http://plaintextoffenders.com/

Slide 47

Slide 47 text

0x05 - Refactoring

Slide 48

Slide 48 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 49

Slide 49 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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

0x06 - CSRF

Slide 52

Slide 52 text

Let’s Say...

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

CSRF Protection in HTML ...

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

0x07 - Security Best Practices

Slide 58

Slide 58 text

User Server secret Always Use SSL

Slide 59

Slide 59 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 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

BONUS! JWTs suck for web authentication.

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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

Slide 66

Slide 66 text

How Do People Use JWTs?

Slide 67

Slide 67 text

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

Slide 68

Slide 68 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 69

Slide 69 text

Why Do JWTs Suck?

Slide 70

Slide 70 text

You’re Going to Hit the DB Anyway

Slide 71

Slide 71 text

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

Slide 72

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

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