<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:util="http://www.springframework.org/schema/util"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:c="http://www.springframework.org/schema/c"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"

       default-init-method="initialize"
       default-destroy-method="destroy">

    <!--
    This is a map of transition rules that guide the behavior of the MFA flow
    and controls how factors are sequenced, skipped, etc. The key of each entry
    is the name of the step/flow out of which control is passing. The starting
    rule has an empty key.

    Each entry is a bean inherited from "shibboleth.authn.MFA.Transition". Per
    the Javadoc for net.shibboleth.idp.authn.MultiFactorAuthenticationTransition:

        p:nextFlow (String)
            - A flow to run if the previous step signaled a "proceed" event, for simple
                transitions.

        p:nextFlowStrategy (Function<ProfileRequestContext,String>)
            - A function to run if the previous step signaled a "proceed" event, for dynamic
                transitions. Returning null ends the MFA process.

        p:nextFlowStrategyMap (Map<String,Object> where Object is String or Function<ProfileRequestContext,String>)
            - Fully dynamic way of expressing control paths. Map is keyed by a previously
                signaled event and the value is a flow to run or a function to
                return the flow to run. Returning null ends the MFA process.

    When no rule is provided, there's an implicit "null" that ends the MFA flow
    with whatever event was last signaled. If the "proceed" event from a step is
    the final event, then the MFA process attempts to complete itself successfully.
    -->

    <util:map id="secondFactorHelpersMap">
        <entry key="httpServletRequestContext" value-ref="shibboleth.HttpServletRequest" />
        <entry key="attributeResolver" value-ref="shibboleth.AttributeResolverService" />
        <entry key="fudiscrToken" value-ref="fudiscr.UserHasAnyTokenPredicate" />
    </util:map>

    <!-- https://doku.tid.dfn.de/en:shibidp:plugin-fudiscr -->
    <util:map id="shibboleth.authn.MFA.TransitionMap">
        <!-- 
            first rule
            choose first login flow => authn/Password 
        -->
	 

        <entry key="">
            <bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Password"/>
        </entry>

        <!--
            second rule 
            runs a function if Password succeeds, to determine whether an additional factor is required.
        -->
        <entry key="authn/Password">
            <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="secondFactor"/>
        </entry>

        <!-- An implicit final rule will return whatever the final flow returns. -->
    </util:map>
 
    <!--
        The state of the tokens is not checked here, so a deactivated token would result in fudiscr.UserHasAnyTokenPredicate returning true
    -->
    <bean id="secondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript" 
          p:customObject-ref="secondFactorHelpersMap">
        <constructor-arg>
            <value>
            <![CDATA[
		function ipStr2Val(ipStr) {
			var ipVal = 0;
			var ipArr = ipStr.split('.');
			for (var i=0;i<4;i++) {
				ipVal = ipVal * 256 + parseInt(ipArr[i]);
			}
			return ipVal;    
		}

		function ipAddrMatch(ipStr, ipRangeStr) {
			var ipmnArr = ipRangeStr.split('/');
			var mask = 32 - (2 == ipmnArr.length ? parseInt(ipmnArr[1]) : 32);
			if (ipStr2Val(ipStr) >> mask == ipStr2Val(ipmnArr[0]) >> mask) {
			    return true;
			}
			return false;
		}

                nextFlow = "authn/fudiscr";
                logger = Java.type( "org.slf4j.LoggerFactory" ).getLogger( "de.hzdr.mfa.secondFactor" );

                httpRequestServletContext = custom.get("httpServletRequestContext");
		ip = httpRequestServletContext.remoteAddr;
                logger.info("IP Address:" + ip);

		ipsPermitStr = "%{idp.authn.ipAddress.permit}";
		ipsRequireStr = "%{idp.authn.ipAddress.require}";
		ipsPermitArr = ipsPermitStr.split(' ').join('').split(',');
		ipsRequireArr = ipsRequireStr.split(' ').join('').split(',');
                logger.info("Permit IP address ranges:" + ipsPermitArr );
                logger.info("Require IP address ranges:" + ipsRequireArr );

		match = false;
		for (j=0;j<ipsPermitArr.length;j++) {
			if (ipAddrMatch(ip,ipsPermitArr[j])) {
				logger.info("IP address " + ip + " matches range " + ipsPermitArr[j]);
				match = true;
			}
		}
		for (j=0;j<ipsRequireArr.length;j++) {
			if (ipAddrMatch(ip,ipsRequireArr[j])) {
				logger.info("... but is excluded, because in " + ipsRequireArr[j]);
				match = false;
			}
		}
		if (match) {
			logger.info("IP Address matches -> no second factor needed");	    
			nextFlow = null;
		} else {
			logger.info("IP Address doesn't match -> second factor needed");	    
	                authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext");
	                mfaCtx = authCtx.getSubcontext("net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext");
	                if ( mfaCtx.isAcceptable() ) {
	                    nextFlow = null;
	    
	                    fudiscrToken = custom.get("fudiscrToken");
	                    if (fudiscrToken.test(input)) {
	                        logger.debug( 'Second factor auth does run' );
	                        nextFlow = "authn/fudiscr";
	                    }
	                } else {
	                    logger.debug( "Second factor auth needs to run, but for rollout the token is not needed" );
	                }
		}

    
                nextFlow;    // pass control to second factor or end with the first
            ]]>
            </value>
        </constructor-arg>
    </bean>

</beans>
