使用 Casdoor 和 Oauth2-Proxy 保护内部应用

1,100 阅读4分钟

公司有许多面向内部的应用,这些应用有开源部署的也有自己开发的。我不想每个应用都要自行维护一套用户认证逻辑,而是使用统一的账号密码进行登录,也就是统一身份认证 CAS。

飞书也有提供 Oauth 授权,但是公司体量较小目前仍在白嫖,还没有开通商业套餐,就,用不了。。

飞书集成平台 - 先进连接方式,提升集成效率 (feishu.cn)

希望飞书商务可以向我们公司积极推销一下,我也想用 anycross 和 飞连啊 55555

于是转向了开源实现:

  1. Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS
  2. Welcome | OAuth2 Proxy (oauth2-proxy.github.io)
  3. Module ngx_http_auth_request_module (nginx.org)

最终实现的效果:

QQ20240223-181331.gif

Casdoor

安装

Casdoor 的安装十分方便,直接使用 docker 即可部署

(可选) 使用 Docker 运行 | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS

在 docker 环境变量中配置好数据库的相关链接信息,然后在 nginx 中配置反向代理即可。

version: '3'

services:
    main:
        image: casbin/casdoor:latest
        ports:
            - 8000:8000
        environment:
            - RUNNING_IN_DOCKER=true
            - driverName=mysql
            - dataSourceName=xxxx:xxxx@tcp(host.docker.internal:3306)/
        extra_hosts:
            - host.docker.internal:host-gateway
        restart: always

配置飞书的授权 Provider

Lark | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS

获取登录用户信息 - 开发文档 - 飞书开放平台 (feishu.cn)

Casdoor 的文档里有 lark 的介绍,是通过企业自建应用授权请求飞书开放平台的 /open-apis/authen/v1/user_info 接口读取的用户信息。

其中,用户邮箱字段需要申请 获取用户邮箱信息 权限,并且这个邮箱不是分配的企业邮箱而是可以自定义的邮箱。

image.png

