Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Security - Authenticate not by IP but by Domain/SubDomain?

I have a Spring based web service that I want to provide Spring security. Its working and that it can authenticate through USER and ADMIN roles. However I have a new requirement that I need to authenticate a request not of the USER and ADMIN roles but with the subdomain that the request came from.

Typically, there is the authentication by IP:

 <http use-expressions="true">
    <intercept-url pattern="/admin*"
        access="hasRole('admin') and hasIpAddress('192.168.1.0/24')"/>
    ...
  </http>

However, my case is quite different, I need to authenticate based on domain and subdomain where the request came from.

Like:

jim.foo.com 
tim.foo.com

Where jim.foo.com and tim.foo.com have the same IP address. And each subdomain gets authenticated separately.

Is it possible?

like image 548
quarks Avatar asked Dec 12 '22 15:12

quarks


2 Answers

As requested by @franz-ebner in a comment to @zayagi's answer I can give a concrete full example here. @zayagi's answer is a perfect answer - this is just to help others with specific use cases.

This example was written in Java 8 when Spring Boot was at 1.1.6 with Spring 4.0.x (Spring boot is now 1.3.0 with Spring 4.2 which contains web configuration improvements). This was written at the end of 2014 and may need refreshing, but providing it as-is since it was requested and may be able to help others. I can't provide the tests because they have specific IP addresses in them that I do not want to share ;-)

The first class defines spring security expressions (e.g. isCompanyInternal()) and includes support for the x-forwarded-for header. With this header you should not trust it in all situations because it can be added by anyone and can pose a security threat. For this reason, only certain internal ip ranges are trusted with this header.

    package org.mycompany.spring.security.web.acccess.expression;

    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.expression.WebSecurityExpressionRoot;
    import org.springframework.security.web.util.matcher.IpAddressMatcher;
    import org.springframework.util.Assert;

    import java.util.Optional;
    import java.util.stream.Stream;

    public class MyCompanyWebSecurityExpressionRoot extends WebSecurityExpressionRoot {
        public static final String LOCALHOST = "127.0.0.1";

        public static final String COMPANY_DESKTOPS = "-suppressed for example-";
        public static final String COMPANY_INTERNET_1 = "-suppressed for example-";
        public static final String COMPANY_INTERNET_2 = "-suppressed for example-";

        /**
         * See http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
         */
        public static final String RFC_1918_INTERNAL_A = "10.0.0.0/8";
        /**
         * See http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
         */
        public static final String RFC_1918_INTERNAL_B = "172.16.0.0/12";
        /**
         * See http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
         */
        public static final String RFC_1918_INTERNAL_C = "192.168.0.0/16";


        private IpAddressMatcher[] internalIpMatchers;
        private IpAddressMatcher trustedProxyMatcher;

        public MyCompanyWebSecurityExpressionRoot(Authentication a, FilterInvocation fi) {
            super(a, fi);
            setInternalIpRanges(RFC_1918_INTERNAL_A,
                    COMPANY_INTERNET_1,
                    COMPANY_INTERNET_2,
                    COMPANY_DESKTOPS,
                    RFC_1918_INTERNAL_B,
                    RFC_1918_INTERNAL_C,
                    LOCALHOST);
            setTrustedProxyIpRange(RFC_1918_INTERNAL_A);
        }

        public boolean hasAnyIpAddress(String... ipAddresses) {
            return Stream.of(ipAddresses)
                    .anyMatch(ipAddress -> new IpAddressMatcher(ipAddress).matches(request));
        }

        public boolean hasAnyIpAddressBehindProxy(String trustedProxyRange, String... ipAddresses) {
            String remoteIpAddress = getForwardedIp(trustedProxyRange).orElseGet(request::getRemoteAddr);
            return Stream.of(ipAddresses)
                            .anyMatch(ipAddress -> new IpAddressMatcher(ipAddress).matches(remoteIpAddress));
        }

        public boolean isCompanyInternal() {
            String remoteIpAddress = getForwardedIp(trustedProxyMatcher).orElseGet(request::getRemoteAddr);
            return Stream.of(internalIpMatchers)
                    .anyMatch(matcher -> matcher.matches(remoteIpAddress));
        }

        /**
         * <p>This specifies one or more IP addresses/ranges that indicate the remote client is from the company network.</p>
         *
         * <p>If not set, this will default to all of the following values:</p>
         *     <ul>
         *         <li>{@code RFC_1918_INTERNAL_A}</li>
         *         <li>{@code RFC_1918_INTERNAL_B}</li>
         *         <li>{@code RFC_1918_INTERNAL_C}</li>
         *         <li>{@code COMPANY_INTERNET_1}</li>
         *         <li>{@code COMPANY_INTERNET_2}</li>
         *         <li>{@code COMPANY_DESKTOPS}</li>
         *         <li>{@code LOCALHOST}</li>
         *     </ul>
         *
         * @param  internalIpRanges ip addresses or ranges. Must not be empty.
         *
         */
        public void setInternalIpRanges(String... internalIpRanges) {
            Assert.notEmpty(internalIpRanges, "At least one IP address/range is required");
            this.internalIpMatchers = Stream.of(internalIpRanges)
                    .map(IpAddressMatcher::new)
                    .toArray(IpAddressMatcher[]::new);
        }

        /**
         * <p>When checking for the <code>x-forwarded-for</code> header in the incoming request we will only use
         * that value from a trusted proxy as it can be spoofed by anyone. This value represents the IP address
         * or IP range that we will trust.</p>
         *
         * <p>The default value if this is not set is {@code RFC_1918_INTERNAL_A}.</p>
         *
         * @param  trustedProxyIpRange ip address or range. Must not be null.
         *
         */
        public void setTrustedProxyIpRange(String trustedProxyIpRange) {
            Assert.notNull(trustedProxyIpRange, "A non-null value is for trusted proxy IP address/range");
            this.trustedProxyMatcher = new IpAddressMatcher(trustedProxyIpRange);
        }

        private Optional<String> getForwardedIp(String trustedProxyRange) {
            return getForwardedIp(new IpAddressMatcher(trustedProxyRange));
        }

        private Optional<String> getForwardedIp(IpAddressMatcher trustedProxyMatcher) {
            String proxiedIp = request.getHeader("x-forwarded-for");
            if (proxiedIp != null && trustedProxyMatcher.matches(request.getRemoteAddr())) {
                return Optional.of(proxiedIp);
            }
            return Optional.empty();
        }

    }

