-
Notifications
You must be signed in to change notification settings - Fork 0
Shibboleth IdP Integration
George Thomas edited this page Jul 28, 2022
·
3 revisions
- Install Shibboleth IdP V4.2.1+
- Enable Shibboleth MFA FLow
- Install & Enable Shibboleth TOTP Plugin
- Install TOTP Manager
-
Log in to TOTP Manager with the admin credentials.
-
Go to Settings and copy the Client ID and the Client Secret.
-
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>
-
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"/>
-
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>
-
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>
-
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>
-
Restart Shibboleth IdP.