Giter Site home page Giter Site logo

quart-auth's Introduction

Quart-Auth

Build Status docs pypi python license

Quart-Auth is an extension for Quart to provide for secure cookie authentication (session management). It allows for a session to be logged in, authenticated and logged out.

Usage

To use Quart-Auth with a Quart app you have to create an QuartAuth and initialise it with the application,

app = Quart(__name__)
QuartAuth(app)

or via the factory pattern,

auth_manager = QuartAuth()

def create_app():
    app = Quart(__name__)
    auth_manager.init_app(app)
    return app

In addition you will need to configure Quart-Auth, which defaults to the most secure. At a minimum you will need to set secret key,

app.secret_key = "secret key"  # Do not use this key

which you can generate via,

>>> import secrets
>>> secrets.token_urlsafe(16)

Tou may also need to disable secure cookies to use in development, see configuration below.

With QuartAuth initialised you can use the login_required function to decorate routes that should only be accessed by authenticated users,

from quart_auth import login_required

@app.route("/")
@login_required
async def restricted_route():
    ...

If no user is logged in, an Unauthorized exception is raised. To catch it, install an error handler,

@app.errorhandler(Unauthorized)
async def redirect_to_login(*_: Exception) -> ResponseReturnValue:
    return redirect(url_for("login"))

You can also use the login_user, and logout_user functions to start and end sessions for a specific AuthenticatedUser instance,

from quart_auth import AuthUser, login_user, logout_user

@app.route("/login")
async def login():
    # Check Credentials here, e.g. username & password.
    ...
    # We'll assume the user has an identifying ID equal to 2
    login_user(AuthUser(2))
    ...

@app.route("/logout")
async def logout():
    logout_user()
    ...

The user (authenticated or not) is available via the global current_user including within templates,

from quart import render_template_string
from quart_auth import current_user

@app.route("/")
async def user():
    return await render_template_string("{{ current_user.is_authenticated }}")

Contributing

Quart-Auth is developed on GitHub. You are very welcome to open issues or propose pull requests.

Testing

The best way to test Quart-Auth is with Tox,

$ pip install tox
$ tox

this will check the code style and run the tests.

Help

The Quart-Auth documentation is the best places to start, after that try searching stack overflow or ask for help on gitter. If you still can't find an answer please open an issue.

quart-auth's People

Contributors

chemtrails avatar cr0hn avatar enchant97 avatar heinekayn avatar mcsinyx avatar perobertson avatar pgjones avatar pohlt avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

quart-auth's Issues

FeatReq: basic_auth_required accepting callable

Currently basic_auth_required is a bit simplistic and "dangerous" as the credentials are stored in the code. I suggest replacing the args of the wrapper by a single callable function which:

  1. could do the same as currently, that is, comparing user and password values
  2. address other use cases like validating user/password in an external API for instance

The latter is what I am after as "our" quart API acts as a middleman to an external API. We pass (aiohttp.ClientSession) user and password to its login page and if we get a token back (sessionId), it's validated. We then use that cookie in further calls.

Configuration does not seem to be respected

I have the following environment variables set:

QUART_DEBUG=True
QUART_SECRET_KEY=<redacted>
QUART_AUTH_COOKIE_NAME=VMS
QUART_AUTH_COOKIE_SAMESITE=Lax
QUART_AUTH_COOKIE_SECURE=False

and the following snippet of code to initialize the app:

app = Quart(__name__)
app.config.from_prefixed_env()

auth_manager = AuthManager(app)

logger.info(event="Configuration at startup", **app.config)

So when the app starts out it is currently dumping out the app config, which shows the following (unrelated things removed):

2023-02-17T22:34:55.563625Z [info ] Configuration at startup AUTH_COOKIE_NAME=VMS AUTH_COOKIE_SAMESITE=Lax AUTH_COOKIE_SECURE=False

So at this point it seems to me like things would work. I then go to the browser and hit my login page which POSTs to a route that calls login_user. That generally seems to work ok as well, but what ends up in the response headers set-cookie seems like it did not pay attention to the configuration.

I end up with QUART_AUTH=<redacted>; Secure; HttpOnly; Path=/; SameSite=Lax which means it didn't take the name or the secure value. Because I'm currently working on this locally I need at least the SECURE=False part, so Chrome of course rejects this since it's coming over HTTP.

I want to believe I'm just misusing something, but it looks straightforward enough and I don't see anything else I should be doing to get the config recognized. Is something not working right or am I doing it wrong? I can't really tell.

I don't imagine this is related, but for whatever it's worth Quart is running out of http://0.0.0.0:8081 (via Hypercorn) and the frontend is http://0.0.0.0:8080, and there is an Nginx reverse proxy redirecting 8080/api (the root of all Quart stuff) to 8081/api. I don't think Quart-Auth is even looking at that, but figured I'd mention it.

Error trying to login user