The second class defines the expression handler that you inject when configuring your security.

    package org.mycompany.spring.security.web.acccess.expression;

    import org.springframework.security.access.expression.SecurityExpressionOperations;
    import org.springframework.security.authentication.AuthenticationTrustResolver;
    import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.web.FilterInvocation;
    import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;

    public class MyCompanyWebSecurityExpressionHandler extends DefaultWebSecurityExpressionHandler {
        private static final AuthenticationTrustResolver TRUST_RESOLVER = new AuthenticationTrustResolverImpl();

        private String[] customInternalIpRanges;
        private String customTrustedProxyIpRange;


        @Override
        protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication, FilterInvocation fi) {
            MyCompanyWebSecurityExpressionRoot root = new MyCompanyWebSecurityExpressionRoot(authentication, fi);
            root.setPermissionEvaluator(getPermissionEvaluator());
            root.setTrustResolver(TRUST_RESOLVER);
            root.setRoleHierarchy(getRoleHierarchy());

            if (customInternalIpRanges != null) {
                root.setInternalIpRanges(customInternalIpRanges);
            }
            if (customTrustedProxyIpRange != null) {
                root.setTrustedProxyIpRange(customTrustedProxyIpRange);
            }

            return root;
        }

        /**
         * <p>Only set this if you want to override the default internal IP ranges defined within
         * {@link MyCompanyWebSecurityExpressionRoot}.</p>
         *
         * <p>See {@link MyCompanyWebSecurityExpressionRoot#setInternalIpRanges(String...)}</p>
         *
         * @param customInternalIpRanges ip address or ranges
         */
        public void setCustomInternalIpRanges(String... customInternalIpRanges) {
            this.customInternalIpRanges = customInternalIpRanges;
        }

        /**
         * Only set this if you want to override the default trusted proxy IP range set in
         * {@link MyCompanyWebSecurityExpressionRoot}.
         *
         * @param customTrustedProxyIpRange ip address or range
         */
        public void setCustomTrustedProxyIpRange(String customTrustedProxyIpRange) {
            this.customTrustedProxyIpRange = customTrustedProxyIpRange;
        }
    }

Finally here is an example of using these together: @Configuration public static class WebInternalSecurityConfig extends WebSecurityConfigurerAdapter {

        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring()
                    .antMatchers("/favicon.ico", "/robots.txt");
        }

        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                .csrf().disable()
                .authorizeRequests()
                .expressionHandler(new MyCompanyWebSecurityExpressionHandler())
                    .anyRequest().access("isCompanyInternal()");
        }
    }
like image 107
Matt Byrne Avatar answered Mar 02 '23 22:03

Matt Byrne


It's possible to define your own functions beyond the built-in ones that are defined in SecurityExpressionRoot and its subclass WebSecurityExpressionRoot. You only need to extend the latter, add your own functions that isnpect the request object the way you like, and then configure Spring Security to use that instead of the default one (WebSecurityExpressionRoot). Here is how:

  1. Override DefaultWebSecurityExpressionHandler.createSecurityExpressionRoot() in a subclass that constructs your own SecurityExpressionRoot implementation containing your custom functions.
  2. Create a bean of this custom handler and make a reference to it with <expression-handler ref="yourCustomSecurityExpressionRootHandler"> within the <http> config element.
like image 25
zagyi Avatar answered Mar 02 '23 22:03

zagyi