first pass, non-working

This commit is contained in:
Matthew Scragg 2015-10-13 22:52:30 -05:00
parent 459a9c2c59
commit 3c2f4a0445
17 changed files with 295 additions and 121 deletions

View file

@ -161,4 +161,4 @@ if ENV != "DEV":
ASSETS_DEBUG = False ASSETS_DEBUG = False
SQLALCHEMY_ECHO = False SQLALCHEMY_ECHO = False
MODULES = ['wiki', 'auth', 'search'] MODULES = ['wiki', 'search', 'auth', 'auth.local', 'auth.oauth']

View file

View file

View file

@ -1,9 +1,10 @@
import click import click
from realms.lib.util import random_string from realms.lib.util import random_string
from realms.modules.auth.models import User from realms.modules.auth.local.models import User
from realms.lib.util import green, red, yellow from realms.lib.util import green, red, yellow
from realms import flask_cli from realms import flask_cli
@flask_cli.group(short_help="Auth Module") @flask_cli.group(short_help="Auth Module")
def cli(): def cli():
pass pass

View file

@ -0,0 +1,107 @@
from flask import current_app, render_template
from flask.ext.login import logout_user, login_user
from realms import login_manager, db
from realms.lib.model import Model
from ..models import BaseUser
from .forms import LoginForm
from itsdangerous import URLSafeSerializer, BadSignature
from hashlib import sha256
import bcrypt
@login_manager.token_loader
def load_token(token):
# Load unsafe because payload is needed for sig
sig_okay, payload = URLSafeSerializer(current_app.config['SECRET_KEY']).loads_unsafe(token)
if not payload:
return None
# User key *could* be stored in payload to avoid user lookup in db
user = User.get_by_id(payload.get('id'))
if not user:
return None
try:
if BaseUser.signer(sha256(user.password).hexdigest()).loads(token):
return user
else:
return None
except BadSignature:
return None
class User(Model, BaseUser):
__tablename__ = 'users'
type = 'local'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(128), unique=True)
email = db.Column(db.String(128), unique=True)
password = db.Column(db.String(60))
admin = False
hidden_fields = ['password']
readonly_fields = ['email', 'password']
@property
def auth_token_id(self):
return self.password
@staticmethod
def load_user(*args, **kwargs):
return User.get_by_id(args[0])
@staticmethod
def create(username, email, password):
u = User()
u.username = username
u.email = email
u.password = User.hash_password(password)
u.save()
@staticmethod
def get_by_username(username):
return User.query().filter_by(username=username).first()
@staticmethod
def get_by_email(email):
return User.query().filter_by(email=email).first()
@staticmethod
def signer(salt):
return URLSafeSerializer(current_app.config['SECRET_KEY'] + salt)
@staticmethod
def auth(email, password):
user = User.get_by_email(email)
if not user:
# User doesn't exist
return False
if User.check_password(password, user.password):
# Password is good, log in user
login_user(user, remember=True)
return user
else:
# Password check failed
return False
@staticmethod
def hash_password(password):
return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(12))
@staticmethod
def check_password(password, hashed):
return bcrypt.hashpw(password.encode('utf-8'), hashed.encode('utf-8')) == hashed
@classmethod
def logout(cls):
logout_user()
@staticmethod
def login_form():
form = LoginForm()
return render_template('auth/local/login.html', form=form)

View file

@ -0,0 +1,51 @@
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for
from realms.modules.auth.local.models import User
from realms.modules.auth.local.forms import LoginForm, RegistrationForm
blueprint = Blueprint('auth.local', __name__)
@blueprint.route("/login/local", methods=['POST'])
def login():
form = LoginForm()
if not form.validate():
flash('Form invalid', 'warning')
return redirect(url_for('auth.login'))
if User.auth(request.form['email'], request.form['password']):
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
else:
flash('Email or Password Incorrect', 'warning')
return redirect(url_for('auth.login'))
@blueprint.route("/register", methods=['GET', 'POST'])
def register():
if not current_app.config['REGISTRATION_ENABLED']:
flash("Registration is disabled")
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
form = RegistrationForm()
if request.method == "POST":
if not form.validate():
flash('Form invalid', 'warning')
return redirect(url_for('auth.local.register'))
if User.get_by_username(request.form['username']):
flash('Username is taken', 'warning')
return redirect(url_for('auth.local.register'))
if User.get_by_email(request.form['email']):
flash('Email is taken', 'warning')
return redirect(url_for('auth.local.register'))
User.create(request.form['username'], request.form['email'], request.form['password'])
User.auth(request.form['email'], request.form['password'])
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
return render_template("auth/register.html", form=form)

View file

