Precollection of Username During Authentication
There are some use cases for which it’s essential to obtain a username prior to authentication rather than in conjunction with collecting a password. Usually these cases involve scenarios in which the identity of the user (unauthenticated, obviously, so self-asserted by the user) is needed to determine how authentication should happen. It’s obviously essential that a user not be able to subvert a policy in some way; typically this approach is used in order to allow a user to self-nominate that they have the ability to do something that not all users are able to do, such as use a token-based method (because they have such a token).
The IdP does not have such a feature built-in, but it’s relatively simple to construct a simple flow and view template to peform this step and leverage the information in the MFA login flow. This article includes examples showing how to do this while performing an attribute lookup based on the identity to make a login method determination (some scripting is needed but no Java).
Custom Flow Example
The first step is to produce a custom flow to be added to the IdP that prompts for the username. This is not a login flow, but will be executed by the MFA flow as an initial step. The file containing the flow definition must have the appropriate name and be dropped into a like-named subdirectory under the IdP’s flows folder. The flow definition itself is fully “declarative” and does not rely on any Java code.
The flow definition assumes that the (as with the password form convention), the username field will be named “j_username”. In plain English, what the flow does is present a view template to prompt for the username, pull it from the servlet request as a parameter and store it in a dedicated context node designed for that purpose, and then loop back to the form if and only if the username was not supplied.
Place in flows/collectusername/collectusername-flow.xml
<flow xmlns="http://www.springframework.org/schema/webflow"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
parent="authn.abstract">
<!--
Example flow to prompt for a username for examination by MFA flow.
-->
<view-state id="PromptForUsername" view="collect-username">
<on-render>
<evaluate expression="environment" result="viewScope.environment" />
<evaluate expression="opensamlProfileRequestContext" result="viewScope.profileRequestContext" />
<evaluate expression="flowRequestContext.getExternalContext().getNativeRequest()" result="viewScope.request" />
<evaluate expression="flowRequestContext.getExternalContext().getNativeResponse()" result="viewScope.response" />
<evaluate expression="flowRequestContext.getActiveFlow().getApplicationContext().containsBean('shibboleth.CustomViewContext') ? flowRequestContext.getActiveFlow().getApplicationContext().getBean('shibboleth.CustomViewContext') : null" result="viewScope.custom" />
</on-render>
<transition on="proceed" to="ExtractUsernameFromFormRequest" />
</view-state>
<action-state id="ExtractUsernameFromFormRequest">
<evaluate expression="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.authn.context.UsernameContext), true).setUsername(flowRequestContext.getExternalContext().getNativeRequest().getParameter('j_username'))" />
<evaluate expression="'proceed'" />
<!-- Let the validate action handle any problems later. -->
<transition to="CheckUsername" />
</action-state>
<decision-state id="CheckUsername">
<if test="opensamlProfileRequestContext.getSubcontext(T(net.shibboleth.idp.authn.context.AuthenticationContext)).getSubcontext(T(net.shibboleth.idp.authn.context.UsernameContext), true).getUsername() != null"
then="proceed"
else="PromptForUsername" />
</decision-state>
</flow>
The second file required is the view template to render the collection form. The only firm rule is that the filename needs to correspond to the view
attribute in the <view-state>
element in the flow and it must contain a form with an input field whose name matches the parameter retrieved in the flow’s extraction step.
Place in views/collect-username.vm
##
## Velocity Template for DisplayUsernamePasswordPage 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
## request - HttpServletRequest
## response - HttpServletResponse
## environment - Spring Environment object for property resolution
## custom - arbitrary object injected by deployer
##
<!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">
<section>
<form action="$flowExecutionUrl" method="post">
#parse("csrf/csrf.vm")
<label for="username">#springMessageText("idp.login.username", "Username")</label>
<input id="username" name="j_username" type="text" value="" />
<div class="grid">
<div class="grid-item">
<button type="submit" name="_eventId_proceed"
onClick="this.childNodes[0].nodeValue='Please wait...'"
>Continue</button>
</div>
</div>
</form>
</section>
</main>
<footer class="footer">
<div class="cc">
<p>#springMessageText("idp.footer", "Insert your footer text here.")</p>
</div>
</footer>
</body>
</html>
MFA Flow Example
Given the previous custom flow, the following demonstrates a simple way to use it for one possible use case, determining if the volunteered user is enrolled into a special authentication option as an alternative to using a password. The structure of the example is:
Run the username collection flow.
Resolve a special attribute that indicates whether that user is enrolled.
If enrolled, run the special Passwordless login flow instead of the Password flow (obviously the actual Passwordless flow is not part of this example). Otherwise run the standard Password flow and then optionally the second factor flow.
The example assumes that the site has deployed the Duo technology (obviously anything similar would apply) as a second factor for ordinary cases, and that the special login flow is an alternative to that combination. In other words, it potentially accomodates SPs requesting or requiring multi-factor authentication, but also assumes that the special login flow is equivalent to both of those “typical” factors. Configuring the various flows and supported Principal sets is out of scope of this example, but is required in order for the example to actually work correctly.
In conf/authn/mfa-authn-config.xml
<?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">
<util:map id="shibboleth.authn.MFA.TransitionMap">
<!-- First rule runs the username collection login flow. -->
<entry key="">
<bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="collectusername" />
</entry>
<!--
Second rule runs a function that does a username-based lookup for a policy attribute.
-->
<entry key="collectusername">
<bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="lookupUsername" />
</entry>
<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 check for policy value. -->
<bean id="lookupUsername" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"
p:customObject-ref="shibboleth.AttributeResolverService">
<constructor-arg>
<value>
<![CDATA[
nextFlow = "authn/Password";
authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext");
username = authCtx.getSubcontext("net.shibboleth.idp.authn.context.UsernameContext").getUsername();
resCtx = input.getSubcontext(
"net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext", true);
// Look up the username
resCtx.setPrincipal(username);
// resolve the attribute to determine if a first factor is sufficient
resCtx.getRequestedIdPAttributeNames().add("enrolled");
resCtx.resolveAttributes(custom);
attribute = resCtx.getResolvedIdPAttributes().get("enrolled");
valueType = Java.type("net.shibboleth.idp.attribute.StringAttributeValue");
if (attribute != null && attribute.getValues().contains(new valueType("true"))) {
nextFlow = "authn/Passwordless"
}
input.removeSubcontext(resCtx); // cleanup
nextFlow; // pass control to the appropriate login method
]]>
</value>
</constructor-arg>
</bean>
<!-- Example script to see if second factor is required. -->
<bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript">
<constructor-arg>
<value>
<![CDATA[
nextFlow = "authn/DuoOIDC";
// Check if second factor is necessary for request to be satisfied.
authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext");
mfaCtx = authCtx.getSubcontext("net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext");
if (mfaCtx.isAcceptable()) {
nextFlow = null;
}
nextFlow; // pass control to second factor or end with the first
]]>
</value>
</constructor-arg>
</bean>
</beans>