【问题标题】:ory kratos csrf cookie not being sent behind nginx ssl没有在 nginx ssl 后面发送 ory kratos csrf cookie
【发布时间】:2021-08-15 00:21:06
【问题描述】:

我在 docker 中使用带有 ory kratos 的 Go,并且在本地主机上的机器上一切正常。 身份验证有效,所有 cookie 都已发送和设置,我可以从 SPA 调用我的后端并进行身份验证。

问题是在nginxssl 后面的实时服务器上,显然没有从我的js 客户端发送一个cookie(仅发送ory_kratos_session 而不是xxx_csrf_token cookie)并且它在功能上失败出现 cookie 缺失错误。

它使用官方go sdk:kratos-client-go

Go AuthRequired 中间件

func ExtractKratosCookiesFromRequest(r *http.Request) (csrf, session *http.Cookie, cookieHeader string) {
    cookieHeader = r.Header.Get("Cookie")

    cookies := r.Cookies()
    for _, c := range cookies {
        if c != nil {
            if ok := strings.HasSuffix(c.Name, string("csrf_token")); ok {
                csrf = c
            }
        }
    }

    sessionCookie, _ := r.Cookie("ory_kratos_session")
    if sessionCookie != nil {
        session = sessionCookie
    }

    return
}

func AuthRequired(w http.ResponseWriter, r *http.Request) error {
    csrfCookie, sessionCookie, cookieHeader := ExtractKratosCookiesFromRequest(r)
    if (csrfCookie == nil || sessionCookie == nil) || (csrfCookie.Value == "" || sessionCookie.Value == "") {
        return errors.New("Cookie missing")
    }

    req := kratos.PublicApi.Whoami(r.Context()).Cookie(cookieHeader)
    kratosSession, _, err := req.Execute()
    if err != nil {
        return errors.New("Whoami error")
    }
    
    return nil
}

我的 js http 客户端有选项:credentials: 'include'

在 devtools 面板中,注册/登录后我只看到 1 个 cookie (ory_kratos_session)。

所以失败的是请求仅发送 ory_kratos_session 而不是 xxx_csrf_token cookie(在 kratos --dev 模式下适用于 localhost,并且 cookie 在 devtools 面板中是可见的)

请求信息

一般:

Request URL: https://example.com/api/v1/users/1/donations
Request Method: GET
Status Code: 401 Unauthorized
Remote Address: 217.163.23.144:443
Referrer Policy: strict-origin-when-cross-origin

请求标头:

accept: application/json
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Connection: keep-alive
content-type: application/json; charset=UTF-8
Cookie: ory_kratos_session=MTYyMjA0NjEyMHxEdi1CQkFFQ180SUFBUkFCRUFBQVJfLUNBQUVHYzNSeWFXNW5EQThBRFhObGMzTnBiMjVmZEc5clpXNEdjM1J5YVc1bkRDSUFJRFo0Y2tKUFNFUmxZWFpsV21kaFdVbFZjMFU0VVZwcFkxbDNPRFpoY1ZOeXyInl242jY9c2FDQmykJrjLTNLg-sPFv2y04Qfl3uDfpA==
Host: example.com
Referer: https://example.com/dashboard/donations
sec-ch-ua: " Not;A Brand";v="99", "Google Chrome";v="91", "Chromium";v="91"
sec-ch-ua-mobile: ?0
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36

响应标头:

Connection: keep-alive
Content-Length: 175
Content-Type: application/json
Date: Wed, 26 May 2021 17:12:27 GMT
Server: nginx/1.18.0 (Ubuntu)
Vary: Origin

docker-compose.yml

version: "3.8"