@ -1,39 +1,41 @@
from flask import current_app from flask import current_app
from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin from flask.ext.login import UserMixin, logout_user, login_user, AnonymousUserMixin
from realms import login_manager, db from realms import login_manager
from realms.lib.model import Model
from realms.lib.util import gravatar_url from realms.lib.util import gravatar_url
from itsdangerous import URLSafeSerializer, BadSignature from itsdangerous import URLSafeSerializer, BadSignature
from hashlib import sha256 from hashlib import sha256
import bcrypt import bcrypt
import importlib
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(auth_id):
return User.get_by_id(user_id) return Auth.load_user(auth_id)
auth_users = {}
@login_manager.token_loader class Auth(object):
def load_token(token):
# Load unsafe because payload is needed for sig
sig_okay, payload = URLSafeSerializer(current_app.config['SECRET_KEY']).loads_unsafe(token)
if not payload: @staticmethod
return None def get_auth_user(auth_type):
print auth_type
mod = importlib.import_module('realms.modules.auth.%s.models' % auth_type)
return mod.User
# User key *could* be stored in payload to avoid user lookup in db @staticmethod
user = User.get_by_id(payload.get('id')) def load_user(auth_id):
print auth_id
auth_type, user_id = auth_id.split("/")
return Auth.get_auth_user(auth_type).load_user(user_id)
if not user: @staticmethod
return None def login_forms():
forms = []
try: # TODO be dynamic
if User.signer(sha256(user.password).hexdigest()).loads(token): for t in ['local']:
return user forms.append(Auth.get_auth_user(t).login_form())
else: return forms
return None
except BadSignature:
return None
class AnonUser(AnonymousUserMixin): class AnonUser(AnonymousUserMixin):
@ -42,40 +44,42 @@ class AnonUser(AnonymousUserMixin):
admin = False admin = False
class User(Model, UserMixin): class BaseUser(UserMixin):
__tablename__ = 'users' id = None
id = db.Column(db.Integer, primary_key=True) email = None
username = db.Column(db.String(128), unique=True) username = None
email = db.Column(db.String(128), unique=True) type = 'base'
password = db.Column(db.String(60))
admin = False
hidden_fields = ['password'] def get_id(self):
readonly_fields = ['email', 'password'] return unicode("%s/%s" % (self.type, self.id))
def get_auth_token(self): def get_auth_token(self):
key = sha256(self.password).hexdigest() key = sha256(self.auth_token_id).hexdigest()
return User.signer(key).dumps(dict(id=self.id)) return BaseUser.signer(key).dumps(dict(id=self.id))
@property
def auth_token_id(self):
raise NotImplementedError
@property @property
def avatar(self): def avatar(self):
return gravatar_url(self.email) return gravatar_url(self.email)
@staticmethod @staticmethod
def create(username, email, password): def load_user(*args, **kwargs):
u = User() raise NotImplementedError
u.username = username
u.email = email @staticmethod
u.password = User.hash_password(password) def create(*args, **kwargs):
u.save() pass
@staticmethod @staticmethod
def get_by_username(username): def get_by_username(username):
return User.query().filter_by(username=username).first() pass
@staticmethod @staticmethod
def get_by_email(email): def get_by_email(email):
return User.query().filter_by(email=email).first() pass
@staticmethod @staticmethod
def signer(salt): def signer(salt):
@ -83,19 +87,7 @@ class User(Model, UserMixin):
@staticmethod @staticmethod
def auth(email, password): def auth(email, password):
user = User.get_by_email(email) raise NotImplementedError()
if not user:
# User doesn't exist
return False
if User.check_password(password, user.password):
# Password is good, log in user
login_user(user, remember=True)
return user
else:
# Password check failed
return False
@staticmethod @staticmethod
def hash_password(password): def hash_password(password):
@ -109,4 +101,8 @@ class User(Model, UserMixin):
def logout(cls): def logout(cls):
logout_user() logout_user()
@staticmethod
def login_form():
pass
login_manager.anonymous_user = AnonUser login_manager.anonymous_user = AnonUser

View file

View file

@ -0,0 +1,36 @@
from flask import render_template
from flask_oauthlib.client import OAuth
from realms import config
from ..models import BaseUser
oauth = OAuth()
class OAuthUser(BaseUser):
# OAuth remote app
app = None
class TwitterUser(OAuthUser):
app = oauth.remote_app(
'twitter',
base_url='https://api.twitter.com/1/',
request_token_url='https://api.twitter.com/oauth/request_token',
access_token_url='https://api.twitter.com/oauth/access_token',
authorize_url='https://api.twitter.com/oauth/authenticate',
consumer_key=config.TWITTER_KEY,
consumer_secret=config.TWITTER_SECRET)
def __init__(self, id_, username, email=None):
self.id = id_
self.username = username
self.email = email
@staticmethod
def load_user(*args, **kwargs):
return TwitterUser(args[0])
@staticmethod
def login_form():
return render_template('auth/oauth/twitter.html')

View file

@ -0,0 +1,30 @@
from flask import Blueprint, url_for, request, flash, redirect
from .models import TwitterUser
blueprint = Blueprint('auth.oauth', __name__)
def oauth_failed(next_url):
flash(u'You denied the request to sign in.')
return redirect(next_url)
@blueprint.route("/login/twitter")
def login_twitter():
return TwitterUser.app.authorize(callback=url_for('twitter_callback',
next=request.args.get('next') or request.referrer or None))
@blueprint.route('/login/twitter/callback')
def twitter_callback():
next_url = request.args.get('next') or url_for('index')
resp = TwitterUser.app.authorized_response()
if resp is None:
return oauth_failed(next_url)
session['twitter_token'] = (
resp['oauth_token'],
resp['oauth_token_secret']
)
session['twitter_user'] = resp['screen_name']
flash('You were signed in as %s' % resp['screen_name'])
return redirect(next_url)

