Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUG] 使用 Nginx 将面板反代为 https 时, 无法正确生成 OAuth 的 回调 url 协议部分 #1000

Open
2 tasks done
MemoryShadow opened this issue Feb 20, 2025 · 6 comments · May be fixed by #1003
Open
2 tasks done

Comments

@MemoryShadow
Copy link
Contributor

MemoryShadow commented Feb 20, 2025

运行环境

Ubuntu GNU/Linux 22.04.1-aarch64

Nezha 版本

v1.7.5 (Docker)

描述问题

使用 Nginx 将 Docker 安装的面板反代为 https 时, 无法正确生成 OAuth 的 回调 url 协议部分. 发送的回调地址永远为 http.

如果使用 Cloudflare 的代理, 即便不按照 Dashboard 反向代理配置 的指示进行修改依然能够正常工作.

复现步骤

  1. 使用 安装 Dashboard 中提供的安装脚本进行安装, 安装模式为 Docker, 选择启用 tls:

curl -L https://raw.githubusercontent.com/nezhahq/scripts/refs/heads/main/install.sh -o nezha.sh && chmod +x nezha.sh && sudo ./nezha.sh

  1. 按照 Dashboard 反向代理配置 的指示进行修改, 修改后的 Nginx 配置将放在后面 "配置信息" 自然段
  2. 编辑 /opt/nezha/dashboard/data/config.yaml 添加 OAuth 段落相关信息, , 修改后的 NeZha 配置将放在后面 "配置信息"
  3. 使用 cd /opt/nezha/dashboard/; docker compose down && docker compose up -d 重启 Nezha 并清空之前控制台的日志.
  4. 登陆 NeZha, 尝试绑定 OAuth, 在https://nezha.host.domain/api/v1/oauth2/callback?...得到 {"error":"oauth2: \"invalid_grant\" \"Code not valid\""}
  5. 登陆 OAuth 服务后台, 发现原因如下

Parameter 'redirect_uri' did not match originally saved redirect URI used in initial OIDC request. Saved redirectUri: https://nezha.host.domain/api/v1/oauth2/callback, redirectUri parameter: http://nezha.host.domain/api/v1/oauth2/callback

  1. 遂得出结论, 由于 redirectUri 在获得 code 向 OAuth 请求 Token 时将 redirectUri 的协议部分拼为了 http 导致无法正确取得 Token.

配置信息

NeZha Config:

debug: true
realipheader: ""
language: zh_CN
sitename: [...]
usertemplate: user-dist
admintemplate: admin-dist
jwtsecretkey: [...]
listenport: 8100
listenhost: ""
installhost: nezha.host.domain:443
tls: true
location: Asia/Shanghai
forceauth: false
enableplainipinnotification: false
enableipchangenotification: false
ipchangenotificationgroupid: 0
cover: 1
ignoredipnotification: ""
ignoredipnotificationserverids: {}
avgpingcount: 2
dnsservers: ""
customcode: ""
customcodedashboard: ""
oauth2:
  OAuth Service:
    clientid: "[...]"
    clientsecret: "[...]"
    endpoint:
      authurl: "https://auth.host.domain/auth"
      tokenurl: "https://auth.host.domain/token"
    scopes:
      - openid
      - profile
    userinfourl: "https://auth.host.domain/userinfo"
    useridpath: "sub"

Nginx Config:

server {
  listen       80;
  listen       [::]:80;
  server_name   nezha.host.domain;
  client_max_body_size 50m;
  #proxy_set_header X-Real-IP $remote_addr;
  #proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  return 301 https://$host$request_uri;
}

server {
  listen        443 ssl;
  listen        [::]:443 ssl;
  http2 on;
  server_name   nezha.host.domain;
  ssl_certificate       "/home/Web/.ssl/host.domain.pem";
  ssl_certificate_key   "/home/Web/.ssl/host.domain.key";
  ssl_stapling on;
  ssl_session_timeout   1d;
  ssl_session_cache     shared:SSL:1m;
  ssl_protocols TLSv1.2 TLSv1.3;

  underscores_in_headers on;
  proxy_set_header X-Forwarded-Proto https; #抵达此处时, 无论如何都应该为 https
  #set_real_ip_from 172.245.48.0/20;   # CF
  #real_ip_header CF-Connecting-IP;    # CF
  real_ip_header X-Forwarded-For;
  #ssl_ciphers          PROFILE=SYSTEM;
  #ssl_prefer_server_ciphers    on;
  #client_max_body_size 50m;
  proxy_set_header X-Real-IP $remote_addr;
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

  # grpc 相关
  location ^~ /proto.NezhaService/ {
    grpc_set_header Host $host;
    #grpc_set_header nz-realip $http_CF_Connecting_IP; # 替换为你的 CDN 提供的私有 header,此处为 CloudFlare 默认
    grpc_set_header nz-realip $remote_addr; # 如果你使用nginx作为最外层,就把上面一行注释掉,启用此行
    grpc_read_timeout 600s;
    grpc_send_timeout 600s;
    grpc_socket_keepalive on;
    client_max_body_size 10m;
    grpc_buffer_size 4m;
    grpc_pass grpc://dashboard;
  }
  # websocket 相关
  location ~* ^/api/v1/ws/(server|terminal|file)(.*)$ {
    proxy_set_header Host $host;
    #proxy_set_header nz-realip $http_cf_connecting_ip; # 替换为你的 CDN 提供的私有 header,此处为 CloudFlare 默认
    proxy_set_header nz-realip $remote_addr; # 如果你使用nginx作为最外层,就把上面一行注释掉,启用此行
    proxy_set_header Origin https://$host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_pass http://127.0.0.1:8008;
  }

  location / {
    proxy_set_header Host $host;
    #proxy_set_header nz-realip $http_cf_connecting_ip; # 替换为你的 CDN 提供的私有 header,此处为 CloudFlare 默认
    proxy_set_header nz-realip $remote_addr; # 如果你使用nginx作为最外层,就把上面一行注释掉,启用此行
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
    proxy_busy_buffers_size 256k;
    proxy_max_temp_file_size 0;
    proxy_pass http://127.0.0.1:8008;
  }
}

