Python Pyramid AuthTktAuthenticationPolicy auto-expired cookies based on inactivity issue - python

Someone please help. I'm new to pyramid. So according to the pyramid docs about AuthTktAuthenticationPolicy
developers says that we can set up auto-expired cookies based on inactivity - awesome.
But it doesn't work for me, so it says that you need to use reissue_time parameter and set it in pair with timeout. If timeout is for example set to 1200, so then reissue_time should be timout / 10 = 120, 2 minutes.
As i understood the point is to make auto logout after 2 minutes when a user is inactive. But when i try to reload the page token or session is not expiring.
#myapp/__init__.py
from pyramid.config import Configurator
from sqlalchemy import engine_from_config
from myapp.models import initialize_sql
from myapp import views
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
def main(global_config, **settings):
""" This function returns a Pyramid WSGI application.
"""
authentication_policy = AuthTktAuthenticationPolicy(secret='secret', hashalg='sha512', timeout=1200, reissue_time=120)
authorization_policy = ACLAuthorizationPolicy()
config = Configurator(settings=settings, root_factory='myapp.factory.RootFactory', )
config.include('pyramid_jinja2')
#config.add_jinja2_renderer('.html', settings_prefix='jinja2.')
config.add_static_view('static', 'static', cache_max_age=3600)
config.scan('myapp.models')
config.set_authentication_policy(authentication_policy)
config.set_authorization_policy(authorization_policy)
engine = engine_from_config(settings, 'sqlalchemy.')
initialize_sql(engine)
views.routes(config)
return config.make_wsgi_app()
#factory/rootfactory.py
from pyramid.security import (
Allow,
Authenticated,
)
class RootFactory(object):
'''Root factory class. Acl auth system'''
__acl__ = [(Allow, Authenticated, 'view'),]
def __init__(self, request):
pass
#views/__init__.py
def routes(config):
config.add_route('home', '/')
config.add_route('login', '/login')
config.add_route('logout', '/logout')
config.scan('myapp')
#views/auth.py
from pyramid.response import Response
from pyramid.security import remember, forget, authenticated_userid
from pyramid.httpexceptions import HTTPFound, HTTPForbidden
from pyramid.view import view_config, forbidden_view_config
from sqlalchemy.exc import DBAPIError
from ..models import DBSession
from ..models.user import User
from ..forms.login import LoginForm
#view_config(route_name='login', renderer='myapp:templates/login.jinja2')
#forbidden_view_config(renderer='myapp:templates/login.jinja2')
def login_view(request):
if request.authenticated_userid:
return HTTPFound(location=request.application_url)
next = request.params.get('next') or request.route_url('home')
login_form = LoginForm(request.POST)
if request.method == 'POST' and login_form.validate():
login = request.params.get('login', '')
password = request.params.get('password', '')
user = User.by_login(login)
if user and user.validate_password(password):
headers = remember(request, login)
return HTTPFound(location=next, headers=headers)
return {'form': login_form}
#view_config(route_name='logout', renderer='myapp:templates/logout.jinja2')
def logout_view(request):
headers = forget(request)
loc = request.route_url('login')
return HTTPFound(location=loc, headers=headers)
#views/home.py
from pyramid.security import authenticated_userid
from pyramid.httpexceptions import HTTPForbidden
from pyramid.response import Response
from pyramid.view import view_config
from sqlalchemy.exc import DBAPIError
from ..models import DBSession
from ..models.user import User
#view_config(route_name='home', permission='view', renderer='myapp:templates/base.jinja2')
def home_view(request):
return Response('Ok')

Your session is logged out after 1200 seconds, or 20 minutes. You'll be issued a new cookie at most every 120 seconds, or 2 minutes.
In other words, the reissue_time only limits how often a new token is issued. If a user contacts the server very frequently, it would be costly to keep issuing new tokens. Instead, a new token is only issued if the old token is at least reissue_time seconds old. A new token will be fresh for another timeout seconds, so each re-issue extends the session.
Only if a user contacts the server with a cookie issued more than timeout seconds old is the session no longer valid.

Related

flask_jwt_extended.exceptions.NoAuthorizationError: Missing Authorization Header - Authorization not working