View file

@ -1,72 +1,22 @@
from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for from flask import current_app, render_template, request, redirect, Blueprint, flash, url_for
from realms.modules.auth.models import User from flask.ext.login import logout_user
from realms.modules.auth.forms import LoginForm, RegistrationForm from realms.modules.auth.models import Auth
blueprint = Blueprint('auth', __name__) blueprint = Blueprint('auth', __name__)
@blueprint.route("/logout")
def logout_page():
User.logout()
flash("You are now logged out")
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
@blueprint.route("/login", methods=['GET', 'POST']) @blueprint.route("/login", methods=['GET', 'POST'])
def login(): def login():
form = LoginForm() return render_template("auth/login.html", forms=Auth.login_forms())
if request.method == "POST":
if not form.validate():
flash('Form invalid', 'warning')
return redirect(url_for('auth.login'))
if User.auth(request.form['email'], request.form['password']):
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
else:
flash('Email or Password Incorrect', 'warning')
return redirect(url_for('auth.login'))
return render_template("auth/login.html", form=form)
@blueprint.route("/register", methods=['GET', 'POST']) @blueprint.route("/logout")
def register(): def logout():
logout_user()
if not current_app.config['REGISTRATION_ENABLED']: flash("You are now logged out")
flash("Registration is disabled") return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))
form = RegistrationForm()
if request.method == "POST":
if not form.validate():
flash('Form invalid', 'warning')
return redirect(url_for('auth.register'))
if User.get_by_username(request.form['username']):
flash('Username is taken', 'warning')
return redirect(url_for('auth.register'))
if User.get_by_email(request.form['email']):
flash('Email is taken', 'warning')
return redirect(url_for('auth.register'))
User.create(request.form['username'], request.form['email'], request.form['password'])
User.auth(request.form['email'], request.form['password'])
return redirect(request.args.get("next") or url_for(current_app.config['ROOT_ENDPOINT']))
return render_template("auth/register.html", form=form)
@blueprint.route("/settings", methods=['GET', 'POST']) @blueprint.route("/settings", methods=['GET', 'POST'])
def settings(): def settings():
return render_template("auth/settings.html") return render_template("auth/settings.html")
@blueprint.route("/logout")
def logout():
User.logout()
return redirect(url_for(current_app.config['ROOT_ENDPOINT']))

View file

@ -0,0 +1,5 @@
{% from 'macros.html' import render_form, render_field %}
{% call render_form(form, action_url=url_for('auth.local.login'), action_text='Login', btn_class='btn btn-primary') %}
{{ render_field(form.email, placeholder='Email', type='email', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', required=1) }}
{% endcall %}

View file

@ -1,8 +1,6 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% from 'macros.html' import render_form, render_field %}
{% block body %} {% block body %}
{% call render_form(form, action_url=url_for('auth.login'), action_text='Login', btn_class='btn btn-primary') %} {% for form in forms %}
{{ render_field(form.email, placeholder='Email', type='email', required=1) }} {{ form|safe }}
{{ render_field(form.password, placeholder='Password', type='password', required=1) }} {% endfor %}
{% endcall %}
{% endblock %} {% endblock %}

View file

@ -1,7 +1,7 @@
{% extends 'layout.html' %} {% extends 'layout.html' %}
{% from 'macros.html' import render_form, render_field %} {% from 'macros.html' import render_form, render_field %}
{% block body %} {% block body %}
{% call render_form(form, action_url=url_for('auth.register'), action_text='Register', btn_class='btn btn-primary') %} {% call render_form(form, action_url=url_for('auth.local.register'), action_text='Register', btn_class='btn btn-primary') %}
{{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }} {{ render_field(form.username, placeholder='Username', type='username', **{"required": 1, "data-parsley-type": "alphanum"}) }}
{{ render_field(form.email, placeholder='Email', type='email', required=1) }} {{ render_field(form.email, placeholder='Email', type='email', required=1) }}
{{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }} {{ render_field(form.password, placeholder='Password', type='password', **{"required": 1, "data-parsley-minlength": "6"}) }}

View file

@ -74,7 +74,7 @@
{% else %} {% else %}
<li><a href="{{ url_for('auth.login') }}"><i class="fa fa-user"></i> &nbsp;Login</a></li> <li><a href="{{ url_for('auth.login') }}"><i class="fa fa-user"></i> &nbsp;Login</a></li>
{% if config.REGISTRATION_ENABLED %} {% if config.REGISTRATION_ENABLED %}
<li><a href="{{ url_for('auth.register') }}"><i class="fa fa-users"></i> &nbsp;Register</a></li> <li><a href="{{ url_for('auth.local.register') }}"><i class="fa fa-users"></i> &nbsp;Register</a></li>
{% endif %} {% endif %}
{% endif %} {% endif %}
</ul> </ul>