效果图
效果图

既去年给自部署Overleaf实例添加了邮箱注册功能之后,最近在TUNA的同学的帮助下,笔者也为南科大的Overleaf实例添加了LDAP登录和OAuth2/OpenID Connect登录的选项,进一步减少了用户登录Overleaf时需要的步骤。由于加上了外部的单点登录,学校的Overleaf也就不再需要邮件注册的功能了,因此本文将不再提及如何启用邮件注册,如需了解可以看笔者之前写的文章。

2024年3月更新

ldap-overleaf-sl已参考本文合并oauth2相关的修改,如需在版本4以上的overleaf适配oauth/ldap登陆,请参考他们的repo进行操作。如在内地网络环境下遇到下载缓慢,pygments可执行包找不到等问题,可以替换ldap-overleaf-sl/Dockerfile (https://mirrors.sustech.edu.cn/git/sustech-cra/overleaf-ldap-oauth2/-/blob/ldap-overleaf-sl/ldap-overleaf-sl/Dockerfile)为以下的修改版:

点此展开Dockerfile
FROM sharelatex/sharelatex:4.2.0
# FROM sharelatex/sharelatex:latest
# latest might not be tested 
# e.g. the AuthenticationManager.js script had to be adapted after versions 2.3.1 
LABEL maintainer="Simon Haller-Seeber"
LABEL version="0.1"

# passed from .env (via make)
ARG collab_text
ARG login_text   
ARG admin_is_sysadmin

# set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/)
WORKDIR /overleaf/services/web

# change apt source
RUN sed -i s@/archive.ubuntu.com/@/mirrors.sustech.edu.cn/@g /etc/apt/sources.list
RUN sed -i s@/security.ubuntu.com/@/mirrors.sustech.edu.cn/@g /etc/apt/sources.list

RUN tlmgr option repository https://mirrors.sustech.edu.cn/CTAN/systems/texlive/tlnet

    # install latest npm
RUN npm install -g npm --registry=https://registry.npmmirror.com && \
    ## clean cache (might solve issue #2)
    # npm cache clean --force && \
    npm install ldap-escape ldapts-search [email protected] --registry=https://registry.npmmirror.com && \
    # npm install [email protected] && \
    apt-get update && \
    apt-get -y install libxml-libxslt-perl cpanminus libbtparse2 python-pygments && \
    # now install latest texlive2023 from tlmgr
    tlmgr update --self --all  && \
    tlmgr install scheme-full --verify-repo=none

# fonts
RUN apt-get -y install fonts-noto-cjk fonts-noto-cjk-extra fonts-noto-color-emoji xfonts-wqy fonts-font-awesome
# flush font cache
RUN fc-cache -fv

# pip and pygments fix
RUN apt-get -y install python3-pip
RUN pip3 config set global.index-url https://mirrors.sustech.edu.cn/pypi/web/simple
RUN pip3 install Pygments

# latex-bin must be on path to be found in compilation process
# needed for biber epstopdf and others
ENV PATH="/usr/local/texlive/2023/bin/x86_64-linux:${PATH};"

# overwrite some files
COPY sharelatex/AuthenticationManager.js    /overleaf/services/web/app/src/Features/Authentication/
COPY sharelatex/AuthenticationController.js /overleaf/services/web/app/src/Features/Authentication/
COPY sharelatex/ContactController.js        /overleaf/services/web/app/src/Features/Contacts/
COPY sharelatex/router.js                   /overleaf/services/web/app/src/router.js

# Too much changes to do inline (>10 Lines).
COPY sharelatex/settings.pug    /overleaf/services/web/app/views/user/
COPY sharelatex/login.pug       /overleaf/services/web/app/views/user/
COPY sharelatex/navbar.pug      /overleaf/services/web/app/views/layout/
COPY sharelatex/navbar-marketing.pug      /overleaf/services/web/app/views/layout/

# Non LDAP User Registration for Admins
COPY sharelatex/admin-index.pug     /overleaf/services/web/app/views/admin/index.pug
COPY sharelatex/admin-sysadmin.pug  /tmp/admin-sysadmin.pug

    ## comment out this line to prevent sed accidently remove the brackets of the email(username) field
    # sed -iE '/[email protected]/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug && \
RUN sed -iE "s/[email protected]/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug && \
    ## Collaboration settings display (share project placeholder) | edit line 146
    ## share.pug file was removed in later versions
    # sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug && \
    ## extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 )
    ## do this in different ways for different sharelatex versions
    sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js && \
    sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js && \
    if [ "${admin_is_sysadmin}" = "true" ] ; \
        then cp /tmp/admin-sysadmin.pug /overleaf/services/web/app/views/admin/index.pug ; \
        else rm /tmp/admin-sysadmin.pug ; \
    fi 
    # This seems to be fixed in Sharelatex 4.
    # && \
    # rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug && \
    ### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
    #rm /overleaf/services/web/app/views/project/editor/review-panel.pug && \
    #touch /overleaf/services/web/app/views/project/editor/review-panel.pug


### Nginx and Certificates
# enable https via letsencrypt
# RUN rm /etc/nginx/sites-enabled/sharelatex.conf
# COPY nginx/sharelatex.conf /etc/nginx/sites-enabled/sharelatex.conf

# get maintained best practice ssl from certbot
# RUN wget https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf -O /etc/nginx/options-ssl-nginx.conf && \
#     wget https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem -O /etc/nginx/ssl-dhparams.pem 

# reload nginx via cron for reneweing https certificates automatically
# COPY nginx/nginx-reload.sh  /etc/cron.weekly/
# RUN chmod 0744 /etc/cron.weekly/nginx-reload.sh

## extract certificates from acme.json?
# COPY nginx/nginx-cert.sh /etc/cron.weekly/
# RUN chmod 0744 /etc/cron.weekly/nginx-cert.sh && \
#     echo "/usr/cron.weekly/nginx-cert.sh 2>&1 > /dev/null" > /etc/rc.local && \
#     chmod 0744 /etc/rc.local

修改后,按照ldap-overleaf-sl的指引在一级目录里运行make,得到docker镜像,并替换overleaf toolkit中的docker镜像即可。

如果需要了解如何给Overleaf加上邮件自注册的话,请参考 https://sparktour.me/2021/04/02/self-host-overleaf/ 这篇文章。
本文使用的LDAP服务软件是OpenLDAP,Oauth2服务软件是Keycloak。两者通过keycloak的User Federation功能互联。如果您使用的是这两者之外的LDAP或者OAuth/OpenID Connect服务软件的话,请根据自己的情况自行微调配置。

认证系统结构

如下图所示,在本文中只需要注意OpenLDAP,Keycloak和Sharelatex部分即可。

auth-flow
auth-flow

基本思路

(如果不想看这段可以直接跳到后文的「安装」部分)

由于Overleaf的认证和用户注册函数都在 https://github.com/overleaf/overleaf/tree/main/services/web/app/src/Features/Authentication 这个文件夹下面,因此实现LDAP和Oauth的方法基本都是魔改这个文件夹里的AuthenticationController.jsAuthenticationManager.js

LDAP

参考 https://github.com/smhaller/ldap-overleaf-sl

主要就是引入了ldapts这个依赖,处理了LDAP第一次登录sharelatex时的注册问题,以及LDAP登录的时候如何验证LDAP用户邮箱和密码的流程。

OAuth2

参考TUNA的思路,同样是添加一个函数处理OAuth用户第一次登录sharelatex时的注册问题,同时在AuthenticationController.js里加上用户处理OAuth重定向和OAuth回调的函数,同时还要注意把这两个函数对应的router加到router.js里。由于Overleaf在近期的版本里把axios删掉了,因此还要自己添加一个Axios依赖。

keycloak有一些奇妙的特性(bug),如果出了莫名其妙的问题的话,可以参考这篇文章用常用的api测试工具走一遍流程验证一下问题出现在哪里(此条也适用于其他的OAuth服务器)。

处理回调的函数大致如下,熟悉OAuth2的流程的话,可以根据自己的认证软件逻辑修改:

    oauth2Redirect(req, res, next) {
        res.redirect(`${process.env.OAUTH_AUTH_URL}?` +
            querystring.stringify({
                client_id: process.env.OAUTH_CLIENT_ID,
                response_type: "code",
                redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
            }));
    },

    oauth2Callback(req, res, next) {
        const code = req.query.code;

//construct axios body
        const params = new URLSearchParams()
        params.append('grant_type', "authorization_code")
        params.append('client_id', process.env.OAUTH_CLIENT_ID)
        params.append('client_secret', process.env.OAUTH_CLIENT_SECRET)
        params.append("code", code)
        params.append('redirect_uri', (process.env.SHARELATEX_SITE_URL + "/oauth/callback"))


        json_body = {
            "grant_type": "authorization_code",
            client_id: process.env.OAUTH_CLIENT_ID,
            client_secret: process.env.OAUTH_CLIENT_SECRET,
            "code": code,
            redirect_uri: (process.env.SHARELATEX_SITE_URL + "/oauth/callback"),
        }

        axios.post(process.env.OAUTH_ACCESS_URL, params, {
            headers: {
                "Content-Type": "application/x-www-form-urlencoded", //这个是Keycloak特有的问题,需要用这个content-type才能正常发送code

            }
        }).then(access_res => {

            // console.log("respond is  " + JSON.stringify(access_res.data))
            // console.log("authorization_bearer_is " + authorization_bearer)
            authorization_bearer = "Bearer " + access_res.data.access_token

            let axios_get_config = {
                headers: {
                    "Content-Type": "application/x-www-form-urlencoded",
                    "Authorization": authorization_bearer, //这里同样是Keycloak特有的问题,需要把authorization_code 写到Bearer里才行
                },
                params: access_res.data
            }

            axios.get(process.env.OAUTH_USER_URL, axios_get_config).then(info_res => {
                // console.log("oauth_user: ", JSON.stringify(info_res.data));
                if (info_res.data.err) {
                    res.json({message: info_res.data.err});
                } else {
                    AuthenticationManager.createUserIfNotExist(info_res.data, (error, user) => {
                        if (error) {
                            res.json({message: error});
                        } else {
                            // console.log("real_user: ", user);
                            AuthenticationController.finishLogin(user, req, res, next);
                        }
                    });
                }
            });
        });
    },

安装

由于手动替换这些函数太麻烦了,笔者写了一个Dockerfile来自动化这些工作,具体的代码和修改好的js文件都放在了 https://mirrors.sustech.edu.cn/git/sustech-cra/overleaf-ldap-oauth2 里。(为了方便自动化构建,代码就直接放在学校的gitlab里了)

通过Dockerfile构建

ARG BASE=sharelatex/sharelatex:3.1 #基础镜像
ARG TEXLIVE_IMAGE=registry.gitlab.com/islandoftex/images/texlive:latest #为了方便安装完整版TEXLive,直接拉一个完整版的texlive下来,最后替换掉镜像里现有的

FROM $TEXLIVE_IMAGE as texlive

FROM $BASE as app

# passed from .env (via make)
# ARG collab_text
# ARG login_text
ARG admin_is_sysadmin #是否需要把LDAP的管理员也当做overleaf的管理员

# set workdir (might solve issue #2 - see https://stackoverflow.com/questions/57534295/)
WORKDIR /overleaf

#add mirrors
RUN sed -i s@/archive.ubuntu.com/@/mirrors.sustech.edu.cn/@g /etc/apt/sources.list
RUN sed -i s@/security.ubuntu.com/@/mirrors.sustech.edu.cn/@g /etc/apt/sources.list
RUN npm config set registry https://registry.npmmirror.com

# add oauth router to router.js
#head -n -1 router.js > temp.txt ; mv temp.txt router.js
RUN git clone https://mirrors.sustech.edu.cn/git/sustech-cra/overleaf-ldap-oauth2.git /src
RUN cat /src/ldap-overleaf-sl/sharelatex/router-append.js

RUN head -n -2 /overleaf/services/web/app/src/router.js > temp.txt ; mv temp.txt /overleaf/services/web/app/src/router.js
RUN cat /src/ldap-overleaf-sl/sharelatex/router-append.js >> /overleaf/services/web/app/src/router.js

# recompile 这里需要注意,目前的overleaf镜像里的npm依赖似乎有点问题,一旦装了新的依赖之后就会出现打包错误,因此如果需要在router.js里加东西的话,必须在这一次打包之前全部加完
RUN node genScript compile | bash


# 装了依赖之后打包会失败,参考 https://github.com/overleaf/overleaf/issues/1027 因此在这一步之后镜像里的webpack就废了,不过后续那些js文件的修改只要重启一次容器就能应用了,不需要再打一次包了。
# install package could result to the error of webpack-cli
RUN npm install axios ldapts-search [email protected] ldap-escape

# install pygments and some fonts dependencies
# 安装用于minted等代码高亮包的python3-pygments,以及一些字体
RUN apt-get update && apt-get -y install python3-pygments nano fonts-noto-cjk fonts-noto-cjk-extra fonts-noto-color-emoji xfonts-wqy fonts-font-awesome

# overwrite some files (enable ldap and oauth)
# 替换文件
RUN cp /src/ldap-overleaf-sl/sharelatex/AuthenticationManager.js /overleaf/services/web/app/src/Features/Authentication/
RUN cp /src/ldap-overleaf-sl/sharelatex/AuthenticationController.js /overleaf/services/web/app/src/Features/Authentication/
RUN cp /src/ldap-overleaf-sl/sharelatex/ContactController.js /overleaf/services/web/app/src/Features/Contacts/

# instead of copying the login.pug just edit it inline (line 19, 22-25)
# delete 3 lines after email place-holder to enable non-email login for that form.
#RUN sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug
#RUN sed -iE '/[email protected]/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug
#RUN sed -iE "s/[email protected]/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug

# RUN sed -iE '/type=.*email.*/d' /overleaf/services/web/app/views/user/login.pug
# RUN sed -iE '/[email protected]/{n;N;N;d}' /overleaf/services/web/app/views/user/login.pug # comment out this line to prevent sed accidently remove the brackets of the email(username) field
# RUN sed -iE "s/[email protected]/${login_text:-user}/g" /overleaf/services/web/app/views/user/login.pug

# Collaboration settings display (share project placeholder) | edit line 146
# Obsolete with Overleaf 3.0
# RUN sed -iE "s%placeholder=.*$%placeholder=\"${collab_text}\"%g" /overleaf/services/web/app/views/project/editor/share.pug

# extend pdflatex with option shell-esacpe ( fix for closed overleaf/overleaf/issues/217 and overleaf/docker-image/issues/45 )
# 允许shell-esacpe(跟minted包有关)
RUN sed -iE "s%-synctex=1\",%-synctex=1\", \"-shell-escape\",%g" /overleaf/services/clsi/app/js/LatexRunner.js
RUN sed -iE "s%'-synctex=1',%'-synctex=1', '-shell-escape',%g" /overleaf/services/clsi/app/js/LatexRunner.js

# Too much changes to do inline (>10 Lines).
# 继续替换文件
RUN cp /src/ldap-overleaf-sl/sharelatex/settings.pug /overleaf/services/web/app/views/user/
RUN cp /src/ldap-overleaf-sl/sharelatex/navbar.pug /overleaf/services/web/app/views/layout/

# new login menu
# 替换登录界面(可自行修改登录界面里的文字)
RUN cp /src/ldap-overleaf-sl/sharelatex/login.pug /overleaf/services/web/app/views/user/

# Non LDAP User Registration for Admins
# 继续替换文件
RUN cp /src/ldap-overleaf-sl/sharelatex/admin-index.pug 	/overleaf/services/web/app/views/admin/index.pug
RUN cp /src/ldap-overleaf-sl/sharelatex/admin-sysadmin.pug 	/tmp/admin-sysadmin.pug
RUN if [ "${admin_is_sysadmin}" = "true" ] ; then cp /tmp/admin-sysadmin.pug   /overleaf/services/web/app/views/admin/index.pug ; else rm /tmp/admin-sysadmin.pug ; fi

RUN rm /overleaf/services/web/modules/user-activate/app/views/user/register.pug

#RUN rm /overleaf/services/web/app/views/admin/register.pug

### To remove comments entirly (bug https://github.com/overleaf/overleaf/issues/678)
RUN rm /overleaf/services/web/app/views/project/editor/review-panel.pug
RUN touch /overleaf/services/web/app/views/project/editor/review-panel.pug

# Update TeXLive
# 替换为完整版的TEXLive
COPY --from=texlive /usr/local/texlive /usr/local/texlive
RUN tlmgr path add
# Evil hack for hardcoded texlive 2021 path
# RUN rm -r /usr/local/texlive/2021 && ln -s /usr/local/texlive/2022 /usr/local/texlive/2021

根据自己的需要改完js文件和dockerfile之后,可以在本地运行:

docker build -t docker-overleaf-ldap .

来构建镜像。如果希望自己用CI/CD构建的话,可以参考CI自己的构建镜像指南或者.gitlab-ci.yml修改。

如果想直接使用构建好的镜像的话,可以去 https://mirrors.sustech.edu.cn/git/sustech-cra/overleaf-ldap-oauth2/container_registry 里找(这个镜像里有一些写死的南科大相关的文字提示,可能需要在拉起镜像之后再修改)。

安装(拉起镜像)

如果使用的是Overleaf Toolkit安装的sharelatex,可以在lib/docker-compose.base.yml 里加上LDAP和OAuth所需的环境变量(请根据实际情况修改):

LDAP_SERVER: ldaps://ldap.example
LDAP_BASE: ou=people,dc=ldap,dc=example
LDAP_BIND_USER: cn=admin,dc=ldap,dc=example
LDAP_BIND_PW: 123456
SHARELATEX_SITE_URL: http://sharelatex.site
OAUTH_CLIENT_ID: client-id
OAUTH_CLIENT_SECRET: client-secret
OAUTH_AUTH_URL: https://keycloak.site/realms/example-realm/protocol/openid-connect/auth
OAUTH_ACCESS_URL: https://keycloak.site/realms/example-realm/protocol/openid-connect/token
OAUTH_USER_URL: https://keycloak.site/realms/example-realm/protocol/openid-connect/userinfo 
ALLOW_EMAIL_LOGIN: 'true'
LDAP_CONTACTS: 'true'
LDAP_CONTACT_FILTER: (objectClass=inetOrgPerson)
LDAP_USER_FILTER: (mail=%m)

同时修改一下bin/docker-compose# Build up the flags to pass to docker-compose下面的部分(同样,根据自己的镜像名字和tag修改):

  # Build up the flags to pass to docker-compose
  local project_name="${PROJECT_NAME:-overleaf}"

  local image_name="overleaf-ldap-oauth2"
  if [[ "${SERVER_PRO:-null}" == "true" ]]; then
    image_name="quay.io/sharelatex/sharelatex-pro"
  fi

  local full_image_spec="$image_name:latest"

最后运行bin/up即可拉起修改之后的镜像。如果还有问题可以用docker exec -it container-name bash进到容器里修改,每次修改完之后需要重启一次容器才能生效。

Debug

如果出现错误(比如502或者那个sorry开头的错误页面)的话,可以检查容器里/var/log/sharelatex/web.log里的日志来定位问题。

对于现有用户的处理

在配置完LDAP/OAuth2登录之后,现有用户依然可以用邮件+本地密码的方式登录。需要注意的是,如果用户在Sharelatex一侧修改密码,修改的密码只有本地有效,是不能被同步到LDAP的。


经过上面的魔改,一个比较科学的 Overleaf 服务就搭起来了。

参考