Combining Flask-restless, Flask-security and regular Python requests
Asked Answered
D

2

10

My goal is to provide a REST API to my web application. Using:

  • Python 2.7.5
  • Flask==0.10.1
  • Flask-Restless==0.13.1
  • Flask-Security==1.7.3

I need to secure access to my data for both web and REST access. However, I am unable to get any regular python request succeeding when trying to connect to secured API.

The following outputs are obtained using the fully-functional module provided at the end of this question.

I manage to get a correct answer when using http://127.0.0.1:5000/api/v1/free_stuff:

>>> import requests
>>> r=requests.get('http://127.0.0.1:5000/api/v1/free_stuff')
>>> print 'status:', r.status_code
status: 200  # all is fine

When trying authentication with http://127.0.0.1:5000/api/v1/protected_stuff:

>>> from requests.auth import HTTPBasicAuth, HTTPDigestAuth
>>> r=requests.get('http://127.0.0.1:5000/api/v1/protected_stuff', 
                   auth=HTTPBasicAuth('test', 'test')) #  the same with ``HTTPDigestAuth``
>>> print 'status:', r.status_code
status: 401
>>> r.json()  # failed!
{u'message': u'401: Unauthorized'}

Here is a dummy functional module used to produce the above results:

from flask import Flask, render_template, url_for, redirect
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore, \
    UserMixin, RoleMixin, login_required, current_user
from flask.ext.restless import APIManager
from flask.ext.restless import ProcessingException

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'

# Create database connection object
db = SQLAlchemy(app)

# Define Flask-security models
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 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))

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))
#Some additional stuff to query over...
class SomeStuff(db.Model):
    __tablename__ = 'somestuff'
    id = db.Column(db.Integer, primary_key=True)
    data1 = db.Column(db.Integer)
    data2 = db.Column(db.String(10))
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
    user = db.relationship(User, lazy='joined', join_depth=1, viewonly=True)

# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)

# API
def auth_func(**kw):
    #import ipdb; ipdb.set_trace()
    if not current_user.is_authenticated():
        raise ProcessingException(description='Not authenticated!',
                code=401)
    return True
apimanager = APIManager(app, flask_sqlalchemy_db=db)

apimanager.create_api(SomeStuff,
    methods=['GET', 'POST', 'DELETE', 'PUT'],
    url_prefix='/api/v1',
    collection_name='free_stuff',
    include_columns=['data1', 'data2', 'user_id'])

apimanager.create_api(SomeStuff,
    methods=['GET', 'POST', 'DELETE', 'PUT'],
    url_prefix='/api/v1',
    preprocessors=dict(GET_SINGLE=[auth_func], GET_MANY=[auth_func]),
    collection_name='protected_stuff',
    include_columns=['data1', 'data2', 'user_id'])

# Create a user to test with
@app.before_first_request
def create_user():
    db.create_all()
    user_datastore.create_user(email='test', password='test')
    user_datastore.create_user(email='test2', password='test2')
    ###
    stuff = SomeStuff(data1=2, data2='toto', user_id=1)
    db.session.add(stuff)
    stuff = SomeStuff(data1=5, data2='titi', user_id=1)
    db.session.add(stuff)
    db.session.commit()

# Views
@app.route('/')
@login_required
def home():
    return render_template('index.html')

@app.route('/logout/')
def log_out():
    logout_user()
    return redirect(request.args.get('next') or '/')


if __name__ == '__main__':
    app.run()

Any idea?

[edit] to be fully functional via web interface, you need to have a templates sub-folder having at least the following login.html file:

{% block body %}
  <form action="" method=post class="form-horizontal">
    <h2>Signin to FlaskLogin(Todo) Application </h2>
    <div class="control-group">
        <div class="controls">
          <input type="text" id="username" name="username" class="input-xlarge"
            placeholder="Enter Username" required>
        </div>
    </div>

    <div class="control-group">
        <div class="controls">
          <input type="password" id="password" name="password" class="input-xlarge"
            placeholder="Enter Password" required>
        </div>
    </div>

    <div class="control-group">
        <div class="controls">
          <button type="submit" class="btn btn-success">Signin</button>
        </div>
    </div>  
  </form>
{% endblock %}
Dartboard answered 12/6, 2014 at 14:16 Comment(0)
D
17

I finally went to Flask-JWT (https://pypi.python.org/pypi/Flask-JWT/0.1.0)

Here is my modified minimal example:

from flask import Flask, render_template, request, url_for, redirect
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.security import Security, SQLAlchemyUserDatastore, \
    UserMixin, RoleMixin, login_required, current_user, logout_user
from flask.ext.restless import APIManager
from flask.ext.restless import ProcessingException
from flask.ext.login import user_logged_in
# JWT imports
from datetime import timedelta
from flask_jwt import JWT, jwt_required

# Create app
app = Flask(__name__)
app.config['DEBUG'] = True
app.config['SECRET_KEY'] = 'super-secret'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite://'
# expiration delay for tokens (here is one minute)
app.config['JWT_EXPIRATION_DELTA'] = timedelta(seconds=60)

# Create database connection object
db = SQLAlchemy(app)

# creates the JWT Token authentication  ======================================
jwt = JWT(app)
@jwt.authentication_handler
def authenticate(username, password):
    user = user_datastore.find_user(email=username)
    print '%s vs. %s' % (username, user.email)
    if username == user.email and password == user.password:
        return user
    return None

@jwt.user_handler
def load_user(payload):
    user = user_datastore.find_user(id=payload['user_id'])
    return user

# Define Flask-security models ===============================================
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 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))

