fixed passkey

This commit is contained in:
Linrador 2026-06-23 20:41:04 +02:00
parent df52f94868
commit 724fbf52c9
8 changed files with 137 additions and 10 deletions

View File

@ -112,8 +112,17 @@ func authAllowedRPOrigins() []string {
out = append(out, origin) out = append(out, origin)
} }
for _, origin := range configuredPublicAppOrigins() {
addOrigin(origin)
}
scheme := "https"
if !httpsEnabled() {
scheme = "http"
}
for _, host := range localAppHostCandidates() { for _, host := range localAppHostCandidates() {
addOrigin(originForHost("https", host, appPort())) addOrigin(originForHost(scheme, host, appPort()))
} }
// Explicit env values remain supported, but they extend the dynamic list // Explicit env values remain supported, but they extend the dynamic list
@ -125,6 +134,33 @@ func authAllowedRPOrigins() []string {
return out return out
} }
func configuredPublicAppOrigins() []string {
keys := []string{
"APP_PUBLIC_URLS",
"APP_PUBLIC_URL",
"PUBLIC_APP_URLS",
"PUBLIC_APP_URL",
"NSFWAPP_URL",
}
out := make([]string, 0, len(keys))
for _, key := range keys {
raw := strings.TrimSpace(os.Getenv(key))
if raw == "" {
continue
}
for _, part := range strings.Split(raw, ",") {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
}
return out
}
func configuredAuthOrigins() []string { func configuredAuthOrigins() []string {
raw := strings.TrimSpace(os.Getenv("AUTH_RP_ORIGINS")) raw := strings.TrimSpace(os.Getenv("AUTH_RP_ORIGINS"))
@ -381,6 +417,57 @@ func originIsAllowed(origin string, allowed []string) bool {
return false return false
} }
func requestHostOriginAllowed(origin string, r *http.Request) bool {
normalizedOrigin, ok := canonicalOrigin(origin)
if !ok {
return false
}
u, err := url.Parse(normalizedOrigin)
if err != nil {
return false
}
originHost := normalizeOriginHost(u.Hostname())
requestHost, requestPort := splitRequestHostPort(r.Host)
if originHost == "" || requestHost == "" || originHost != requestHost {
return false
}
if strings.TrimSpace(u.Port()) != requestPort {
return false
}
if u.Scheme == "https" {
return true
}
return u.Scheme == "http" && isLocalWebAuthnHost(originHost)
}
func splitRequestHostPort(host string) (string, string) {
host = strings.TrimSpace(host)
if host == "" {
return "", ""
}
if h, p, err := net.SplitHostPort(host); err == nil {
return normalizeOriginHost(h), strings.TrimSpace(p)
}
return normalizeOriginHost(host), ""
}
func isLocalWebAuthnHost(host string) bool {
host = normalizeOriginHost(host)
if host == "localhost" {
return true
}
ip := net.ParseIP(host)
return ip != nil && ip.IsLoopback()
}
func (am *AuthManager) webAuthnForRequest(r *http.Request) (*webauthn.WebAuthn, error) { func (am *AuthManager) webAuthnForRequest(r *http.Request) (*webauthn.WebAuthn, error) {
origin := originFromRequest(r) origin := originFromRequest(r)
if origin == "" { if origin == "" {
@ -388,7 +475,7 @@ func (am *AuthManager) webAuthnForRequest(r *http.Request) (*webauthn.WebAuthn,
} }
allowedOrigins := authAllowedRPOrigins() allowedOrigins := authAllowedRPOrigins()
if !originIsAllowed(origin, allowedOrigins) { if !originIsAllowed(origin, allowedOrigins) && !requestHostOriginAllowed(origin, r) {
return nil, errors.New("origin not allowed for WebAuthn: " + origin) return nil, errors.New("origin not allowed for WebAuthn: " + origin)
} }

View File

@ -0,0 +1,32 @@
package main
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestRequestHostOriginAllowedAcceptsHTTPSFQDN(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "https://l14pbbk95100006.tegdssd.de:9999/api/auth/passkey/login/options", nil)
if !requestHostOriginAllowed("https://l14pbbk95100006.tegdssd.de:9999", r) {
t.Fatal("expected matching HTTPS request host origin to be allowed")
}
}
func TestRequestHostOriginAllowedRejectsDifferentHost(t *testing.T) {
r := httptest.NewRequest(http.MethodPost, "https://l14pbbk95100006.tegdssd.de:9999/api/auth/passkey/login/options", nil)
if requestHostOriginAllowed("https://evil.example:9999", r) {
t.Fatal("expected different origin host to be rejected")
}
}
func TestAuthAllowedRPOriginsIncludesConfiguredPublicURL(t *testing.T) {
t.Setenv("APP_PUBLIC_URL", "https://l14pbbk95100006.tegdssd.de:9999")
origins := authAllowedRPOrigins()
if !originIsAllowed("https://l14pbbk95100006.tegdssd.de:9999", origins) {
t.Fatalf("expected configured public URL in allowed origins: %#v", origins)
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -941,15 +941,23 @@ func publicAppURLs() []string {
out := make([]string, 0, 8) out := make([]string, 0, 8)
seen := map[string]bool{} seen := map[string]bool{}
for _, host := range localAppHostCandidates() { addURL := func(raw string) {
u := originForHost(scheme, host, port) u, ok := canonicalOrigin(raw)
if u == "" || seen[u] { if !ok || seen[u] {
continue return
} }
seen[u] = true seen[u] = true
out = append(out, u) out = append(out, u)
} }
for _, u := range configuredPublicAppOrigins() {
addURL(u)
}
for _, host := range localAppHostCandidates() {
addURL(originForHost(scheme, host, port))
}
return out return out
} }

View File

@ -115,7 +115,7 @@ export default function LoginPage({ onLoggedIn }: Props) {
if (raw.includes('passkey registration verify failed')) { if (raw.includes('passkey registration verify failed')) {
return ( return (
raw + raw +
'\n\nPrüfe AUTH_RP_ORIGIN und AUTH_RP_ID. Die Werte müssen exakt zur Browser-URL passen.' '\n\nPruefe, ob du den Passkey unter genau dieser Browser-URL registriert hast. Passkeys sind an Hostname und RP-ID gebunden.'
) )
} }
@ -135,7 +135,7 @@ export default function LoginPage({ onLoggedIn }: Props) {
) { ) {
return ( return (
raw + raw +
'\n\nDas sieht nach einem Origin/RP-ID Problem aus. Prüfe AUTH_RP_ORIGIN und AUTH_RP_ID.' '\n\nDas sieht nach einem Origin/RP-ID Problem aus. Pruefe, ob Hostname, HTTPS und die Passkey-Registrierung zusammenpassen.'
) )
} }

View File

@ -1891,7 +1891,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
if (raw.includes('passkey registration verify failed')) { if (raw.includes('passkey registration verify failed')) {
return ( return (
raw + raw +
'\n\nPrüfe AUTH_RP_ORIGIN und AUTH_RP_ID. Die Werte müssen exakt zur Browser-URL passen.' '\n\nPruefe, ob du den Passkey unter genau dieser Browser-URL registriert hast. Passkeys sind an Hostname und RP-ID gebunden.'
) )
} }
@ -1911,7 +1911,7 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
) { ) {
return ( return (
raw + raw +
'\n\nDas sieht nach einem Origin/RP-ID Problem aus. Prüfe AUTH_RP_ORIGIN und AUTH_RP_ID.' '\n\nDas sieht nach einem Origin/RP-ID Problem aus. Pruefe, ob Hostname, HTTPS und die Passkey-Registrierung zusammenpassen.'
) )
} }