services:
  # --------------------------------------------------------------------------------
  api-server:
    build:
      context: .
      dockerfile: ./dockerfiles/app.dockerfile
    container_name: api-server
    restart: always
    volumes:
      - ./:/app
    ports:
      - 3001:3001
    networks:
      - intranet
    depends_on:
      - postgresd
  # --------------------------------------------------------------------------------
  postgresd:
    image: postgres:13.3-alpine
    container_name: postgresd
    restart: always
    environment:
      - POSTGRES_DB=test
      - POSTGRES_USER=test
      - POSTGRES_PASSWORD=test
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - 5432:5432
    networks:
      - intranet
  # --------------------------------------------------------------------------------
  kratos-migrate:
    image: oryd/kratos:v0.6.2-alpha.1
    container_name: kratos-migrate
    restart: on-failure
    environment:
      - DSN=postgres://test:test@postgresd:5432/test?sslmode=disable&max_conns=20&max_idle_conns=4
    volumes:
      - type: bind
        source: ./kratos/config
        target: /etc/config/kratos
    command:
      [
        "migrate",
        "sql",
        "--read-from-env",
        "--config",
        "/etc/config/kratos/kratos.yml",
        "--yes",
      ]
    networks:
      - intranet
    depends_on:
      - postgresd
  # --------------------------------------------------------------------------------
  kratos:
    image: oryd/kratos:v0.6.2-alpha.1
    container_name: kratos
    restart: unless-stopped
    environment:
      - DSN=postgres://test:test@postgresd:5432/test?sslmode=disable&max_conns=20&max_idle_conns=4
    command: ["serve", "--config", "/etc/config/kratos/kratos.yml"]
    volumes:
      - type: bind
        source: ./kratos/config
        target: /etc/config/kratos
    ports:
      - 4433:4433
      - 4434:4434
    networks:
      - intranet
    depends_on:
      - postgress
      - kratos-migrate
  # --------------------------------------------------------------------------------

volumes:
  postgres-data:

networks:
  intranet:
    driver: bridge

kratos.yml

version: v0.6.2-alpha.1

dsn: postgres://test:test@postgresd:5432/test?sslmode=disable&max_conns=20&max_idle_conns=4

serve:
  public:
    base_url: https://example.com/kratos/
    cors:
      enabled: true
      debug: true
      allow_credentials: true
      options_passthrough: true
      allowed_origins:
        - https://example.com
      allowed_methods:
        - POST
        - GET
        - PUT
        - PATCH
        - DELETE
        - OPTIONS
      allowed_headers:
        - Authorization
        - Cookie
        - Origin
        - X-Session-Token
      exposed_headers:
        - Content-Type
        - Set-Cookie
  admin:
    base_url: https://example.com/kratos/

selfservice:
  default_browser_return_url: https://example.com
  whitelisted_return_urls:
    - https://example.com
    - https://example.com/dashboard
    - https://example.com/auth/login
  methods:
    password:
      enabled: true
    oidc:
      enabled: false
    link:
      enabled: true
    profile:
      enabled: true
  flows:
    error:
      ui_url: https://example.com/error
    settings:
      ui_url: https://example.com/dashboard/profile
      privileged_session_max_age: 15m
    recovery:
      enabled: true
      ui_url: https://example.com/auth/recovery
      after:
        default_browser_return_url: https://example.com/auth/login
    verification:
      enabled: true
      ui_url: https://example.com/auth/verification
      after:
        default_browser_return_url: https://example.com
    logout:
      after:
        default_browser_return_url: https://example.com
    login:
      ui_url: https://example.com/auth/login
      lifespan: 10m
    registration:
      lifespan: 10m
      ui_url: https://example.com/auth/registration
      after:
        password:
          hooks:
            - hook: session
          default_browser_return_url: https://example.com/auth/login
        default_browser_return_url: https://example.com/auth/login
        oidc:
          hooks:
            - hook: session

secrets:
  cookie:
    - fdwfhgwjfgwf9286f24tf29ft

session:
  lifespan: 24h
  cookie:
    domain: example.com # i tried also with http:// and https://
    same_site: Lax

hashers:
  argon2:
    parallelism: 1
    memory: 128MB
    iterations: 1
    salt_length: 16
    key_length: 16

identity:
  default_schema_url: file:///etc/config/kratos/identity.schema.json

courier:
  smtp:
    connection_uri: smtp://user:pwd@smtp.mailtrap.io:2525
    from_name: test
    from_address: office@test.com

watch-courier: true

log:
  level: debug
  format: text
  leak_sensitive_values: true

我的 Go rest api 有这些 cors 选项:

