브라우저에서 사용자 패스키 생성 시 오류 발생

자신의 사이트에서 패스키를 생성하려고 하면 다음과 같은 알림이 표시됩니다.

The passkey registration process either timed out, was cancelled or is not allowed

하지만 같은 브라우저(Microsoft Edge)와 같은 플러그인(Apple Passkey)을 사용해도 Discourse 메타 포럼에서는 패스키를 정상적으로 생성할 수 있습니다.

Discourse를 최신 버전으로 업그레이드했지만, 이 게시물처럼 작동하지 않습니다.

안녕하세요, 해당 메시지가 다이얼로그에 표시될 때 콘솔 오류가 표시되는 것 같습니다.

브라우저 콘솔을 열어보시고, 보실 수 있는 오류가 있다면 공유해 주시겠습니까?


로그에 이 오류에 대한 정보가 없는 것 같습니다

사이트의 /logs가 아니라 브라우저 콘솔 로그를 확인해야 합니다.

이게 맞나요?

forum.beginner.center/:1  Mixed Content: The page at 'https://forum.beginner.center/' was loaded over HTTPS, but requested an insecure font 'http://forum.beginner.center/fonts/JetBrainsMono-Regular.woff2?v=0.0.19'. This request has been blocked; the content must be served over HTTPS.
forum.beginner.center/:1  Mixed Content: The page at 'https://forum.beginner.center/' was loaded over HTTPS, but requested an insecure font 'http://forum.beginner.center/fonts/JetBrainsMono-Bold.woff2?v=0.0.19'. This request has been blocked; the content must be served over HTTPS.
app.js:270 ℹ️ Discourse v3.5.0.beta9-dev — https://github.com/discourse/discourse/commits/33dfd7dba9 — Ember v5.12.0
[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'nonce-4YvvTZffYuqGaENC8DnQ7yeNg' 'strict-dynamic'".

analytics.eu.umami.is/script.js:1   Failed to load resource: net::ERR_CONNECTION_CLOSED
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
Tracking Prevention blocked access to storage for <URL>.
deprecated.js:62  Deprecation notice: Setting timezone property of user object is deprecated. Use user_option object instead [deprecated since Discourse 2.9.0.beta12] [removal in Discourse 3.0.0.beta1] [deprecation id: discourse.user.userOptions]
a @ deprecated.js:62
security:1 Autofocus processing was blocked because a document already has a focused element.
completion_list.html:14   GET chrome-extension://mfbcdcnpokpoajjciilocoachedjkima/heuristicsRedefinitions.js net::ERR_FILE_NOT_FOUND
completion_list.html:13   GET chrome-extension://mfbcdcnpokpoajjciilocoachedjkima/extensionState.js net::ERR_FILE_NOT_FOUND
completion_list.html:12   GET chrome-extension://mfbcdcnpokpoajjciilocoachedjkima/utils.js net::ERR_FILE_NOT_FOUND
ajax.js:188   POST https://forum.beginner.center/u/register_passkey.json 401 (Unauthorized)
send @ jquery.js:9940
ajax @ jquery.js:9521
o @ ajax.js:188
(匿名) @ rsvp-DaQAFb0W.js:435
e @ rsvp-DaQAFb0W.js:451
A @ ajax.js:201
registerPasskey @ user.js:650
createPasskey @ user-passkeys.gjs:86
await in createPasskey
didConfirm @ user-passkeys.gjs:140
didConfirmWrapped @ dialog.js:134
_join @ index.js:788
join @ index.js:605
p @ index.js:152
(匿名) @ index.js:250
submit @ confirm-session.gjs:84
await in submit
(匿名) @ d-button.gjs:138
invoke @ index.js:264
flush @ index.js:180
flush @ index.js:334
_end @ index.js:762
end @ index.js:565
_runExpiredTimers @ index.js:869
setTimeout
setTimeout @ index.js:39
_installTimerTimeout @ index.js:912
_later @ index.js:823
later @ index.js:652
T @ index.js:562
_triggerAction @ d-button.gjs:135
click @ d-button.gjs:93
user-passkeys.gjs:104  {jqXHR: {…}, textStatus: 'error', errorThrown: ''}errorThrown: ""jqXHR: abort: ƒ (e)always: ƒ ()catch: ƒ (e)done: ƒ (e)fail: ƒ (e)getAllResponseHeaders: ƒ ()getResponseHeader: ƒ (e)jqTextStatus: "error"overrideMimeType: ƒ (e)pipe: ƒ ()progress: ƒ (e)promise: ƒ (e)readyState: 4requestedUrl: "/u/register_passkey.json"responseJSON: {errors: Array(1)}responseText: "{\"errors\":[\"The origin of the authentication request does not match the server origin.\"]}"setRequestHeader: ƒ (e,t)state: ƒ ()status: 401statusCode: ƒ (e)statusText: "error"then: ƒ (e,i,n)[[Prototype]]: ObjecttextStatus: "error"[[Prototype]]: Objectconstructor: ƒ Object()hasOwnProperty: ƒ hasOwnProperty()isPrototypeOf: ƒ isPrototypeOf()propertyIsEnumerable: ƒ propertyIsEnumerable()toLocaleString: ƒ toLocaleString()toString: ƒ toString()valueOf: ƒ valueOf()__defineGetter__: ƒ __defineGetter__()__defineSetter__: ƒ __defineSetter__()__lookupGetter__: ƒ __lookupGetter__()__lookupSetter__: ƒ __lookupSetter__()__proto__: (...)get __proto__: ƒ __proto__()set __proto__: ƒ __proto__()
createPasskey @ user-passkeys.gjs:104
await in createPasskey
didConfirm @ user-passkeys.gjs:140
didConfirmWrapped @ dialog.js:134
_join @ index.js:788
join @ index.js:605
p @ index.js:152
(匿名) @ index.js:250
submit @ confirm-session.gjs:84
await in submit
(匿名) @ d-button.gjs:138
invoke @ index.js:264
flush @ index.js:180
flush @ index.js:334
_end @ index.js:762
end @ index.js:565
_runExpiredTimers @ index.js:869
setTimeout
setTimeout @ index.js:39
_installTimerTimeout @ index.js:912
_later @ index.js:823
later @ index.js:652
T @ index.js:562
_triggerAction @ d-button.gjs:135
click @ d-button.gjs:93

오, 이건 유용한 정보네요. 하지만 401 오류는 수많은 이유로 발생할 수 있습니다. 저희 내부 패스키 전문가와 확인해 보겠습니다.

사이트가 모든 요청을 https로 처리하도록 구성되어 있는지 확인해 주세요. 패스키 챌린지 검증은 모든 요청이 https를 통해 이루어져야 합니다. 또한 브라우저와 서버 사이의 도메인이 정확히 일치해야 합니다. 어딘가 불일치가 있으면 검증이 실패합니다.

이를 위한 설정인 force_https가 있습니다. 시도해 볼 수 있으며 도움이 될 수 있습니다(다만 서버가 올바르게 구성되지 않은 경우 접근이 차단될 수도 있으니 주의하세요).

이것은 제 app.yml입니다.
OpenResty(nginx 기반)를 리버스 프록시로 사용 중입니다.

expose:
  - "6180:80"   # http
  - "6443:443" # https
  - "587:587"

https 포트 6443으로 제 사이트를 사용할 수 없습니다.

image

그래서 http 리버스 프록시만 설정했습니다.

이것이 제 OpenResty 설정입니다.

server {
    listen 80 ; 
    listen 443 ssl http2 ; 
    server_name forum.beginner.center; 
    index index.php index.html index.htm default.php default.htm default.html; 
    proxy_set_header Host $host; 
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 
    proxy_set_header X-Forwarded-Host $server_name; 
    proxy_set_header X-Real-IP $remote_addr; 
    proxy_http_version 1.1; 
    proxy_set_header Upgrade $http_upgrade; 
    proxy_set_header Connection $http_connection; 
    access_log /www/sites/forum.beginner.center/log/access.log main; 
    error_log /www/sites/forum.beginner.center/log/error.log; 
    location ^~ /.well-known/acme-challenge {
        allow all; 
        root /usr/share/nginx/html; 
    }
    if ($scheme = http) {
        return 301 https://$host$request_uri; 
    }
    ssl_certificate /www/sites/forum.beginner.center/ssl/fullchain.pem; 
    ssl_certificate_key /www/sites/forum.beginner.center/ssl/privkey.pem; 
    ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1; 
    ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:!aNULL:!eNULL:!EXPORT:!DSS:!DES:!RC4:!3DES:!MD5:!PSK:!KRB5:!SRP:!CAMELLIA:!SEED; 
    ssl_prefer_server_ciphers on; 
    ssl_session_cache shared:SSL:10m; 
    ssl_session_timeout 10m; 
    error_page 497 https://$host$request_uri; 
    proxy_set_header X-Forwarded-Proto https; 
    add_header Strict-Transport-Security "max-age=31536000"; 
    include /www/sites/forum.beginner.center/proxy/*.conf; 
}

이것은 리버스 프록시 설정입니다.

location ^~ / {
    proxy_pass http://127.0.0.1:6180; 
    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 $http_connection; 
    proxy_set_header X-Forwarded-Proto $scheme; 
    proxy_http_version 1.1; 
    add_header X-Cache $upstream_cache_status; 
    add_header Cache-Control no-cache; 
    proxy_ssl_server_name off; 
    proxy_ssl_name $proxy_host; 
    add_header Strict-Transport-Security "max-age=31536000"; 
}

안녕하세요, 도와주실 분 없나요?

지연된 답변에 대해 죄송합니다. 이 문제는 실제로 프록시와 관련이 있습니다. 정확히 어떤 부분인지 단정할 수는 없지만, 도메인 이름, 프로토콜(http 또는 https), 포트 중 하나가 문제를 일으키고 있습니다.

패스키는 프론트엔드와 백엔드가 동일한 도메인, 프로토콜, 포트에서 실행되는지 검증합니다. 이 중 하나라도 일치하지 않으면 이러한 오류가 발생합니다.

Rails CLI에서 다음을 시도해 볼 수 있나요?

DiscourseWebauthn.origin

그리고 브라우저에서 사이트를 접근할 때 사용하는 URL과 비교해 보세요. 두 값은 일치해야 합니다.

force https를 활성화했을 때 사용자 패스키 기능이 작동했습니다.

참고

Passkey의 Webauthn 표준을 제가 이해한 바로는, Relying Party(Discourse)와 Client(브라우저 또는 모바일 기기), 그리고 Authenticator(예: yubikey) 간의 안전한 연결에 의존합니다. 따라서 Discourse 애플리케이션에서 발생하는 통신에는 https가 필요합니다. https를 강제하는 것이 해결책이 될 수 있지만, 단순히 헤더를 추가하는 것만으로도 충분할 수 있습니다.

proxy_set_header X-Forwarded-Proto https;

https 강제 적용이 도움이 된다면(어쨌든 권장됩니다) 모든 것이 정상입니다.

i don’t know how to config proxy_set_header X-Forwarded-Proto

in app.yaml?