Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions internal/verifier/apiv1/handler_client_registration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
26 changes: 4 additions & 22 deletions pkg/helpers/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 == "" {
Expand All @@ -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 {
Expand Down