ALLOWED_ORIGINS=https://example.com
ALLOWED_METHODS=GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS
ALLOWED_HEADERS=Content-Type,Authorization,Cookie,Origin,X-Session-Token,X-CSRF-Token,Vary
EXPOSED_HEADERS=Content-Type,Authorization,Content-Length,Cache-Control,Content-Language,Content-Range,Set-Cookie,Pragma,Expires,Last-Modified,X-Session-Token,X-CSRF-Token
MAX_AGE=86400
ALLOW_CREDENTIALS=true

nginx 默认

upstream go-api {
    server 127.0.0.1:3001;
}

upstream kratos {
    server 127.0.0.1:4433;
}

upstream kratos-admin {
    server 127.0.0.1:4434;
}

server {
        server_name example.com www.example.com;

        location / {
                root /var/www/website;
                try_files $uri $uri/ /index.html;
        }
  
        location /api/ {
                 proxy_pass http://go-api;
                 proxy_http_version 1.1;
                 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 X-Forwarded-Port $server_port;
                 proxy_set_header x-forwarded-proto $scheme;
                 proxy_set_header Upgrade $http_upgrade;
                 proxy_set_header Connection 'upgrade';
                 proxy_cache_bypass $http_upgrade;
        }

        location /kratos/ {
                 proxy_pass http://kratos/;
                 proxy_http_version 1.1;
                 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 X-Forwarded-Port $server_port;
                 proxy_set_header x-forwarded-proto $scheme;
                 proxy_set_header Upgrade $http_upgrade;
                 proxy_set_header Connection 'upgrade';
                 proxy_cache_bypass $http_upgrade;
        }

       location /kratos-admin/ {
                 proxy_pass http://kratos-admin/;
                 proxy_http_version 1.1;
                 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 X-Forwarded-Port $server_port;
                 proxy_set_header x-forwarded-proto $scheme;
                 proxy_set_header Upgrade $http_upgrade;
                 proxy_set_header Connection 'upgrade';
                 proxy_cache_bypass $http_upgrade;
         }

    listen [::]:443 ssl ipv6only=on; # managed by Certbot
    listen 443 ssl; # managed by Certbot
    certs go here...
}

我不明白为什么它不能在实时服务器上运行,它必须是 ssl 的东西

这是我正在使用的 http 客户端(ky.js,但它与 fetch 无关)

const options = {
  prefixUrl: 'https://example.com/api/v1',
  headers: {
    'Content-Type': 'application/json; charset=UTF-8',
    Accept: 'application/json',
  },
  timeout: 5000,
  mode: 'cors',
  credentials: 'include',
};

export const apiClient = ky.create(options);

我只是调用受 AuthRequired 中间件保护的后端保护路由,没什么特别的:

function createTodo(data) {
  return apiClient.post(`todos`, { json: data }).json();
}

ory/kratos-client (js sdk) 配置如下:

const conf = new Configuration({
  basePath: 'https://example.com/kratos',
  // these are axios options (kratos js sdk uses axios under the hood)
  baseOptions: { 
    withCredentials: true,
    timeout: 5000,
  },
});

export const kratos = new PublicApi(conf);

奇怪的是,在 Firefox 中我在 devtools 面板中看到 2 个 cookie,但在 chrome 中却没有。

这是 csrf 之一:

aHR0cHM6Ly9hbmltb25kLnh5ei9rcmF0b3Mv_csrf_token:"Kx+PXWeoxsDNxQFGZBgvlTJScg9VIYEB+6cTrC0zsA0="
Created:"Thu, 27 May 2021 10:21:45 GMT"
Domain:".example.com"
Expires / Max-Age:"Fri, 27 May 2022 10:22:32 GMT"
HostOnly:false
HttpOnly:true
Last Accessed:"Thu, 27 May 2021 10:22:32 GMT"
Path:"/kratos/"
SameSite:"None"
Secure:true
Size: 91

这是会话 cookie:

