| package passkey |
|
|
| import ( |
| "errors" |
| "fmt" |
| "net" |
| "net/http" |
| "net/url" |
| "strings" |
| "time" |
|
|
| "github.com/QuantumNous/new-api/common" |
| "github.com/QuantumNous/new-api/setting/system_setting" |
|
|
| "github.com/go-webauthn/webauthn/protocol" |
| webauthn "github.com/go-webauthn/webauthn/webauthn" |
| ) |
|
|
| const ( |
| RegistrationSessionKey = "passkey_registration_session" |
| LoginSessionKey = "passkey_login_session" |
| VerifySessionKey = "passkey_verify_session" |
| ) |
|
|
| |
| func BuildWebAuthn(r *http.Request) (*webauthn.WebAuthn, error) { |
| settings := system_setting.GetPasskeySettings() |
| if settings == nil { |
| return nil, errors.New("未找到 Passkey 设置") |
| } |
|
|
| displayName := strings.TrimSpace(settings.RPDisplayName) |
| if displayName == "" { |
| displayName = common.SystemName |
| } |
|
|
| origins, err := resolveOrigins(r, settings) |
| if err != nil { |
| return nil, err |
| } |
|
|
| rpID, err := resolveRPID(r, settings, origins) |
| if err != nil { |
| return nil, err |
| } |
|
|
| selection := protocol.AuthenticatorSelection{ |
| ResidentKey: protocol.ResidentKeyRequirementRequired, |
| RequireResidentKey: protocol.ResidentKeyRequired(), |
| UserVerification: protocol.UserVerificationRequirement(settings.UserVerification), |
| } |
| if selection.UserVerification == "" { |
| selection.UserVerification = protocol.VerificationPreferred |
| } |
| if attachment := strings.TrimSpace(settings.AttachmentPreference); attachment != "" { |
| selection.AuthenticatorAttachment = protocol.AuthenticatorAttachment(attachment) |
| } |
|
|
| config := &webauthn.Config{ |
| RPID: rpID, |
| RPDisplayName: displayName, |
| RPOrigins: origins, |
| AuthenticatorSelection: selection, |
| Debug: common.DebugEnabled, |
| Timeouts: webauthn.TimeoutsConfig{ |
| Login: webauthn.TimeoutConfig{ |
| Enforce: true, |
| Timeout: 2 * time.Minute, |
| TimeoutUVD: 2 * time.Minute, |
| }, |
| Registration: webauthn.TimeoutConfig{ |
| Enforce: true, |
| Timeout: 2 * time.Minute, |
| TimeoutUVD: 2 * time.Minute, |
| }, |
| }, |
| } |
|
|
| return webauthn.New(config) |
| } |
|
|
| func resolveOrigins(r *http.Request, settings *system_setting.PasskeySettings) ([]string, error) { |
| originsStr := strings.TrimSpace(settings.Origins) |
| if originsStr != "" { |
| originList := strings.Split(originsStr, ",") |
| origins := make([]string, 0, len(originList)) |
| for _, origin := range originList { |
| trimmed := strings.TrimSpace(origin) |
| if trimmed == "" { |
| continue |
| } |
| if !settings.AllowInsecureOrigin && strings.HasPrefix(strings.ToLower(trimmed), "http://") { |
| return nil, fmt.Errorf("Passkey 不允许使用不安全的 Origin: %s", trimmed) |
| } |
| origins = append(origins, trimmed) |
| } |
| if len(origins) == 0 { |
| |
| goto autoDetect |
| } |
| return origins, nil |
| } |
|
|
| autoDetect: |
| scheme := detectScheme(r) |
| if scheme == "http" && !settings.AllowInsecureOrigin && r.Host != "localhost" && r.Host != "127.0.0.1" && !strings.HasPrefix(r.Host, "127.0.0.1:") && !strings.HasPrefix(r.Host, "localhost:") { |
| return nil, fmt.Errorf("Passkey 仅支持 HTTPS,当前访问: %s://%s,请在 Passkey 设置中允许不安全 Origin 或配置 HTTPS", scheme, r.Host) |
| } |
| |
| host := r.Host |
|
|
| |
| if host == "" && system_setting.ServerAddress != "" { |
| if parsed, err := url.Parse(system_setting.ServerAddress); err == nil && parsed.Host != "" { |
| host = parsed.Host |
| if scheme == "" && parsed.Scheme != "" { |
| scheme = parsed.Scheme |
| } |
| } |
| } |
| if host == "" { |
| return nil, fmt.Errorf("无法确定 Passkey 的 Origin,请在系统设置或 Passkey 设置中指定。当前 Host: '%s', ServerAddress: '%s'", r.Host, system_setting.ServerAddress) |
| } |
| if scheme == "" { |
| scheme = "https" |
| } |
| origin := fmt.Sprintf("%s://%s", scheme, host) |
| return []string{origin}, nil |
| } |
|
|
| func resolveRPID(r *http.Request, settings *system_setting.PasskeySettings, origins []string) (string, error) { |
| rpID := strings.TrimSpace(settings.RPID) |
| if rpID != "" { |
| return hostWithoutPort(rpID), nil |
| } |
| if len(origins) == 0 { |
| return "", errors.New("Passkey 未配置 Origin,无法推导 RPID") |
| } |
| parsed, err := url.Parse(origins[0]) |
| if err != nil { |
| return "", fmt.Errorf("无法解析 Passkey Origin: %w", err) |
| } |
| return hostWithoutPort(parsed.Host), nil |
| } |
|
|
| func hostWithoutPort(host string) string { |
| host = strings.TrimSpace(host) |
| if host == "" { |
| return "" |
| } |
| if strings.Contains(host, ":") { |
| if host, _, err := net.SplitHostPort(host); err == nil { |
| return host |
| } |
| } |
| return host |
| } |
|
|
| func detectScheme(r *http.Request) string { |
| if r == nil { |
| return "" |
| } |
| if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" { |
| parts := strings.Split(proto, ",") |
| return strings.ToLower(strings.TrimSpace(parts[0])) |
| } |
| if r.TLS != nil { |
| return "https" |
| } |
| if r.URL != nil && r.URL.Scheme != "" { |
| return strings.ToLower(r.URL.Scheme) |
| } |
| if r.Header.Get("X-Forwarded-Protocol") != "" { |
| return strings.ToLower(strings.TrimSpace(r.Header.Get("X-Forwarded-Protocol"))) |
| } |
| return "http" |
| } |
|
|