Source code for arxiv.users.auth.middleware
"""
Middleware for interpreting authn/z information on requestsself.
This module provides :class:`AuthMiddleware`, which unpacks encrypted JSON
Web Tokens provided via the ``Authorization`` header. This is intended to
support requests that have been pre-authorized by the web server using the
authenticator service (see :mod:`authenticator`).
The configuration parameter ``JWT_SECRET`` must be set in the WSGI request
environ (e.g. Apache's SetEnv) or in the runtime environment. This must be
the same secret that was used by the authenticator service to mint the token.
To install the middleware, use the pattern described in
:mod:`arxiv.base.middleware`. For example:
.. code-block:: python
from arxiv.base import Base
from arxiv.base.middleware import wrap
from arxiv.users import auth
def create_web_app() -> Flask:
app = Flask('foo')
Base(app)
auth.Auth(app)
wrap(app, [auth.middleware.AuthMiddleware])
return app
For convenience, this is intended to be used with
:mod:`arxiv.users.auth.decorators`.
"""
import os
from typing import Callable, Iterable, Tuple
import jwt
from werkzeug.exceptions import Unauthorized, InternalServerError
from arxiv.base.middleware import BaseMiddleware
from arxiv.base import logging
from . import tokens
from .exceptions import InvalidToken, ConfigurationError, MissingToken
from .. import domain
logger = logging.getLogger(__name__)
WSGIRequest = Tuple[dict, Callable]
[docs]class AuthMiddleware(BaseMiddleware):
"""
Middleware to handle auth information on requests.
Before the request is handled by the application, the ``Authorization``
header is parsed for an encrypted JWT. If successfully decrypted,
information about the user and their authorization scope is attached
to the request.
This can be accessed in the application via
``flask.request.environ['session']``. If Authorization header was not
included, then that value will be ``None``.
If the JWT could not be decrypted, the value will be an
:class:`Unauthorized` exception instance. We cannot raise the exception
here, because the middleware is executed outside of the Flask application.
It's up to something running inside the application (e.g.
:func:`arxiv.users.auth.decorators.scoped`) to raise the exception.
"""
[docs] def before(self, environ: dict, start_response: Callable) -> WSGIRequest:
"""Decode and unpack the auth token on the request."""
environ['session'] = None # Create the session key, at a minimum.
environ['token'] = None
token = environ.get('HTTP_AUTHORIZATION') # We may not have a token.
if token is None:
logger.info('No auth token')
return environ, start_response
# The token secret should be set in the WSGI environ, or in os.environ.
secret = environ.get('JWT_SECRET', os.environ.get('JWT_SECRET'))
if secret is None:
raise ConfigurationError('Missing decryption token')
try:
# Try to verify the token in the Authorization header, and attach
# the decoded session data to the request.
session: domain.Session = tokens.decode(token, secret)
environ['session'] = session
# Attach the encrypted token so that we can use it in subrequests.
environ['token'] = token
except InvalidToken as e: # Let the application decide what to do.
logger.error(f'Auth token not valid: {token}')
exception = Unauthorized('Invalid auth token') # type: ignore
environ['session'] = exception
except Exception as e:
logger.error(f'Unhandled exception: {e}')
exception = InternalServerError(f'Unhandled: {e}') # type: ignore
environ['session'] = exception
return environ, start_response