ory_kratos_session:"MTYyMjExMDk1MnxEdi1CQkFFQ180SUFBUkFCRUFBQVJfLUNBQUVHYzNSeWFXNW5EQThBRFhObGMzTnBiMjVmZEc5clpXNEdjM1J5YVc1bkRDSUFJRFZYV25Jd05HaEpTR28xVHpaT1kzTXlSSGxxVHpaaWQyUTVRamhIY2paM3zb24EtkN6Bmv_lRZa7YSRBOYvUGYSUBmZ7RIkDsm4Oyw=="
Created:"Thu, 27 May 2021 10:22:32 GMT"
Domain:".example.com"
Expires / Max-Age:"Thu, 08 Jul 2021 01:22:32 GMT"
HostOnly:false
HttpOnly:true
Last Accessed:"Thu, 27 May 2021 10:22:32 GMT"
Path:"/"
SameSite:"Lax"
Secure:true
Size:234

我认为这与容器中的时区有关,我还在所有容器中都安装了这个卷:-v /etc/localtime:/etc/localtime:ro

PS

问题是,每当我docker-compose restart kratos 时,事情就会被破坏,不知何故显然正在使用旧的 csrf_token。 这应该如何使用,我不能只是告诉我的用户他们去你的浏览器并删除所有缓存和 cookie。 当我修剪它的所有工作时,但是一旦我重新启动 nginx 并且之后它就没有工作(在 docker-compose 重新启动之后也是如此)......非常奇怪

这家伙在这里遇到了同样的问题:csrf problem after restart

【问题讨论】:

  • 我真的认为您没有提供足够的信息来获得答案。我们知道发送到 API 的请求没有 cookie,因为您已经提供了请求标头。显然,如果没有那个 cookie,它就会失败。但是您还没有提供 设置 cookie 的代码,还没有显示请求是如何创建的,等等。
  • cookie 由 kratos 身份验证服务器在成功流程(注册/登录等)后设置
  • 在 kratos 配置中我看到 example.xyz 但在 apiClient 它是 example.com - 他们真的在不同的域上还是只是一个错字?
  • @dave 啊抱歉打错了...我会改正的

标签: docker go nginx cookies cross-domain


【解决方案1】:

我认为您的 kratos 配置不正确。 属性serve.public.base_url 应该是请求的来源,例如https://example.com/kratos/ 而不是你的本地主机 http://127.0.0.1:4433

也只是一个建议,你的管理端点不应该暴露给公众,你的后端服务应该请求内部网络上的管理 url(例如在 docker 内部或本地主机上)。您的 serve.admin.base_url 应该改为 http://127.0.0.1:4434 并从 nginx 中删除。

nginx 配置对我来说似乎是正确的。我相信您只需要它就可以工作:

location /kratos/ {
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_pass http://127.0.0.1:4433;
}

