Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Override Flask-Security's /login endpoint

I'm working on a site where the users login via OAuth, not a password-based system.

Because of this, Flask-Security's default login page doesn't actually work for my use case, because I need the /login endpoint for the OAuth setup. I was able to make it so that my /login route wasn't being overridden by Flask-Security's by changing the SECURITY_LOGIN_URL setting option.

This is all working fine, the OAuth login page shows up and returns all the information needed.

The problem kicks in because I'm also trying to utilize the @login_required decorator.

If the user isn't logged in, instead of redirecting to my /login page, the @login_required decorator is redirecting to Flask-Security's page.

Obviously, the config endpoint doesn't help in this situation.

Is it possible to force Flask-Security to use my login route (OAuth) instead of its page?

Here's some example code that shows what I'm talking about with Flask-Security overriding defined routes:

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, request
from flask_security import (Security, SQLAlchemyUserDatastore,
                            UserMixin, RoleMixin, login_required,
                            login_user, logout_user, current_user)
from passlib.context import CryptContext
import os


class Config:
    basedir = os.path.abspath(os.path.dirname(__file__))

    SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'app.db')
    SQLALCHEMY_MIGRATE_REPO = os.path.join(basedir, 'db_repository')
    SQLALCHEMY_TRACK_MODIFICATIONS = False

    DEBUG = True
    PORT = 8000
    HOST = "0.0.0.0"

    SECRET_KEY = "foobar123"


config = Config()


app = Flask(__name__, instance_relative_config=True)

app.config.from_object(config)

db = SQLAlchemy(app)

lm = LoginManager()

lm.init_app(app)
lm.login_view = "login"


roles_users = db.Table('roles_users',
                       db.Column('user_id',
                                 db.Integer(),
                                 db.ForeignKey('user.id')),
                       db.Column('role_id',
                                 db.Integer(),
                                 db.ForeignKey('role.id'))
                       )


class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(64), index=True, unique=True)
    email = db.Column(db.String(120), index=True, unique=True)
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))
    provider_id = db.Column(db.String(64), index=True, unique=True)

    @property
    def is_authenticated(self):
        return True

    @property
    def is_active(self):
        return True

    @property
    def is_anonymous(self):
        return True

    def get_id(self):
        return str(self.id)

    def hash_password(self, password):
        self.hashed_password = pwd_context.encrypt(password)

    def verify_password(self, password):
        return pwd_context.verify(password, self.hashed_password)


class Role(db.Model, RoleMixin):
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(80), unique=True)
    description = db.Column(db.String(255))


user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)

pwd_context = CryptContext(
    schemes=["bcrypt", "pbkdf2_sha256", "des_crypt"],
    default="bcrypt",
    all__vary_rounds=0.1
)


@lm.user_loader
def load_user(id):
    return User.query.get(int(id))


@app.route("/")
@app.route("/index")
@login_required
def index():
    """Handles calls to / and /index, return the panel"""
    return render_template("index.html")


@app.route("/login")
def login():
    """Would include a bunch of OAuth stuff, not required for this example
    If you try going to this endpoint, you'll get the Flask-Security /login
    instead."""
    return render_template("login.html")

app.run(debug=True)
like image 960
RPiAwesomeness Avatar asked May 05 '16 22:05

RPiAwesomeness


1 Answers

Funnily enough, I've come across a very similar problem today. If I manage to resolve it in a way I'd consider elegant enough for an SO answer, I'll update this. In the meantime, my thoughts on strategies for approaching the problem are:

  1. Subclass flask-security and overload only the @login_required decorator to redirect you to the right place. Probably quickest, if the redirection is the only issue you're having.
  2. If you're solely using Oauth, then use an alternative decorator to replace @login_required. Flask-OAuthlib is a useful library for oath stuff and the documents show you how to protect a resource using the `@oauth.require_oauth' decorator.

Flask-Stormpath is a commercial solution and I'm not familiar enough to comment on whether it covers this particular ground, so I shan't recommend it as a possible approach. However for background reading they have a useful overview of the authentication hornets nest associated with Flask.

like image 64
thclark Avatar answered Sep 21 '22 19:09

thclark