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