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
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.mosip.esignet.core.constants.Constants;
import io.mosip.esignet.core.constants.ErrorConstants;
import io.mosip.esignet.core.exception.EsignetException;
import io.mosip.esignet.core.validator.RedirectURLValidator;
import org.apache.commons.codec.binary.Hex;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509CertificateHolder;
Expand All @@ -41,7 +42,6 @@
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;
import org.apache.commons.validator.routines.UrlValidator;
import org.jose4j.jwk.PublicJsonWebKey;
import org.jose4j.jwk.RsaJsonWebKey;
import org.jose4j.jwk.EllipticCurveJsonWebKey;
Expand All @@ -60,9 +60,6 @@

import jakarta.xml.bind.DatatypeConverter;

import static org.apache.commons.validator.routines.UrlValidator.ALLOW_ALL_SCHEMES;
import static org.apache.commons.validator.routines.UrlValidator.ALLOW_LOCAL_URLS;

@Slf4j
@Component
public class IdentityProviderUtil {
Expand All @@ -80,13 +77,13 @@ public class IdentityProviderUtil {
private static Base64.Encoder urlSafeEncoder;
private static Base64.Decoder urlSafeDecoder;
private static PathMatcher pathMatcher;
private static UrlValidator urlValidator;
private static RedirectURLValidator urlValidator;

static {
urlSafeEncoder = Base64.getUrlEncoder().withoutPadding();
urlSafeDecoder = Base64.getUrlDecoder();
pathMatcher = new AntPathMatcher();
urlValidator = new UrlValidator(ALLOW_ALL_SCHEMES+ALLOW_LOCAL_URLS);
urlValidator = new RedirectURLValidator();
}

/**
Expand Down Expand Up @@ -219,7 +216,7 @@ public static String generateRandomAlphaNumeric(int length) {
}

private static boolean matchUri(String registeredUri, String requestedUri) {
return (urlValidator.isValid(registeredUri) && urlValidator.isValid(requestedUri))
return (urlValidator.isValid(registeredUri, null) && urlValidator.isValid(requestedUri, null))
&& pathMatcher.match(registeredUri, requestedUri);
Comment thread
mbert marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,24 +1,64 @@
package io.mosip.esignet.core.validator;

import org.apache.commons.validator.routines.RegexValidator;
import org.apache.commons.validator.routines.UrlValidator;
import org.hibernate.validator.constraints.URL;
import static org.apache.commons.validator.routines.UrlValidator.ALLOW_ALL_SCHEMES;
import static org.apache.commons.validator.routines.UrlValidator.ALLOW_LOCAL_URLS;
import org.springframework.stereotype.Component;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

import static org.apache.commons.validator.routines.UrlValidator.ALLOW_ALL_SCHEMES;
import static org.apache.commons.validator.routines.UrlValidator.ALLOW_LOCAL_URLS;

/**
* @class RedirectURLValidator uses a customised Apache {@link UrlValidator}
* to check syntactical validity of redirect URLs, allowing any scheme and local URLs,
* but restricting the authority part to valid IPv4/IPv6 addresses, localhost, or domain
* names with any TLD.
*/
@Component
public class RedirectURLValidator implements ConstraintValidator<RedirectURL, String> {

private final UrlValidator urlValidator = new UrlValidator(ALLOW_ALL_SCHEMES+ALLOW_LOCAL_URLS);
// IPv6 address in brackets – strict RFC 4291 alternation covering all
// compressed (::) and full (8-group) forms; rejects bare garbage like [::::]
private static final String IPV6_REGEX =
"\\[(?:" +
"(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}" + // full 8-group, no ::
"|(?:[0-9a-fA-F]{1,4}:){1,7}:" + // trailing ::
"|(?:[0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}" + // 6+1 around ::
"|(?:[0-9a-fA-F]{1,4}:){1,5}(?::[0-9a-fA-F]{1,4}){1,2}" + // 5+1-2
"|(?:[0-9a-fA-F]{1,4}:){1,4}(?::[0-9a-fA-F]{1,4}){1,3}" + // 4+1-3
"|(?:[0-9a-fA-F]{1,4}:){1,3}(?::[0-9a-fA-F]{1,4}){1,4}" + // 3+1-4
"|(?:[0-9a-fA-F]{1,4}:){1,2}(?::[0-9a-fA-F]{1,4}){1,5}" + // 2+1-5
"|[0-9a-fA-F]{1,4}:(?::[0-9a-fA-F]{1,4}){1,6}" + // 1+1-6
"|:(?::[0-9a-fA-F]{1,4}){1,7}" + // leading ::
"|::" + // all-zeros
")\\]";
// IPv4 address
private static final String IPV4_REGEX = "((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])";
// localhost
private static final String LOCALHOST_REGEX = "localhost";
// Domain name with any TLD (at least two letters).
// Each label must start and end with an alphanumeric character (RFC 1123);
// hyphens are only permitted in the interior of a label.
private static final String DOMAIN_REGEX = "([a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+[a-zA-Z]{2,}";
// Optional port — restricted to the valid TCP/UDP range 0–65535
private static final String PORT_REGEX = "(:(6553[0-5]|655[0-2]\\d|65[0-4]\\d{2}|6[0-4]\\d{3}|[1-5]\\d{4}|\\d{1,4}))?";

// The resulting regular expression validates the authority part of the URL (host and optional port) while allowing any TLD in the domain name.
private static final String AUTHORITY_PART_RX = "^(" + IPV6_REGEX + "|" + IPV4_REGEX + "|" + LOCALHOST_REGEX + "|" + DOMAIN_REGEX + ")" + PORT_REGEX + "$";

private final UrlValidator urlValidator = new UrlValidator(new RegexValidator(AUTHORITY_PART_RX), ALLOW_ALL_SCHEMES+ALLOW_LOCAL_URLS);

/**
* Validates redirect URLs while allowing private/non-IANA TLDs.
*
* @param redirectUrl redirect URL to validate
* @param constraintValidatorContext validation context, unused
* @return true if the redirect URL is valid
*/
@Override
public boolean isValid(String redirectUrl, ConstraintValidatorContext constraintValidatorContext) {
return urlValidator.isValid(redirectUrl);
public boolean isValid(final String redirectUrl, final ConstraintValidatorContext constraintValidatorContext) {
return this.urlValidator.isValid(redirectUrl);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ public void validateRedirectURIPositiveTest() throws EsignetException {
"https://api.dev.mosip.net/home/testament?rr=rrr");
IdentityProviderUtil.validateRedirectURI(Arrays.asList("io.mosip.residentapp://oauth"),
"io.mosip.residentapp://oauth");
// Regression for issue #1883: non-public / private TLDs (.corp, .internal)
// must be accepted by the matchUri path inside validateRedirectURI.
IdentityProviderUtil.validateRedirectURI(Arrays.asList("https://sso.idp.corp/callback"),
"https://sso.idp.corp/callback");
IdentityProviderUtil.validateRedirectURI(Arrays.asList("https://portal.company.internal/**"),
"https://portal.company.internal/auth/callback");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
package io.mosip.esignet.core.validator;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* Verifies that redirect URLs with standard public TLDs are accepted.
*/
public class RedirectURLValidatorTest {

private static final RedirectURLValidator REDIRECT_URL_VALIDATOR = new RedirectURLValidator();

/**
* Tests that valid URLs with standard IANA-registered TLDs are accepted.
*/
@Test
public void standardIanaRegisteredTldsTest() {
// .com
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.com/callback", null));
// .org
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.org/callback", null));
// twoletter TLDs like .de
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.de/callback", null));
// longer TLDs like .technology
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.technology/callback", null));
}

/**
* Tests that valid URLs with non-IANA or custom TLDs are accepted.
*/
@Test
public void nonIanaOrCustomTldTest() {
// .xx is not a real IANA TLD but must be accepted
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://api.dev.mosip.xx/home/test", null));
// .internal is commonly used for private networks
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://myservice.internal/callback", null));
// .local is used in mDNS / private environments
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://myapp.local/callback", null));
// .test is an RFC 2606 reserved name for testing
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://app.test/callback", null));
// private TLD used inside the MOSIP ecosystem
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://api.service.mosip/callback", null));
// non IANA TLD .corp
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://auth.sso.corp/callback", null));
}

/**
* Tests that valid URLs with IPv4 and IPv6 addresses, as well as localhost, are accepted.
*/
@Test
public void ipAddressV4V6Test() {
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("http://192.168.1.1/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("http://10.0.0.1:8080/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("http://localhost/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("http://localhost:8080/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("http://[::1]/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("http://[2001:db8::1]/callback", null));
// invalid: only colons, no hex digits
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("http://[::::]/callback", null));
// invalid: group exceeds four hex digits
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("http://[12345::1]/callback", null));
// invalid: nine groups (too many)
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("http://[1:2:3:4:5:6:7:8:9]/callback", null));
// invalid: non-hex character
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("http://[::gggg]/callback", null));
}

/**
* Tests that valid URLs with ports are accepted and invalid ports are rejected.
*/
@Test
public void portsTest() {
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.com:443/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.com:8443/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.com:65535/callback", null));
// first port value above the valid range must be rejected
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://example.com:65536/callback", null));
// port 0 is accepted (enforcement is left to upper layers)
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.com:0/callback", null));
// clearly out-of-range port must be rejected
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://example.com:99999/callback", null));
}

/**
* Tests that valid URLs with various schemes are accepted.
*/
@Test
public void schemesTest() {
// http
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("http://example.com/callback", null));
// ftp
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("ftp://example.com/callback", null));
// Mobile deep-link used by MOSIP resident app
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("io.mosip.residentapp://oauth", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("myapp://auth.internal/callback", null));
}

/**
* Tests that valid URLs with various path variations, query strings, and fragments are accepted.
*/
@Test
public void pathVariationsAndQueryStringsTest() {
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://api.dev.mosip.net/home/testament?rr=rrr", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid(
"https://api.dev.mosip.net/home/werrrwqfdsfg5fgs34sdffggdfgsdfg?state=reefdf", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://example.com/page#section", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://a.b.c.example.com/callback", null));
Assertions.assertTrue(REDIRECT_URL_VALIDATOR.isValid("https://api.dev.mosip.net/home/test", null));
}

/**
* Tests that invalid URLs are rejected.
*/
@Test
public void invalidUrlTest() {
// null
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid(null, null));
// empty
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("", null));
// A URL without a scheme is not valid
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("example.com/callback", null));
// TLD must be at least two letters; single-char TLD must be rejected
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://example.c/callback", null));
// TLD must consist of letters only, not digits
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://example.123/callback", null));
// 256 is not a valid octet
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("http://256.0.0.1/callback", null));
// space in host
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://exam ple.com/callback", null));
// label must not start with a hyphen (RFC 1123)
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://-bad.example.com/callback", null));
// label must not end with a hyphen (RFC 1123)
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://bad-.example.com/callback", null));
// only scheme
Assertions.assertFalse(REDIRECT_URL_VALIDATOR.isValid("https://", null));
}
}