记一次authentik配置debug的心路历程

一切开始之前

随着队伍内部的服务越来越多(对外:Outline, wiki, hedgedoc, 以及之后可能会有的网盘服务;对内:PVE, Portainer, …),我们越来越觉得需要一个SSO服务,使用户使用一个账户就可以访问这些服务。同时,由于我们的用户包含校内和校外的同学,也需要进一步做权限管理。

在初步的调研中我们初步确定keycloakauthentik比较满足我们的需求。但是我们的需求中有一点:Outline不支持外部的权限管理,只支持外部的认证服务,而我们不希望允许任何未授权的用户访问Outline服务,这意味这我们必须在认证(authentication)步骤加入鉴权。而keycloak在这方面有着严重的问题,意味着我们必须ban掉一切第三方身份认证服务。因此我们只能选择authentik。

我们同时允许用户在authentik注册本地的账号,和通过微软等第三方idp进行账号注册和登录。

起因

在服务部署后,我们发现用户在使用第三方idp登录时会遇到偶发的认证失败问题,报错Flow does not apply to current user,并且难以复现

而日志并没有提供太多有效的信息,导致我们难以定位问题所在,暂且考虑是哪里发生了竞争导致

问题定位

根据配置时的经验,Flow does not apply to current user应该是因为有用户进入了一个flow,但是却被flow绑定的policy判断没有权限进入flow而产生的报错提示。由此我们定位到了default-source-authentication flow的default-source-authentication-if-sso policy

这个policy只是简单地判断用户是否来自外部idp,应该没有理由对正常认证流程的用户执行失败,由此我们考虑问题可能出在这里。我们打开了这个policy的记录执行日志选项,查看发生错误时的context,然后有了意外收获

这里是一个发生问题的日志,红笔涂抹处为用户名。我们可以看到用户最初登录时匿名用户,此时policy结果为true,然后登录成功,然而马上又发生了一次policy执行结果为false,且此时的用户为匿名用户。此时用户已经正常登录成功了才对,怎么可能为匿名用户呢?

在客户端复现问题后,我们查看了相关的网络日志

用户在请求认证相关endpoint时,几乎同时请求了/api/v3/core/brands/current//api/v3/root/config两个endpoint,用户带着session请求,然而后端竟然返回了Set-Cookie: authentik_session="",导致虽然认证endpoint返回了已认证的session,但此时浏览器端的session却被清空,再次请求认证endpoint时仍为未登录状态。

此时用户不是从外部idp返回,所以导致了policy执行失败。但是后端怎么会清空session呢?

我们查看了authentik的源码,发现只有一处可能导致返回这样的Set-Cookie头,即SessionMiddlewareprocess_response处调用了delete_cookie方法

这也就意味着用户的session在实际的SessionStore中实际不存在。但考虑到它曾经还是合法的,只有一种可能:用户的Session被作废了

authentik的用户登录使用的是django.contrib.auth的login,而阅读源码我们发现在认证成功后,会产生一个新的session_key,而作废掉原先的session_key

因此,用户带着旧的session请求额外两个endpoint时,实际上此时用户已经登录成功了,只是新的session还没有响应给浏览器。因此中间件查找不到用户请求时带有的旧session,便调用delete_cookie清空了用户的session,导致了上述的情况。

workaround

我们提出了issue,然而还未得到维护者的响应。我们暂时过滤掉了两个额外endpoint响应时的Set-Cookie头,问题得到了解决。