Allow explicit local WebAuthn ports
verify / verify (push) Successful in 3m35s

This commit is contained in:
2026-09-03 22:30:23 -04:00
parent d8b09c8ae5
commit 17bd9453e2
8 changed files with 63 additions and 11 deletions
+25 -6
View File
@@ -12,8 +12,10 @@ import (
"errors"
"fmt"
"io"
"net"
"net/url"
"regexp"
"strconv"
"strings"
"time"
@@ -36,9 +38,14 @@ const (
var accountNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9_.-]{2,63}$`)
type Config struct {
RPID string
RPDisplayName string
Origin string
RPID string
RPDisplayName string
Origin string
// AllowDevelopmentPort permits an explicit non-default HTTPS port only
// for localhost or a reserved .test relying-party ID. Production origins
// remain portless, while local applications can terminate trusted HTTPS
// without requiring a privileged listener.
AllowDevelopmentPort bool
EnrollmentLifetime time.Duration
RegistrationTTL time.Duration
LoginTTL time.Duration
@@ -66,7 +73,7 @@ func New(repository Repository, authService *auth.Service, config Config) (*Serv
if repository == nil || authService == nil {
return nil, errors.New("authwebauthn: repository and auth service are required")
}
if err := validateOrigin(config.RPID, config.Origin); err != nil {
if err := validateOrigin(config.RPID, config.Origin, config.AllowDevelopmentPort); err != nil {
return nil, err
}
if strings.TrimSpace(config.RPDisplayName) == "" || len(config.RPDisplayName) > 80 {
@@ -725,12 +732,24 @@ func passwordMigrationBinding(userID string) [32]byte {
return BindingDigest([]byte("gamertan-web/password-to-passkey/v1\x00" + userID))
}
func validateOrigin(rpID, rawOrigin string) error {
func validateOrigin(rpID, rawOrigin string, allowDevelopmentPort bool) error {
if strings.TrimSpace(rpID) == "" || strings.TrimSpace(rawOrigin) == "" {
return errors.New("authwebauthn: relying-party ID and origin are required")
}
origin, err := url.Parse(rawOrigin)
if err != nil || origin.Scheme != "https" || origin.Hostname() != rpID || origin.Port() != "" || origin.User != nil || origin.Path != "" || origin.RawQuery != "" || origin.Fragment != "" {
if err != nil || origin.Scheme != "https" || origin.Hostname() != rpID || origin.User != nil || origin.Path != "" || origin.RawQuery != "" || origin.Fragment != "" {
return errors.New("authwebauthn: origin must be the exact HTTPS relying-party origin")
}
port := origin.Port()
if port == "" {
if origin.Host != rpID {
return errors.New("authwebauthn: origin must be the exact HTTPS relying-party origin")
}
return nil
}
developmentRP := rpID == "localhost" || strings.HasSuffix(rpID, ".test")
value, portErr := strconv.ParseUint(port, 10, 16)
if !allowDevelopmentPort || !developmentRP || portErr != nil || value == 0 || value == 443 || strconv.FormatUint(value, 10) != port || origin.Host != net.JoinHostPort(rpID, port) {
return errors.New("authwebauthn: origin must be the exact HTTPS relying-party origin")
}
return nil
+14
View File
@@ -231,11 +231,25 @@ func TestConfigurationAndEntropyFailures(t *testing.T) {
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "http://tend.gamertan.com"},
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://other.gamertan.com"},
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://tend.gamertan.com/path"},
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:8443"},
{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://tend.gamertan.com:8443", AllowDevelopmentPort: true},
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:443", AllowDevelopmentPort: true},
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:08443", AllowDevelopmentPort: true},
{RPID: "localhost", RPDisplayName: "Tend", Origin: "https://localhost:0", AllowDevelopmentPort: true},
} {
if _, err = authwebauthn.New(store, authService, config); err == nil {
t.Fatalf("accepted config=%+v", config)
}
}
for _, config := range []authwebauthn.Config{
{RPID: "localhost", RPDisplayName: "Tend Local", Origin: "https://localhost:8443", AllowDevelopmentPort: true},
{RPID: "tend.test", RPDisplayName: "Tend Local", Origin: "https://tend.test:8443", AllowDevelopmentPort: true},
} {
configured, configureErr := authwebauthn.New(store, authService, config)
if configureErr != nil || configured == nil {
t.Fatalf("development config=%+v service=%v err=%v", config, configured, configureErr)
}
}
service, err := authwebauthn.New(store, authService, authwebauthn.Config{RPID: "tend.gamertan.com", RPDisplayName: "Tend", Origin: "https://tend.gamertan.com", Random: failingReader{}})
if err != nil {
t.Fatal(err)