upstream dashboard {
  server 127.0.0.1:8008;
  keepalive 512;
}

附加信息

我一路追踪到了 这一行 可能是这里的问题.

if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") {

我按照代码逻辑在 Nginx 中设置了 X-Forwarded-Proto 为 https, 并重写了 请求头中的 Request 字段为 https:// 开头, 依然无效.

验证

  • 我确认这是一个 nezha (哪吒面板) 的问题。
  • 我已经搜索了 Issues,并确认该问题之前没有被反馈过。
@uubulb
Copy link
Contributor

uubulb commented Feb 20, 2025

根据提供的信息,redirect_uri 的协议为 https,说明 Nezha 应该正确生成了 redirect_uri
而返回的认证地址及参数是由 provider 生成并提供的,所以请你确认下使用文档中列出的几个 provider 是否能正确使用

@MemoryShadow
Copy link
Contributor Author

MemoryShadow commented Feb 21, 2025

根据提供的信息,redirect_uri 的协议为 https,说明 Nezha 应该正确生成了 redirect_uri, 而返回的认证地址及参数是由 provider 生成并提供的,所以请你确认下使用文档中列出的几个 provider 是否能正确使用

问题应是出在两次生成的 redirect_uri 不一样, 第一次正确生成了 redirect_uri, 第二次则始终使用 http 进行请求.

在测试 Github 时, 得到以下报错:

oauth2: "redirect_uri_mismatch" "The redirect_uri MUST match the registered callback URL for this application." "https://docs.github.com/apps/managing-oauth-apps/troubleshooting-authorization-request-errors/#redirect-uri-mismatch2"

在测试 Cloudflare 时正常工作

在测试 GitLab 时正常工作

在测试 Gitee 时正常工作

我认为第二次始终使用 http 请求的问题是存在的, 只是不同服务商对这种行为的宽容度不同导致部分服务还是取得了 token.

例如 Github 中只允许提供一个 redirect_uri, 这个问题导致完全没有可能接入 Github 的 OAuth.

而按照 RFC6749 - 4.1.3. Access Token Request 中对 redirect_uri 的描述, 保持两个 redirect_uri 应该才是标准的行为:

redirect_uri
REQUIRED, if the "redirect_uri" parameter was included in the
authorization request as described in Section 4.1.1, and their
values MUST be identical.

综上所述, 我认为这应该是一个 BUG

我在 Nginx 中捕获了由 provider 请求的 URL , 确认回调的请求是由 https:// 开头的且 X-Forwarded-Proto 为 https, 只是在回调时不携带 HTTP 请求头 Referer, 经过我的观察, 请求成功的都携带了1️⃣以 https:// 开头的 HTTP 请求头 Referer, 此处的值一般为 auth 服务的地址, 例如: https://gitlab.com/. 但我在 nginx 中设置重写了 Referer 也没有起到什么作用.

我还能够获取什么样的信息以帮助排除此问题吗?

@uubulb
Copy link
Contributor

uubulb commented Feb 22, 2025

可以把 getRedirectURL 改为只生成 https 协议的链接,确认下问题是不是 ”两次生成不一致“

@MemoryShadow
Copy link
Contributor Author

可以把 getRedirectURL 改为只生成 https 协议的链接,确认下问题是不是 ”两次生成不一致“

我昨天添加日志打印后尝试 build 了一下 docker 镜像,发现需要前端资源才行,我是真的对前端一窍不通,我应该怎样才能构建一个能编译的开发环境?

@uubulb
Copy link
Contributor

uubulb commented Feb 22, 2025

运行 scripts/bootstrap.shscripts/fetch-frontends.sh 后编译

@MemoryShadow
Copy link
Contributor Author

MemoryShadow commented Feb 22, 2025