Hi! I'm running quart 0.18.0 and quart-auth 0.7.0 and I'm running into an error when running the app. I've manually modified flask-sqlalchemy and quart-csrf to the new context tracking system, basically replacing the _ctx stuff with g, and the safe_str_cmp modification in werkzeug. Is there something I'm missing? I'm doing the login like this:

user = User.query.filter_by(email=form.email.data).first()
if user and bcrypt.check_password_hash(user.password, form.password.data):
    login_user(AuthUser(user.id), remember=form.remember.data)

and I have the following in my run.py (using app factory pattern):

app = create_app()
app.auth_manager.user_class = User   # this "User" my user class from models.py


@app.errorhandler(Unauthorized)
async def redirect_to_login(*_: Exception):
    return redirect(url_for('users.login'))

The error:

Traceback (most recent call last):
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1652, in full_dispatch_request
    result = await self.dispatch_request(request_context)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1691, in dispatch_request
    self.raise_routing_exception(request_)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1171, in raise_routing_exception
    raise request.routing_exception
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\ctx.py", line 62, in match_request
    ) = self.url_adapter.match(  # type: ignore
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\werkzeug\routing\map.py", line 624, in match
    raise NotFound() from None
werkzeug.exceptions.NotFound: 404 Not Found: The requested URL was not found on the server. If you entered the URL manually please check your spelling and try again.

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1629, in handle_request
    return await self.full_dispatch_request(request_context)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\flask_patch\app.py", line 28, in new_full_dispatch_request
    return await old_full_dispatch_request(self, request_context)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1654, in full_dispatch_request
    result = await self.handle_user_exception(error)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1095, in handle_user_exception
    return await self.handle_http_exception(error)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1073, in handle_http_exception
    return await self.ensure_async(handler)(error)
  File "C:\Users\gs833787\Documents\auxilios\auxilios\errors\handlers.py", line 19, in error_404
    return await render_template('errors/404.html'), 404
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\templating.py", line 101, in render_template
    await current_app.update_template_context(context)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 494, in update_template_context
    extra_context.update(await self.ensure_async(processor)())
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\flask_patch\app.py", line 44, in _wrapper
    result = func(*args, **kwargs)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart_auth\__init__.py", line 350, in _template_context
    return {"current_user": _load_user()}
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart_auth\__init__.py", line 321, in _load_user
    user = current_app.auth_manager.resolve_user()  # type: ignore
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart_auth\__init__.py", line 134, in resolve_user
    return self.user_class(auth_id)
TypeError: __init__() takes 1 positional argument but 2 were given

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\hypercorn\asyncio\task_group.py", line 21, in _handle
    await invoke_asgi(app, scope, receive, send)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\hypercorn\utils.py", line 247, in invoke_asgi
    await app(scope, receive, send)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1881, in __call__
    await self.asgi_app(scope, receive, send)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1907, in asgi_app
    await asgi_handler(receive, send)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\asgi.py", line 51, in __call__
    _raise_exceptions(done)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\asgi.py", line 353, in _raise_exceptions
    raise task.exception()
  File "C:\Users\gs833787\AppData\Local\Programs\Python\Python310\lib\asyncio\tasks.py", line 232, in __step
    result = coro.send(None)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\asgi.py", line 90, in handle_request
    response = await self.app.handle_request(request)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1633, in handle_request
    return await self.handle_exception(error)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 1120, in handle_exception
    response = await self.ensure_async(handler)(internal_server_error)
  File "C:\Users\gs833787\Documents\auxilios\auxilios\errors\handlers.py", line 30, in error_500
    return await render_template('errors/500.html'), 500
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\templating.py", line 101, in render_template
    await current_app.update_template_context(context)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\app.py", line 494, in update_template_context
    extra_context.update(await self.ensure_async(processor)())
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart\flask_patch\app.py", line 44, in _wrapper
    result = func(*args, **kwargs)
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart_auth\__init__.py", line 350, in _template_context
    return {"current_user": _load_user()}
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart_auth\__init__.py", line 321, in _load_user
    user = current_app.auth_manager.resolve_user()  # type: ignore
  File "C:\Users\gs833787\Documents\auxilios\qvenv\lib\site-packages\quart_auth\__init__.py", line 134, in resolve_user
    return self.user_class(auth_id)
TypeError: __init__() takes 1 positional argument but 2 were given

Thanks in advance!

AttributeError: 'Quart' object has no attribute 'auth_manager' when running app.

Hi! I'm getting this issue when trying to run my app. It happens with Quart-Auth 0.8.0, but not 0.7.0.

I'm using the app factory pattern, like in the docs, but the offending line is in my run.py file, where I do:

app.auth_manager.user_class = AuthUser

AuthUser is a class imported from my models.py file:

class AuthUser(AU):
    def __init__(self, auth_id):
        super().__init__(auth_id)
    
    @property
    def _user(self):
        user = User.query.get(self.auth_id)
        return user

Has anything changed in the last version?

QUART_AUTH cookie missing.

When running the following locally (on Windows 10) with quart 0.18.4, quart-wtforms 1.0.0, and quart-auth 0.9.0, a QUART_AUTH cookie appears, but when running it remotely (on CentOS), the QUART_AUTH cookie isn't populated in the browser. The form loads, successfully submits, and after the line login_user(AuthUser(UserType.viewer.value)), I'd expect to have the cookie. I've swapped return redirect(url_for('home')) for return 123 and the same thing happens -- cookie is populated when running quart locally, but not remotely. I am not using a reverse-proxy or anything. Just hypercorn (hypercorn main:app -b 0.0.0.0:8080 -w 2) and the dev server (py main.py).

The firewall is open on port 8080, and the page loads from the remote server. I've also set this up with Docker, and the same issue occurs.

@app.route("/login", methods=['GET', 'POST'])
async def login():
    form = await LoginForm.create_form()
    if await form.validate_on_submit():
        if form.username.data == UserType.viewer.value and compare_digest(form.password.data, os.getenv('PASSWORD_VIEWER')):
            login_user(AuthUser(UserType.viewer.value))
            return redirect(url_for('home'))
        else:
            form.form_errors.append('Incorrect credentials')
    return await render_template('login.html', form=form)

Any ideas as to why this is happening?

How to create a user class with AuthUser and SQLAlchemy?

I'm using a MySQL database that increment user automatically.

Nevertheless, quart_auth requires an init function and an auth_id that aren't buit to do automated incrementation with SQL Alchemy.

Here is the piece of code I have:

class users(AuthUser, db.Model):
    
    def __init__(self, id):
        super().__init__(id)
        self._auth_id= id
        self._resolved = False
        self.email = None
    
    async def _resolve(self):
        if not self._resolved:
            self.email = await db.fetch_email(self.id)
            self._resolved = True
            
    __tablename__ = 'users'

    async def get_id(self):
    # this matches what user_loader needs to uniquely load a user
        return self.id
    
    #id = db.Column('id', db.Integer, primary_key = True)
    id = db.Column(db.Integer, primary_key=True, unique=True , autoincrement=True)

Unfortunatelly, I get JSON encoding error, probably due to the AuthUser structure.

How can I have automated user_id incrementation with SQLALchemy and use it with AuthUser correctly?

Many thanks

Nginx reverse proxy error

In most cases, it runs well, but when I access it using the regional network IP, it seems to be unable to log in properly
Of course, it does not include the local 127.0.01
No matter how I modify it request.remote_addrwill always be 127.0.0.1
nginx:

 location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Range $http_range;
    proxy_set_header If-Range $http_if_range;
    proxy_redirect off;
    proxy_pass http://127.0.0.1:5000;

  }