I am using flask_jwt_extended for jwt authentication in my flask web application. After the user enters email and password, I make a token using create_access_token and then redirect to another link which can only be accessed with #jwt_required.
app.py file. Notice if the way of importing jwt from user.py file like this is correct.
from flask import Flask
from flask_restful import Api
from resources.organization import OrganizationResourceList, OrganizationResource
from resources.user import LoginResource, user_list, User, jwt
app = Flask(__name__)
app.config['SECRET_KEY'] = '_rV;He_7Bz8TVvA'
app.config['JWT_TOKEN_LOCATION'] = ['headers']
jwt.init_app(app)
api = Api(app)
user_list.append(User(name="Admin User", email="admin#test.com", password="12345", photo="", user_type="host"))
# Authorization
api.add_resource(LoginResource, '/login')
# Organization
api.add_resource(OrganizationResourceList, '/organizations')
if __name__ == '__main__':
app.run(port=5000, debug=True)
user.py file containing LoginResource This is where I am creating token.
from flask import request, Response, render_template, redirect
from flask_restful import Resource
from models.user import User, user_list
from passlib.hash import sha256_crypt
from flask_jwt_extended import create_access_token, create_refresh_token, JWTManager
jwt = JWTManager()
class LoginResource(Resource):
def post(self):
req = request.form
email = req.get("email")
user = [x for x in user_list if x.email == email]
if user:
user = user[0]
password = sha256_crypt.verify(req.get("password"), user.password)
if user and password:
access_token = create_access_token(identity=user.id)
refresh_token = create_refresh_token(user.id)
redir = redirect('/organizations')
redir.headers['Authorization'] = "Bearer %s" % access_token
return redir
return redirect("/login")
Interestingly, when I debug the app, I see the headers of redirect as shown.
organization.py file containing OrganizationResourceList class
from flask import request, Response, render_template
from flask_restful import Resource
from models.organization import Organization, organization_list
from flask_jwt_extended import jwt_required, get_jwt_identity
class OrganizationResourceList(Resource):
#jwt_required()
def get(self):
current_user = get_jwt_identity()
sample_org = Organization(
name='Bin Yousef',
description='My main working company in Qatar',
photo=''
)
data = []
for organization in organization_list:
data.append(organization.data)
return Response(response=render_template('organization/index.html', organizations=data))
After hours of searching, I am still not able to get rid of the error :( Please help

Microsoft Authentication - Python Flask msal Example App Ported to FastAPI

I don't do much web work but I recently began using FastAPI and am building an MVC app with jinja2 templating that uses PowerBI embedded capacity to serve multiple embedded analytics in app owns data arrangement. All of this works beautifully. However, I'm wanting to add further modules and I'd like to use the msal package to do user authentication by routing a user to the Microsoft login page, letting them sign in against a multi-tenant app service I set up in Azure, and then redirecting back to my page via redirect URI, grabbing the token, and progressing with authorization. Microsoft saved a great example our here for doing this in Flask. However, I am having fits porting the example to FastAPI.
I can get the user to the login screen and log in but I am having no luck capturing the token at my call back URI - it's appropriately routing but I am unable to capture the token from the response.
Has anyone (or can anyone) taken that super simple Flask example and ported it to FastAPI? Everything I find online for FAPI is back-end token-bearer headers for APIs - not meant for MVC apps.
Here's my current code. Messy because I have "tests" built in.
import msal
import requests
from fastapi import APIRouter, Request, Response
from fastapi.responses import RedirectResponse
from starlette.templating import Jinja2Templates
from config import get_settings
settings = get_settings()
router = APIRouter()
templates = Jinja2Templates('templates')
# Works
#router.get('/login', include_in_schema=False)
async def login(request: Request):
request.session['flow'] = _build_auth_code_flow(scopes=settings.AUTH_SCOPE)
login_url = request.session['flow']['auth_uri']
return templates.TemplateResponse('error.html', {'request': request, 'message': login_url})
# DOES NOT WORK - Pretty sure error is in here --------------------
#router.get('/getAToken', response_class=Response, include_in_schema=False)
async def authorize(request: Request):
try:
cache = _load_cache(request)
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
request.session.get('flow'), request.session
)
if 'error' in result:
return templates.TemplateResponse('error.html', {'request': request, 'message': result})
request.session['user'] = result.get('id_token_claims')
_save_cache(cache)
except Exception as error:
return templates.TemplateResponse('error.html', {'request': request, 'message': f'{error}: {str(request.query_params)}'})
return templates.TemplateResponse('error.html', {'request': request, 'message': result})
# -----------------------------------------------------
def _load_cache(request: Request):
cache = msal.SerializableTokenCache()
if request.session.get("token_cache"):
cache.deserialize(request.session["token_cache"])
return cache
def _save_cache(request: Request, cache):
if cache.has_state_changed:
request.session["token_cache"] = cache.serialize()
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
settings.CLIENT_ID,
authority=authority or settings.AUTH_AUTHORITY,
client_credential=settings.CLIENT_SECRET,
token_cache=cache
)
def _build_auth_code_flow(authority=None, scopes=None):
return _build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [],
redirect_uri=settings.AUTH_REDIRECT)
def _get_token_from_cache(scope=None):
cache = _load_cache() # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
_save_cache(cache)
return result
Any help is GREATLY appreciated. Happy to answer any questions. Thank you.
This is because FastAPI session variables are stored client-side as a cookie, which has a limit of 4096 bytes of data. The data being stored from the redirect url is pushes the cookie size over this limit and results in the data not being stored. Starlette-session is an alternative SessionMiddleware that stores variables server-side, eliminating cookie limit. Below is a basic (but messy) implementation:
from fastapi import FastAPI
from fastapi.templating import Jinja2Templates
from starlette.requests import Request
from starlette.responses import RedirectResponse
from starlette_session import SessionMiddleware
from starlette_session.backends import BackendType
from redis import Redis
import uvicorn
import functools
import msal
app_client_id = "sample_msal_client_id"
app_client_secret = "sample_msal_client_secret"
tenant_id = "sample_msal_tenant_id"
app = FastAPI()
redis_client = Redis(host="localhost", port=6379)
app.add_middleware(
SessionMiddleware,
secret_key="SECURE_SECRET_KEY",
cookie_name="auth_cookie",
backend_type=BackendType.redis,
backend_client=redis_client,
)
templates = Jinja2Templates(directory="templates")
default_scope = ["https://graph.microsoft.com/.default"]
token_cache_key = "token_cache"
# Private Functions - Start
def _load_cache(session):
cache = msal.SerializableTokenCache()
if session.get(token_cache_key):
cache.deserialize(session[token_cache_key])
return cache
def _save_cache(cache,session):
if cache.has_state_changed:
session[token_cache_key] = cache.serialize()
def _build_msal_app(cache=None):
return msal.ConfidentialClientApplication(
app_client_id,
client_credential=app_client_secret,
authority=f"https://login.microsoftonline.com/{tenant_id}",
token_cache=cache
)
def _build_auth_code_flow(request):
return _build_msal_app().initiate_auth_code_flow(
default_scope, #Scopes
redirect_uri=request.url_for("callback") #Redirect URI
)
def _get_token_from_cache(session):
cache = _load_cache(session) # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(default_scope, account=accounts[0])
_save_cache(cache,session)
return result
# Private Functions - End
# Custom Decorators - Start
def authenticated_endpoint(func):
#functools.wraps(func)
def is_authenticated(*args,**kwargs):
try:
request = kwargs["request"]
token = _get_token_from_cache(request.session)
if not token:
return RedirectResponse(request.url_for("login"))
return func(*args,**kwargs)
except:
return RedirectResponse(request.url_for("login"))
return is_authenticated
# Custom Decorators - End
# Endpoints - Start
#app.get("/")
#authenticated_endpoint
def index(request:Request):
return {
"result": "good"
}
#app.get("/login")
def login(request:Request):
return templates.TemplateResponse("login.html",{
"version": msal.__version__,
'request': request,
"config": {
"B2C_RESET_PASSWORD_AUTHORITY": False
}
})
#app.get("/oauth/redirect")
def get_redirect_url(request:Request):
request.session["flow"] = _build_auth_code_flow(request)
return RedirectResponse(request.session["flow"]["auth_uri"])
#app.get("/callback")
async def callback(request:Request):
cache = _load_cache(request.session)
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(request.session.get("flow", {}), dict(request.query_params))
if "error" in result:
return templates.TemplateResponse("auth_error.html",{
"result": result,
'request': request
})
request.session["user"] = result.get("id_token_claims")
request.session[token_cache_key] = cache.serialize()
return RedirectResponse(request.url_for("index"))
# Endpoints - End
if __name__ == "__main__":
uvicorn.run("main:app",host='0.0.0.0', port=4557,reload=True)`

Flask Restul Basic Authentication

I have following code for flask restful basic authentication
from flask import Flask
from flask_restful import Resource, Api
from flask_httpauth import HTTPBasicAuth
app = Flask(__name__)
api = Api(app, prefix="/api/v1")
auth = HTTPBasicAuth()
USER_DATA = {
"admin": "SuperSecretPwd"
}
#auth.verify_password
def verify(username, password):
if not (username and password):
return False
return USER_DATA.get(username) == password
class PrivateResource(Resource):
#auth.login_required
def get(self):
return {"meaning_of_life": 42}
api.add_resource(PrivateResource, '/private')
if __name__ == '__main__':
app.run(debug=True)
But if My resource class PrivateResource is in separate file, how can I use #auth.login_required. I don't want to import app.py in every resource file.
You can structure your project like this:
In app.py
from flask import Flask
from flask_restful import Api
from my_resource import PrivateResource
app = Flask(__name__)
api = Api(app, prefix="/api/v1")
# add all resources here
api.add_resource(PrivateResource, '/private')
if __name__ == '__main__':
app.run(debug=True)
Handle authentication in authentication.py
from flask_httpauth import HTTPBasicAuth
auth = HTTPBasicAuth()
USER_DATA = {
"admin": "SuperSecretPwd"
}
#auth.verify_password
def verify(username, password):
if not (username and password):
return False
return USER_DATA.get(username) == password
and your resources in separate files like my_resource.py
from flask_restful import Resource
from authentication import auth
class PrivateResource(Resource):
#auth.login_required
def get(self):
return {"meaning_of_life": 42}
If every resource requires authentication, you can add the decorator automatically to every resource, see the decorators parameters in the third line
from flask_httpauth import HTTPBasicAuth
from werkzeug.security import check_password_hash
from flask import Flask
from flask_restful import Api
application: Flask = Flask(__name__)
auth = HTTPBasicAuth()
api: Api = Api(app=application, serve_challenge_on_401=True, decorators=[auth.login_required])
PW_HASH: str = ..
#auth.verify_password
def verify_password(username: str, password: str):
if username == 'foo' and check_password_hash(PW_HASH, password):
return username
You always need to import the auth instance from app.py, because you are creating a new instance of Auth by calling auth = HTTPBasicAuth(). And you are registering your validation method on that specific instance, see #auth.verify_password. So only this instance is able to handle your authentication. So you need to import it every time you want to use it. But you don't need to import the whole module. Its enough to import only that instance: from app.py import auth

Flask login can't use custom view

In my app i'm using Flask login application. But i can't configure it to use custom view.
My project structure:
ProjectName
-app
--auth
----models.py
----views.py
app_settings.py
config.py
extensions.py
app_settings.py:
# -*- coding: utf-8 -*-
import flask
from werkzeug.routing import BaseConverter
from app.auth.views import auth
from extension import db, login_manager
from config import BaseConfig
from app.auth.models import User, Role, load_user
# For import *
__all__ = ['create_app']
DEFAULT_BLUEPRINTS = (
main,
auth
)
def create_app(config=None, app_name=None, blueprints=None):
if blueprints is None:
blueprints = DEFAULT_BLUEPRINTS
app = flask.Flask('app', instance_relative_config=True)
app.config.from_object(BaseConfig)
configure_blueprints(app, blueprints)
configure_extensions(app)
configure_hook(app)
configure_error_handlers(app)
return app
def configure_extensions(app):
db.init_app(app)
# Flask-Login
# https://flask-login.readthedocs.org/en/latest/
login_manager.init_app(app)
login_manager.user_loader(load_user)
login_manager.login_view = "/auth/login"
#app.teardown_request
def shutdown_session(exception=None):
from extension import db
db.session.remove()
I tried: login_manager.login_view = "auth.login" too. But nothing helped. It continue using standard login view and template.
auth/views.py
from flask.ext.login import login_user
auth = flask.Blueprint('auth', __name__, url_prefix='/auth')
#auth.route('/login', methods=['GET', 'POST'])
def login():
# Here we use a class of some kind to represent and validate our
# client-side form data. For example, WTForms is a library that will
# handle this for us, and we use a custom LoginForm to validate.
form = LoginForm(flask.request.form)
if form.validate_on_submit():
# Login and validate the user.
# user should be an instance of your `User` class
user = User.query.filter_by(email=form.email.data).first()
login_user(user)
flask.flash('Logged in successfully.')
next = flask.request.args.get('next')
# next_is_valid should check if the user has valid
# permission to access the `next` url
return flask.redirect(flask.request.args.get('next', '/'))
return flask.render_template('auth/login.html', form=form)
try login_manager.login_view = "login" You are going to want to point it to the URL of the login view. In this case that is defined by #auth.route('/login')
Only the endpoint login should be required.

How to share sessions between modules on a Google App Engine Python application?

I'm trying to make a basic app on Google App Engine with two modules using Google App Engine Modules(https://developers.google.com/appengine/docs/python/modules/) and They share session information between the modules:
Modules:
Module 1 - Login Page: a basic page with a login form where if is a valid user I create a session and then the user is redirected to the dashboard page(Module 2)
Module 2 - Dashboard Page: a page that show a message if the module can read the data in the session variable
The problem is that in the dashboard module the session data created in the Login Page(module 1) does not exist.
Is it possible access the sessions data between two o more modules in Google App Engine?
Source:
baseHandler.py
import webapp2
from webapp2_extras import sessions
class BaseHandler(webapp2.RequestHandler):
def render_template(self, view_filename, params=None):
params = {}
path = os.path.join(os.path.dirname(__file__), 'views', view_filename)
self.response.out.write(template.render(path, params))
def display_message(self, message):
"""Utility function to display a template with a simple message."""
params = {}
self.render_template('message.html', params)
def dispatch(self):
# Get a session store for this request.
self.session_store = sessions.get_store(request=self.request)
try:
# Dispatch the request.
webapp2.RequestHandler.dispatch(self)
finally:
# Save all sessions.
self.session_store.save_sessions(self.response)
#webapp2.cached_property
def session(self):
# Returns a session using the default cookie key.
return self.session_store.get_session()
Module 1(Signin) main.py
from google.appengine.ext import ndb
import webapp2
from webapp2_extras.security import hash_password
import logging
import os
import sys
import jinja2
from src.basehandler import BaseHandler
from src.user import User
JINJA_ENVIRONMENT = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
extensions=['jinja2.ext.autoescape']
)
class MainPage(BaseHandler):
def get(self):
template_values = {}
template = JINJA_ENVIRONMENT.get_template('templates/index.html')
self.response.write( template.render( template_values ) )
def post(self):
email_address = self.request.get('input-email')
password = self.request.get('input-password')
password = hash_password( password, 'sha1', salt=None, pepper=None )
qry = User.query(
ndb.AND(User.email_address == email_address,
User.password == password
)
).fetch()
if qry:
self.session['loggedin'] = True
self.redirect('http://dashboard.myURL.appspot.com/')
else:
self.redirect('/?error=invaliduser')
config = {}
config['webapp2_extras.sessions'] = {
'secret_key': 'my-super-secret-key',
}
app = webapp2.WSGIApplication([
('/', MainPage)
], debug=True, config=config )
Module 2(Dashboard) main.py
from google.appengine.ext import ndb
import webapp2
import logging
import os
import sys
import jinja2
from src.basehandler import BaseHandler
from src.user import User
JINJA_ENVIRONMENT = jinja2.Environment(
loader=jinja2.FileSystemLoader(os.path.dirname(__file__)),
extensions=['jinja2.ext.autoescape']
)
class Main(BaseHandler):
def get(self):
msg = ''
if not self.session.get('loggedin'):
msg = 'There is not session'
else:
msg = 'There is a session'
template_values = { 'msg': msg }
template = JINJA_ENVIRONMENT.get_template('templates/index.html')
self.response.write( template.render( template_values ) )
config = {}
config['webapp2_extras.sessions'] = {
'secret_key': 'my-super-secret-key',
}
app = webapp2.WSGIApplication([
('/', Main)
], debug=True, config=config )
Any help is welcome, sorry if something is misspelled
By default webapp2_extra.sessions uses cooki-based sessions. These will be tied to a specific domain. Your modules are probably at module1.yourapp.appspot.com and module2.yourapp.appspot.com (a guess). The second module won't be able to see the cookies set by the first module.
In your config try setting the domain for the cookie.
config['webapp2_extras.sessions'] = {
'secret_key': 'my-super-secret-key',
cookie_args': {
'domain' : "yourapp.appspot.com"
}
The docs say:
- domain: Domain of the cookie. To work accross subdomains the
domain must be set to the main domain with a preceding dot, e.g.,
cookies set for `.mydomain.org` will work in `foo.mydomain.org` and
`bar.mydomain.org`. Default is None, which means that cookies will
only work for the current subdomain.
from: https://code.google.com/p/webapp-improved/source/browse/webapp2_extras/sessions.py
Or you could also use one of the other backends like memcache or datastore. This is preferred if your sessions contain sensitive info.

Categories