Skip to content

Shibboleth IdP Integration

George Thomas edited this page Jul 28, 2022 · 3 revisions

Prerequisites

Integration

Steps

  1. Log in to TOTP Manager with the admin credentials.

  2. Go to Settings and copy the Client ID and the Client Secret.

  3. Add the following in /opt/shibboleth-idp/conf/idp.properties and replace <clientId> and <clientSecret> with the values copied from previous step.

    # TOTP plugin values
    idp.totp.URL = <TOTP Manager URL>
    idp.totp.clientID = <Client ID>
    idp.totp.clientSecret = <Client Secret>
  4. Add the following in /opt/shibboleth-idp/conf/global.xml

    <util:map id="httpHeaders">
        <entry key="Authorization" value="%{idp.totp.clientID} %{idp.totp.clientSecret}" />
    </util:map>
    
    <util:map id="shibboleth.CustomViewContext">
        <entry key="view" value-ref="shibboleth.CustomViewContext"/>
    </util:map>
    
    <bean id="shibboleth.NonCachingHttpClient" lazy-init="true"
        class="net.shibboleth.idp.profile.spring.relyingparty.metadata.HttpClientFactoryBean"/>
  5. Add the following in /opt/shibboleth-idp/conf/attribute-resolver.xml

    <AttributeDefinition xsi:type="Simple" id="tokenSeeds">
    	<InputDataConnector ref="myHTTP" attributeNames="seed" />
    </AttributeDefinition>
    
    <DataConnector id="myHTTP" xsi:type="HTTP"
        httpClientRef="shibboleth.NonCachingHttpClient"
        acceptTypes="application/json"
        headerMapRef="httpHeaders"
        exportAttributes="body">
        <URLTemplate>
            <![CDATA[
            %{idp.totp.URL}/api/users/enrollment/$pathEscaper.escape($resolutionContext.principal)
            ]]>
        </URLTemplate>
    
        <ResponseMapping>
            <Script>
            <![CDATA[
            logger = Java.type('org.slf4j.LoggerFactory').getLogger('http-check');
    
            var HashSet = Java.type("java.util.HashSet");
            var HttpClientSupport = Java.type("net.shibboleth.utilities.java.support.httpclient.HttpClientSupport");
            var IdPAttribute = Java.type("net.shibboleth.idp.attribute.IdPAttribute");
            var StringAttributeValue = Java.type("net.shibboleth.idp.attribute.StringAttributeValue");
    
            // Limits length to 64k
            var body = HttpClientSupport.toString(response.getEntity(), "UTF-8", 65536);
            var result = JSON.parse(body);
    
            // Seed attribute
            var attr = new IdPAttribute("seed");
            var value = new HashSet();
    
            value.add(new StringAttributeValue(result.seed));
    
            attr.setValues(value);
            connectorResults.add(attr);
    
            // Response body attribute
            var attr = new IdPAttribute("body");
            var value = new HashSet();
    
            value.add(new StringAttributeValue(body));
    
            attr.setValues(value);
            connectorResults.add(attr);
    
            ]]>
            </Script>
        </ResponseMapping>
    </DataConnector>
  6. Replace the contents in /opt/shibboleth-idp/conf/authn/mfa-authn-config.xml with the following

    <?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="myMap">
            <entry key="resolver">
                <ref bean="shibboleth.AttributeResolverService" />
            </entry>
            <entry key="viewRef">
                <ref bean="shibboleth.CustomViewContext" />
            </entry>
        </util:map>
    
        <util:map id="shibboleth.authn.MFA.TransitionMap">
            <!-- First rule runs the Password login flow. -->
            <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="checkSecondFactor" />
            </entry>
            <!-- An implicit final rule will return whatever the final flow returns. -->
        </util:map>
    
        <!-- Example script to see if second factor is required. Currently just returns the TOTP flow -->
        <bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript" p:customObject-ref="myMap">
            <constructor-arg>
                <value>
                <![CDATA[
                    nextFlow = "authn/TOTP";
    
                    logger = Java.type('org.slf4j.LoggerFactory').getLogger('mfa-check');
    
                    // Attribute check is required to decide if first factor alone is enough.
                    resCtx = input.getSubcontext(
                        "net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext", true);
    
                    // Look up the username
                    usernameLookupStrategyClass = Java.type("net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy");
                    usernameLookupStrategy = new usernameLookupStrategyClass();
                    resCtx.setPrincipal(usernameLookupStrategy.apply(input));
    
                    // resolve the attribute to determine if a first factor is sufficient
                    resCtx.getRequestedIdPAttributeNames().add("body");
                    resCtx.resolveAttributes(custom["resolver"]);
    
                    // Check for an attribute value that authorizes use of first factor.
                    bodyAttribute = resCtx.getResolvedIdPAttributes().get("body");
    
                    bodyAttributeValue = bodyAttribute.getValues();
    
                    logger.info(bodyAttributeValue[0].value);
    
                    result = JSON.parse(bodyAttributeValue[0].value);
    
                    custom["viewRef"].get("view").put("URL", "%{idp.totp.URL}");
    
                    custom["viewRef"].get("view").put("enrolled", result.enrolled);
                    custom["viewRef"].get("view").put("token", result.token);
    
                    if (!result.enrolled) {
                        custom["viewRef"].get("view").put("qrCode", result.qrCode);
                        custom["viewRef"].get("view").put("seed", result.seed);
                    }
    
                    input.removeSubcontext(resCtx);   // cleanup
    
                    nextFlow;   // pass control to second factor or end with the first
                ]]>
                </value>
            </constructor-arg>
        </bean>
    
    </beans>
  7. Replace the contents in /opt/shibboleth-idp/views/totp.vm with the following

    ##
    ## Velocity Template for DisplayTOTPView view-state
    ##
    ## Velocity context will contain the following properties
    ## flowExecutionUrl - the form action location
    ## flowRequestContext - the Spring Web Flow RequestContext
    ## flowExecutionKey - the SWF execution key (this is built into the flowExecutionUrl)
    ## profileRequestContext - root of context tree
    ## authenticationContext - context with authentication request information
    ## authenticationErrorContext - context with login error state
    ## authenticationWarningContext - context with login warning state
    ## rpUIContext - the context with SP UI information from the metadata
    ## encoder - HTMLEncoder class
    ## request - HttpServletRequest
    ## response - HttpServletResponse
    ## environment - Spring Environment object for property resolution
    ## custom - arbitrary object injected by deployer
    ##
    #set ($rpContext = $profileRequestContext.getSubcontext('net.shibboleth.idp.profile.context.RelyingPartyContext'))
    ##
    <!DOCTYPE html>
    <html>
        <head>
            <title>#springMessageText("idp.title", "Web Login Service")</title>
            <meta charset="UTF-8" />
            <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
            <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
            <link rel="stylesheet" type="text/css" href="$request.getContextPath()#springMessageText("idp.css", "/css/placeholder.css")">
        </head>
        <body>
            <main class="main">
                <header>
                    <img class="main-logo" src="$request.getContextPath()#springMessageText("idp.logo", "/images/placeholder-logo.png")" alt="#springMessageText("idp.logo.alt-text", "logo")" />
    
                    #set ($serviceName = $rpUIContext.serviceName)
                    #if ($serviceName && !$rpContext.getRelyingPartyId().contains($serviceName))
                        <h1>#springMessageText("idp.login.loginTo", "Login to") $encoder.encodeForHTML($serviceName)</h1>
                    #end
                </header>
    
                <section>
                    <form action="$flowExecutionUrl" method="post">
                        #parse("csrf/csrf.vm")
    
                        #*
                        //
                        //    SP Description & Logo (optional)
                        //    These idpui lines will display added information (if available
                        //    in the metadata) about the Service Provider (SP) that requested
                        //    authentication. These idpui lines are "active" in this example
                        //    (not commented out) - this extra SP info will be displayed.
                        //    Remove or comment out these lines to stop the display of the
                        //    added SP information.
                        //
                        *#
                        #set ($logo = $rpUIContext.getLogo())
                        #if ($logo)
                            <img class="service-logo" src= "$encoder.encodeForHTMLAttribute($logo)" alt="$encoder.encodeForHTMLAttribute($serviceName)">
                        #end
                        #set ($desc = $rpUIContext.getServiceDescription())
                        #if ($desc)
                            <p>$encoder.encodeForHTML($desc)</p>
                        #end
    
                        #parse("totp-error.vm")
    
                        #if (!$custom.enrolled)
                            <h2>Use an authenticator app (such as Google Authenticator) to generate time-based verification codes.</h2>
    
                            <h3>Scan the QR Code in the authenticator app.</h3>
    
                            <img src=$custom.qrCode alt="QR Code">
                            <h3>Once scanned the app should give you a 6 digit Token Code. Enter it here.</h3>
                        #end
    
                        <label for="tokencode">#springMessageText("idp.totp.field", "Token Code")</label>
                        <input id="tokencode" name="tokencode" type="text" value="" />
    
                        <div class="grid">
                            <div class="grid-item">
                                <button type="submit" name="_eventId_proceed"
                                    onClick="verifyCode()"
                                    >#springMessageText("idp.login.login", "Login")</button>
                            </div>
                        </div>
                    </form>
    
                    <ul>
                        <li><a href="#springMessageText("idp.url.password.reset", '#')">#springMessageText("idp.login.forgotPassword", "Forgot your password?")</a></li>
                        <li><a href="#springMessageText("idp.url.helpdesk", '#')">#springMessageText("idp.login.needHelp", "Need Help?")</a></li>
                    </ul>
                </section>
            </main>
            <footer class="footer">
                <div class="cc">
                    <p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
                </div>
            </footer>
    
            <script>
                function verifyCode() {
                    const totp = document.getElementById("tokencode").value;
    
                    const token = "${custom.token}";
    
                    const data = {
                        totp
                    };
    
                    fetch(`${custom.URL}/api/user/log`, {
                        method: "POST",
                        headers: {
                            "Content-Type": "application/json",
                            "Authorization": `bearer ${token}`,
                        },
                        body: JSON.stringify(data),
                    });
                }
            </script>
        </body>
    </html>
    
  8. Restart Shibboleth IdP.

Clone this wiki locally