py:

auth_manager = QuartAuth()
def create_app():
    app = Quart(__name__, template_folder='auth')
    auth_manager.init_app(app)
    return app
app = create_app()

@app.before_serving
async def setup():
    global app,event,domains
    event = asyncio.Event()
    app = cors(app, allow_origin=domains)
    Session(app)

    await registerUser()

@app.route('/login', methods=['GET', 'POST'])
async def login():
    print(request.remote_addr)
    userU = await request.get_json()
    login_user(AuthUser(userU['id']))

@app.route('/check', methods=['GET', 'POST'])
async def check():
    print(await current_user.is_authenticated)

In addition, after testing, it has been found that @app. websocket cannot support the wss protocol and can perform ws connections normally. I am now using websockets as the wss router

Hope to add traffic forwarding function in the future

QuartAuth().user_class as init argument

I find myself writing

auth_manager = QuartAuth(salt = my_salt)
auth_manager.user_class = my_user_class
auth_manager.init_app(app)

where I would prefer to write

QuartAuth(app, salt = my_salt, user_class=my_user_class)

If you are fine with that, I would file a PR.
-> would you mind switching to dataclasses to get rid of the boilerplate QuartAuth.init() method code

Extension init types do not match docs or code

When specifying either cookie_http_only or cookie_secure in the init of QuartAuth as below:

from quart_auth import QuartAuth

quart_auth = QuartAuth(
    cookie_http_only=True,
    cookie_secure=True,
)

mypy will raise errors:

mypy scratch.py
scratch.py:4: error: Argument "cookie_http_only" to "QuartAuth" has incompatible type "bool"; expected "str | None"  [arg-type]
scratch.py:5: error: Argument "cookie_secure" to "QuartAuth" has incompatible type "bool"; expected "str | None"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

LoadUser with quart_auth

How can I load user with quart_auth?

This code:

auth = AuthManager()
auth.init_app(app)

auth.user_class = User
login_serializer = URLSafeTimedSerializer(app.secret_key)

@auth.user_loader
async def load_user(user_id):
    user_data = await User.query.where(User.id == user_id).gino.first()
    if user_data:
        return User(user_data.id)

Returns:
AttributeError: 'AuthManager' object has no attribute 'user_loader'

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.