diff --git a/internal/verifier/apiv1/handler_client_registration_test.go b/internal/verifier/apiv1/handler_client_registration_test.go index 08eea6a11..5ef7514fe 100644 --- a/internal/verifier/apiv1/handler_client_registration_test.go +++ b/internal/verifier/apiv1/handler_client_registration_test.go @@ -186,6 +186,20 @@ func TestClientRegistrationRequest_Validation(t *testing.T) { }, wantErr: true, }, + { + name: "valid redirect URI with ccTLD", + req: ClientRegistrationRequest{ + RedirectURIs: []string{"https://example.se/callback"}, + }, + wantErr: false, + }, + { + name: "valid redirect URI with non-resolving hostname", + req: ClientRegistrationRequest{ + RedirectURIs: []string{"https://nonexistent.test/callback"}, + }, + wantErr: false, + }, { name: "valid token endpoint auth method - client_secret_basic", req: ClientRegistrationRequest{ diff --git a/pkg/helpers/validate.go b/pkg/helpers/validate.go index f1b653560..2e261bdfd 100644 --- a/pkg/helpers/validate.go +++ b/pkg/helpers/validate.go @@ -126,7 +126,10 @@ func NewValidator() (*validator.Validate, error) { // Used by OIDC dynamic client registration (RFC 7591) for redirect_uris. // Per RFC 6749: must have a scheme and must not contain a fragment. // Per RFC 8252 §7.3: loopback redirect URIs MUST be allowed for native clients. - // Non-loopback private IPs are blocked to prevent SSRF. + // No DNS resolution or SSRF check: redirect URIs are never fetched + // server-side — the browser follows them. Blocking unresolvable + // hostnames would reject valid registrations (e.g. any TLD not in the + // verifier's DNS). err = validate.RegisterValidation("redirect_uri", func(fl validator.FieldLevel) bool { urlStr := fl.Field().String() if urlStr == "" { @@ -151,27 +154,6 @@ func NewValidator() (*validator.Validate, error) { return false } - // Per RFC 8252 §7.3: loopback redirect URIs (localhost, 127.0.0.1, [::1]) - // MUST be allowed for native OAuth 2.0 clients. - lowerHost := strings.ToLower(hostname) - if lowerHost == "localhost" || lowerHost == "127.0.0.1" || lowerHost == "::1" { - return true - } - - // Resolve hostname and block private/loopback IPs - ips, err := net.LookupIP(hostname) - if err != nil { - // If we can't resolve, allow — it may be a custom scheme (e.g. eudi-wallet://) - // Custom schemes won't have a resolvable hostname - return parsedURL.Scheme != "http" && parsedURL.Scheme != "https" - } - - for _, ip := range ips { - if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsPrivate() { - return false - } - } - return true }) if err != nil {