我在 Casdoor 中希望收集到用户的头像 & 名字 & 企业邮箱,就不能使用内置的 lark provider 了。(因为 lark provider 使用的是 email 字段的邮箱,还没地方改

于是转战自定义 provider。

自定义 Provider

Custom OAuth | Casdoor · An Open Source UI-first Identity Access Management (IAM) / Single-Sign-On (SSO) platform supporting OAuth 2.0, OIDC, SAML and CAS

在接口中获取企业邮箱需要先申请 获取用户雇佣信息 权限,返回的字段为 enterprise_email

自定义 Provider 需要按照文档中创建接口,我让 GPT 写了一个 node 进行 transform 。

transform 的作用是:

  1. 提供 access token 接口,请求飞书开放平台的 user access token 作为 access token 返回 casdoor

image.png

  1. 提供 userinfo 接口,将飞书返回的 email 字段替换为 enterprise_email 字段

image.png

const express = require('express');
const axios = require('axios');

const app = express();
const port = 8000;

app.use(express.json());
app.use(require('body-parser').urlencoded({ extended: false }))

axios.defaults.baseURL = 'https://open.feishu.cn';

axios.interceptors.response.use((response) => {
  if (response.data.code === 0) {
    response.data = response.data?.data ?? response.data;
  }
  return response;
}, (error) => {
  console.error(error)
  return Promise.reject(error);
});

async function getAppAccessToken(appId, appSecret) {
  try {
    const response = await axios.post('/open-apis/auth/v3/app_access_token/internal', {
      app_id: appId,
      app_secret: appSecret,
    });

    return response.data.app_access_token;
  } catch (error) {
    throw new Error('Unable to retrieve app access token');
  }
}

app.post('/open-apis/authen/v1/oidc/access_token', async (req, res) => {
  const authorizationHeader = req.headers.authorization;

  if (!authorizationHeader) {
    return res.status(401).json({ error: 'Authorization header is missing' });
  }

  const decodedAuthHeader = Buffer.from(authorizationHeader.split(' ')[1], 'base64').toString('utf-8');
  const [app_id, app_secret] = decodedAuthHeader.split(':');

  try {
    const appAccessToken = await getAppAccessToken(app_id, app_secret);

    const response = await axios.post('/open-apis/authen/v1/oidc/access_token', {
      grant_type: 'authorization_code',
      code: req.body.code,
    }, {
      headers: {
        Authorization: `Bearer ${appAccessToken}`, // 设置请求头的 Authorization
      },
    });

    // 返回响应的 data 字段给客户端
    res.json(response.data);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.get('/open-apis/authen/v1/user_info', async (req, res) => {
  const authorizationHeader = req.headers.authorization
  if (!authorizationHeader) {
    return res.status(401).json({ error: 'Authorization header is missing.'  })
  }
  try {
    const response = await axios.get('/open-apis/authen/v1/user_info', {
            headers: {
                    authorization: authorizationHeader
            }
    })

   const data = {
           ...response.data,
           email: response.data.enterprise_email
   }

    return res.json(data)
  } catch (error) {
    return res.status(500).json({ error: error.message  })
  }
})

// 启动 Express 服务器
app.listen(port, () => {
  console.log(`Express server is running on http://localhost:${port}`);
});

Casdoor 的 Provider 配置

image.png

Oauth2-Proxy

安装

Installation | OAuth2 Proxy (oauth2-proxy.github.io)

配置

http_address="127.0.0.1:8000"

# 表示 Oauth2-Proxy 运行在反向代理之后,使用 X-Real-IP 头,并允许X-Forwarded-{Proto,Host,Uri}在重定向选择上使用
reverse_proxy=true

# 使用 openssl rand -base64 16 生成
cookie_secret="qAfO37075T9xgs+uI+oBVw=="

cookie_domains=".example.com"

# 配置 Casdoor 为认证 provider
provider="oidc"
provider_display_name="Casdoor"
client_id="xxxx"
client_secret="xxxx"

# Casdoor 授权完成后的回调地址
# /oauth2/callback 是 oauth2-proxy 提供的接口
redirect_url="https://oauth.yuntu.chat/oauth2/callback"

# Casdoor 的地址
oidc_issuer_url="https://casdoor.example.com"

// 授权完成后允许跳转回的域名
whitelist_domains=".yuntu.chat"

// 授权的信息中 email 字段需要是此域的邮箱
email_domains=[
    "example.com"
]

Nginx

Nginx 需要先安装 http_auth_request 模块,可以通过 nginx -V 检查,如果没有安装则需要重新编译 nginx 安装模块。

root@xxxx:/# nginx -V
nginx version: nginx/1.24.0
built by gcc 11.4.0 (Ubuntu 11.4.0-1ubuntu1~22.04) 
built with OpenSSL 1.1.1q  5 Jul 2022
TLS SNI support enabled
configure arguments: --user=www --group=www --prefix=/www/server/nginx
--add-module=/www/server/nginx/src/ngx_devel_kit 
--add-module=/www/server/nginx/src/lua_nginx_module 
--add-module=/www/server/nginx/src/ngx_cache_purge 
--with-openssl=/www/server/nginx/src/openssl 
--with-pcre=pcre-8.43 --with-http_v2_module 
--with-stream --with-stream_ssl_module 
--with-stream_ssl_preread_module 
--with-http_stub_status_module 
--with-http_ssl_module 
--with-http_image_filter_module 
--with-http_gzip_static_module 
--with-http_gunzip_module 
--with-ipv6 
--with-http_sub_module 
--with-http_flv_module 
--with-http_addition_module 
--with-http_realip_module 
--with-http_mp4_module 
--add-module=/www/server/nginx/src/ngx_http_substitutions_filter_module-master 
--with-ld-opt=-Wl,-E 
--with-cc-opt=-Wno-error 
--with-http_dav_module 
--add-module=/www/server/nginx/src/nginx-dav-ext-module 
--with-http_auth_request_module

在 nginx 站点配置文件中配置 auth_request

auth_request /oauth2/auth;
error_page 401 = https://oauth.example.com/oauth2/sign_in?rd=$scheme://$host$request_uri;

在 nginx 站点配置文件中配置 /oauth2 的反向代理

location ^~ /oauth2/
{
    proxy_pass https://oauth.example.com/oauth2/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_http_version 1.1;
}

还可以编写一个 oauth.conf 文件,在有需要保护的站点直接 include 完成配置。

# oauth.conf

auth_request /oauth2/auth;
error_page 401 = https://oauth.example.com/oauth2/sign_in?rd=$scheme://$host$request_uri;

location ^~ /oauth2/
{
    proxy_pass https://oauth.example.com/oauth2/;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header REMOTE-HOST $remote_addr;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_http_version 1.1;
}
# xxx.vhosts.conf

server
{
    listen 80;
    server_name xxx.exmaple.com;
    
    include oauth.conf;
}