class User(db.Model, UserMixin):
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True)
    password = db.Column(db.String(255))
    active = db.Column(db.Boolean())
    confirmed_at = db.Column(db.DateTime())
    roles = db.relationship('Role', secondary=roles_users,
                            backref=db.backref('users', lazy='dynamic'))
#Some additional stuff to query over...
class SomeStuff(db.Model):
    __tablename__ = 'somestuff'
    id = db.Column(db.Integer, primary_key=True)
    data1 = db.Column(db.Integer)
    data2 = db.Column(db.String(10))
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
    user = db.relationship(User, lazy='joined', join_depth=1, viewonly=True)
# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
security = Security(app, user_datastore)

# Flask-Restless API ==========================================================
@jwt_required()
def auth_func(**kw):
    return True

apimanager = APIManager(app, flask_sqlalchemy_db=db)

apimanager.create_api(SomeStuff,
    methods=['GET', 'POST', 'DELETE', 'PUT'],
    url_prefix='/api/v1',
    collection_name='free_stuff',
    include_columns=['data1', 'data2', 'user_id'])

apimanager.create_api(SomeStuff,
    methods=['GET', 'POST', 'DELETE', 'PUT'],
    url_prefix='/api/v1',
    preprocessors=dict(GET_SINGLE=[auth_func], GET_MANY=[auth_func]),
    collection_name='protected_stuff',
    include_columns=['data1', 'data2', 'user_id'])

# Create some users to test with
@app.before_first_request
def create_user():
    db.create_all()
    user_datastore.create_user(email='test', password='test')
    user_datastore.create_user(email='test2', password='test2')
    ###
    stuff = SomeStuff(data1=2, data2='toto', user_id=1)
    db.session.add(stuff)
    stuff = SomeStuff(data1=5, data2='titi', user_id=1)
    db.session.add(stuff)
    db.session.commit()

# Views
@app.route('/')
@login_required
def home():
    print(request.headers)
    return render_template('index.html')

@app.route('/logout/')
def log_out():
    logout_user()
    return redirect(request.args.get('next') or '/')

if __name__ == '__main__':
    app.run()

Then, to interact with it via requests:

>>>  import requests, json   
>>>  r=requests.get('http://127.0.0.1:5000/api/v1/free_stuff')  # this is OK   
>>>  print 'status:', r.status_code
status: 200   
>>>  r=requests.get('http://127.0.0.1:5000/api/v1/protected_stuff')  # this should fail   
>>>  print 'status:', r.status_code
status: 401   
>>>  print r.json()
{u'status_code': 401, 
u'description': u'Authorization header was missing', 
u'error':    u'Authorization Required'}   
>>>  # Authenticate and retrieve Token   
>>>  r = requests.post('http://127.0.0.1:5000/auth', 
...:                   data=json.dumps({'username': 'test', 'password': 'test'}),
...:                   headers={'content-type': 'application/json'}
...:                   )   
>>>  print 'status:', r.status_code
status: 200   
>>>  token = r.json()['token']   
>>>  # now we have the token, we can navigate to restricted area:   
>>>  r = requests.get('http://127.0.0.1:5000/api/v1/protected_stuff', 
...:                   headers={'Authorization': 'Bearer %s' % token})   
>>>  print 'status:', r.status_code
status: 200 
Dartboard answered 17/6, 2014 at 8:16 Comment(1)
I extended this example and made a complete sample project using this approach, including stubs for testing and templating: github.com/graup/flask-restless-security I also try to keep all packages up to date.Prager
P
2

Your original query (of using python requests module) helped me get unstuck :) I did not do anything different.

I am not using Flask-Restless (yet)

FWIW - I was able to get the auth token using "just" Flask-Security (i.e. without having to use Flask-jwt)

See here for details

Pictish answered 15/1, 2015 at 7:30 Comment(3)
Good call shutting down that CSRF token! Huge pain! :) From what I've been able to tell, using JWTs might be an alternative (as mentioned in the answer above), but storing server-side authtokens is pretty handy for invalidation purposes.Lepidus
Also, I've been fighting with getting Postman running with Flask-Security as well. Just got it working as expected (sorta). I set my content-type to application/json, make my endpoint /login, and then I enter in a 'raw' data of type JSON, and then I enter my raw data as JSON (email/password). It works, gives me back a JSON token once, but from there on, when I'm logged in, it'll spit back my index.html page. To get the token again, I need to /logout and /login again... Kinda lame. I figure it must be sending cookies somehow, but POSTman was supposed to not use those.Lepidus
Flask-Security provides more than an auth token mechanism (that Flask-JWT provides). So it can be an option for many other things pythonhosted.org/Flask-Security. Can you point me to right urls where I can set the expiration_time for the auth_token? Thanks for your blogpost by the waySussman

© 2022 - 2024 — McMap. All rights reserved.