如何使用 nginx 的 auth_request 指令和 Flask 应用程序防止看似随机的重新身份验证提示?

tla*_*ake 5 python authentication nginx flask

设置:

该系统应如下所示:

  • 有两个 AWS 负载均衡器,每个都运行一个 Docker 容器。一个是私有负载均衡器,它包含一个任意服务,应该防止未经身份验证的访问;另一个是面向公众的负载均衡器,其中包含身份验证门户。
    • 在 Docker 容器内(在公共负载均衡器上)是一个监听端口 80 的 NginX 服务器,以及一个在端口 8000 上服务的 Flask 应用程序。
    • NginX 使用该auth_request指令向 Flask 应用程序发出子请求(将凭据作为请求的标头传递),期望响应为 200 或 401。
      • Flask 应用程序从请求中提取凭据并检查该用户名是否已经是flask.session. 如果是,则立即返回 200 响应;否则,它会尝试根据外部事实来源进行身份验证。如果成功,则将该用户添加到flask.session该应用程序并返回 200 响应;否则,它将返回 401 响应。
      • 还有一个过期检查;例如,如果用户登录时间超过一个小时,应用程序应将其删除flask.session并返回 401 响应。然后,用户可以提出新的登录请求。
    • 如果响应为 200,则流量将路由到专用负载均衡器。

用户的浏览器应该缓存他们提交的凭据,因此每个新请求都应该能够立即看到用户参与flask.session并避免进行新的身份验证尝试。

问题:

看似随机,在刷新或浏览受保护资源时(成功验证后),有时会出现验证弹出窗口,并要求用户再次验证。他们可以提交,并且在再次提示重新进行身份验证之前将加载单个资源。

例子:

受保护资源是一个静态网站,由一个索引页、一个 CSS 文件和三个图像组成。初始身份验证后,用户多次刷新页面。其中一次,将触发身份验证提示。他们将再次输入他们的凭据,并加载索引页面。他们将再次提示输入 CSS 文件,然后再次提示输入每个图像。

编码

我不确定我需要在这里链接多少东西才能根除问题,所以我将从负责生成auth_request子请求和后续路由的 nginx 文件以及负责生成的两个 python 文件开始身份验证请求和处理会话。

nginx.default

server {
  listen 80;
  server_name _;

  location / {
    auth_request /auth;
    proxy_pass {{resource_endpoint}};
  }

  location /auth {
    proxy_pass {{auth_backend}};
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
  }

  location /logout {
    proxy_pass {{auth_backend}};
    proxy_pass_request_body off;
    proxy_set_header Content-Length "";
    proxy_set_header X-Original-URI $request_uri;
  }
}
Run Code Online (Sandbox Code Playgroud)

app.py

import flask
import auth0
import os


app = flask.Flask(__name__)
app.secret_key = os.getenv("SECRET_KEY", 'sooper seekrit')


@app.route('/auth', methods=['GET'])
@auth0.requires_auth
def login():
    print("logging in")
    resp_text = (
        "Authentication successful."
    )
    return flask.Response(
        response=resp_text,
        status=200,
    )


@app.route('/logout', methods=['GET'])
def logout():
    # To successfully invalidate a user, we must both clear the Flask session
    # as well as direct them to a '401: Unauthorized' route.
    # http://stackoverflow.com/questions/233507/how-to-log-out-user-from-web-site-using-basic-authentication
    print("logging out")
    flask.session.clear()
    return flask.Response(
        response='Logout',
        status=401,
    )


if __name__ == "__main__":
    app.debug = True
    from gevent.wsgi import WSGIServer
    http_server = WSGIServer(('', 8000), app)
    http_server.serve_forever()
Run Code Online (Sandbox Code Playgroud)

auth0.py

import json
import requests
import flask
import datetime
import os
from functools import wraps


def check_auth(username, password):
    if 'username' in flask.session:
        import pdb; pdb.set_trace()
        if 'logged_in' in flask.session:
            now = datetime.datetime.now()
            expiry_window = datetime.timedelta(
                minutes=int(os.getenv('AUTH0_EXPIRY'))
            )

            if flask.session['logged_in'] >= now - expiry_window:
                return True
            else:
                flask.session.pop('username', None)

    data = {
        'client_id': os.getenv("AUTH0_CLIENT_ID"),
        'username': username,
        'password': password,
        'id_token': '',
        'connection': os.getenv("AUTH0_CONNECTION"),
        'grant_type': 'password',
        'scope': 'openid',
        'device': ''
    }

    resp = requests.post(
        url="https://" + os.getenv('AUTH0_DOMAIN') + "/oauth/ro",
        data=json.dumps(data),
        headers={"Content-type": "application/json"}
    )

    if 'error' in json.loads(resp.text):
        return False
    else:
        flask.session['username'] = username
        flask.session['logged_in'] = datetime.datetime.now()
        return True


def authenticate():
    return flask.Response(
        'Could not verify your access level for that URL.\n'
        'You have to login with proper credentials', 401,
        {'WWW-Authenticate': 'Basic realm="Login Required"'},
    )


def requires_auth(f):    
    @wraps(f)
    def decorated(*args, **kwargs):
        auth = flask.request.authorization
        if not auth or not check_auth(auth.username, auth.password):
            return authenticate()
        return f(*args, **kwargs)
    return decorated
Run Code Online (Sandbox Code Playgroud)