【讨论】:

    【解决方案2】:

    我确实设法解决了这个问题,它在生产中没有--dev 标志也可以正常工作,即使在重新启动服务并更改所有内容后也不会搞砸。

    也许它甚至是我的反应形式,我使用 defaultValue 作为 csrf 令牌输入

    现在每当 window.location url 改变或每当 csrf_token 改变时,它应该使用 csrf_token 的最新值

    const [csrf, setCsrf] = React.useState('');
    
    useEffect(() => {
      if (flowResponse !== null) {
        const csrfVal = flowResponse?.ui?.nodes?.find?.(n => n.attributes.name === 'csrf_token')?.attributes.value;
        setCsrf(csrfVal);
      }
    }, [flowResponse, csrf]);
    
    <input type='hidden' name='csrf_token' value={csrf} readOnly required />
    

    最糟糕的是,它也可能是一个尾随斜线或一些很小的东西,以至于我不确定是什么原因造成的。

    在这里发布所有对我有用的配置:

    可能是我之前尝试过将这个 kratos url 设置为 http://127.0.0.1:4433 或 http://kratos:4433 并且它不起作用(即使我在这 3 个哈哈之间切换)

    初始化 kratos 客户端

    conf := kratos.NewConfiguration()
    conf.Servers[0].URL = "https://example.com/kratos/"
    kratosClient := kratos.NewAPIClient(conf)
    

    kratos.yml

    version: v0.6.2-alpha.1
    
    dsn: postgres://test:test@postgresd:5432/test?sslmode=disable&max_conns=20&max_idle_conns=4
    
    serve:
      public:
        base_url: https://example.com/kratos/
        cors:
          enabled: true
          debug: true
          allow_credentials: true
          options_passthrough: true
          max_age: 0
          allowed_origins:
            - https://example.com
          allowed_methods:
            - POST
            - GET
            - PUT
            - PATCH
            - DELETE
            - OPTIONS
          allowed_headers:
            - Authorization
            - Cookie
            - Origin
            - X-Session-Token
          exposed_headers:
            - Content-Type
            - Set-Cookie
      admin:
        base_url: http://127.0.0.1:4434/
    
    selfservice:
      default_browser_return_url: https://example.com
      whitelisted_return_urls:
        - https://example.com
        - https://example.com/dashboard
        - https://example.com/auth/login
      methods:
        password:
          enabled: true
        oidc:
          enabled: false
        link:
          enabled: true
        profile:
          enabled: true
      flows:
        error:
          ui_url: https://example.com/error
        settings:
          ui_url: https://example.com/dashboard/profile
          privileged_session_max_age: 15m
        recovery:
          enabled: true
          ui_url: https://example.com/auth/recovery
          after:
            default_browser_return_url: https://example.com/auth/login
        verification:
          enabled: true
          ui_url: https://example.com/auth/verification
          after:
            default_browser_return_url: https://example.com
        logout:
          after:
            default_browser_return_url: https://example.com
        login:
          ui_url: https://example.com/auth/login
          lifespan: 10m
        registration:
          lifespan: 10m
          ui_url: https://example.com/auth/registration
          after:
            password:
              hooks:
                - hook: session
              default_browser_return_url: https://example.com/auth/login
            default_browser_return_url: https://example.com/auth/login
            oidc:
              hooks:
                - hook: session
    
    secrets:
      cookie:
        - veRy_S3cRet_tHinG
    
    session:
      lifespan: 24h
      cookie:
        domain: example.com
        same_site: Lax
        path: /           
    // <- i didn't have path before, not sure if it changes anything but it works (before csrf cookie had path /kratos and now when it works it has path /, same as session_cookie)
    
    hashers:
      argon2:
        parallelism: 1
        memory: 128MB
        iterations: 1
        salt_length: 16
        key_length: 16
    
    identity:
      default_schema_url: file:///etc/config/kratos/identity.schema.json
    
    courier:
      smtp:
        connection_uri: smtp://user:pwd@smtp.mailtrap.io:2525
        from_name: example
        from_address: office@example.cpm
    
    watch-courier: true
    
    log:
      level: debug
      format: text
      leak_sensitive_values: true
    

    nginx.conf

    server {
        server_name example.com www.example.com;
    
        location / {
           root /var/www/public;
           try_files $uri $uri/ /index.html;
        }
    
        location /api/ {
          proxy_pass http://127.0.0.1:3001; // backend api url
          proxy_http_version 1.1;
          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 X-Forwarded-Port $server_port;
          proxy_set_header x-forwarded-proto $scheme;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection 'upgrade';
          proxy_cache_bypass $http_upgrade;
        }
    
        location /kratos/ {
          proxy_pass http://127.0.0.1:4433/;  // kratos public url
          proxy_http_version 1.1;
          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 X-Forwarded-Port $server_port;
          proxy_set_header x-forwarded-proto $scheme;
          proxy_set_header Upgrade $http_upgrade;
          proxy_set_header Connection 'upgrade';
          proxy_cache_bypass $http_upgrade;
        }
           
        listen [::]:443 ssl ipv6only=on; # managed by Certbot
        listen 443 ssl; # managed by Certbot
        certs...
    }
    
    server {
        if ($host = www.example.com) {
            return 301 https://$host$request_uri;
        } # managed by Certbot
    
        if ($host = example.com) {
            return 301 https://$host$request_uri;
        } # managed by Certbot
    
       listen 80 default_server;
       listen [::]:80 default_server;
       server_name example.com www.example.com;
       return 404; # managed by Certbot
    }
    

    【讨论】:

      猜你喜欢
      • 2021-04-01
      • 2023-01-16
      • 2023-02-01
      • 2022-11-06
      • 2021-12-30
      • 2016-02-21
      • 2021-04-06
      • 1970-01-01
      • 2015-09-25
      相关资源
      最近更新 更多