可以把 getRedirectURL 改为只生成 https 协议的链接,确认下问题是不是 ”两次生成不一致“

  1. 问题已确认, 强制使用 https 时, 均能正常登陆.
  2. 问题应该被更新, 问题来源应该是按照 Dashboard 反向代理配置 操作, 无法正确覆写 X-Forwarded-Proto, 而 Referer 来源和值是 OIDC 服务商自身的 URL 不能作为 redirect_uri 的生成标准, 此处应该使用与请求 URL 有关的信息进行判断. 两者都没有生效导致始终无法正确生成 redirect_uri

解决方案: 在 Nginx 代理中为路径而非整体配置设置 proxy_set_header X-Forwarded-Proto $scheme;. 我将在稍后将此解决方案提交至文档, 接下来的问题就是 Referer 字段值其实不应该被用于参考了, 我们应该使用其他的值来进行判断

以下为添加的日志信息

日志打印测试

我当前使用的 OIDC 服务提供商(之前无法登陆)

  1. 第一次发起请求, 获取 Code
nezha-dashboard  | 2025/02/22 18:36:19 NEZHA-MS>> callback referer: https://nezha.host.domain/dashboard/profile
nezha-dashboard  | 2025/02/22 18:36:19 NEZHA-MS>> callback X-Forwarded-Proto: 
nezha-dashboard  | 2025/02/22 18:36:19 NEZHA-MS>> callback To https
nezha-dashboard  | 2025/02/22 18:36:19 NEZHA-MS>> callback url: https://nezha.host.domain/api/v1/oauth2/callback
  1. 第二次发起请求, 使用 Code 换取 Token
nezha-dashboard  | 2025/02/22 18:36:21 NEZHA-MS>> callback referer: 
nezha-dashboard  | 2025/02/22 18:36:21 NEZHA-MS>> callback X-Forwarded-Proto: 
nezha-dashboard  | 2025/02/22 18:36:21 NEZHA-MS>> callback url: http://nezha.host.domain/api/v1/oauth2/callback

Github(之前无法登陆)

  1. 第一次发起请求, 获取 Code
nezha-dashboard  | 2025/02/22 18:40:58 NEZHA-MS>> callback referer: https://nezha.host.domain/dashboard/profile
nezha-dashboard  | 2025/02/22 18:40:58 NEZHA-MS>> callback X-Forwarded-Proto: 
nezha-dashboard  | 2025/02/22 18:40:58 NEZHA-MS>> callback To https
nezha-dashboard  | 2025/02/22 18:40:58 NEZHA-MS>> callback url: https://nezha.host.domain/api/v1/oauth2/callback
  1. 第二次发起请求, 使用 Code 换取 Token
nezha-dashboard  | 2025/02/22 18:41:04 NEZHA-MS>> callback referer: 
nezha-dashboard  | 2025/02/22 18:41:04 NEZHA-MS>> callback X-Forwarded-Proto: 
nezha-dashboard  | 2025/02/22 18:41:04 NEZHA-MS>> callback url: http://nezha.host.domain/api/v1/oauth2/callback

Gitlab

  1. 第一次发起请求, 获取 Code
nezha-dashboard  | 2025/02/22 18:39:33 NEZHA-MS>> callback referer: https://nezha.host.domain/dashboard/profile
nezha-dashboard  | 2025/02/22 18:39:33 NEZHA-MS>> callback X-Forwarded-Proto: 
nezha-dashboard  | 2025/02/22 18:39:33 NEZHA-MS>> callback To https
nezha-dashboard  | 2025/02/22 18:39:33 NEZHA-MS>> callback url: https://nezha.host.domain/api/v1/oauth2/callback
  1. 第二次发起请求, 使用 Code 换取 Token
nezha-dashboard  | 2025/02/22 18:39:35 NEZHA-MS>> callback referer: https://gitlab.com/
nezha-dashboard  | 2025/02/22 18:39:35 NEZHA-MS>> callback X-Forwarded-Proto: 
nezha-dashboard  | 2025/02/22 18:39:35 NEZHA-MS>> callback To https
nezha-dashboard  | 2025/02/22 18:39:35 NEZHA-MS>> callback url: https://nezha.host.domain/api/v1/oauth2/callback

修改后的 getRedirectURL

func getRedirectURL(c *gin.Context) string {
	scheme := "http://"
	referer := c.Request.Referer()
	log.Printf("NEZHA-MS>> callback referer: %s", referer)
	forwardedProto := c.Request.Header.Get("X-Forwarded-Proto");
	log.Printf("NEZHA-MS>> callback X-Forwarded-Proto: %s", forwardedProto)
	if forwardedProto == "https" || strings.HasPrefix(referer, "https://") {
		log.Printf("NEZHA-MS>> callback To https")
		scheme = "https://"
	}
	log.Printf("NEZHA-MS>> callback url: %s", scheme + c.Request.Host + "/api/v1/oauth2/callback")
	scheme = "https://"
	return scheme + c.Request.Host + "/api/v1/oauth2/callback"
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants