Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streamline Flow execution for multiple Duo providers #3498

Merged
merged 17 commits into from
Sep 13, 2018
Merged

Conversation

tsschmidt
Copy link
Contributor

This PR refactors the Duo MFA webflow to determine the provider in an initial action that is added to the conversation scope. Subsequent actions then only need to pull the provider from the scope instead of locating it in the application context.

@mmoayyed
Copy link
Member

mmoayyed commented Aug 29, 2018

I think this looks excellent. One comment here is that the call that puts the provider into the webflow scope is fairly generic; while duo is the only provider at this point that does in fact make use of it. It seems like either we need to move the webflow calls into the duo module and not expose generic static methods that would have one believe the API can be used to fetch a provider regardless of the module used for mfa, or we should take a pass at other providers and ensure we stuff a provider into the webflow scope for all of them.

I do like the second option myself.

@tsschmidt
Copy link
Contributor Author

Are we able to set active mfa provider here: SelectiveAuthenticationProviderWebflowEventEventResolver? Then would we not need "duoInitializeLoginAction"?

@mmoayyed
Copy link
Member

mmoayyed commented Aug 29, 2018

Yes that should work. That's the last resolver in the chain that always executes, and in fact already puts the resolved provider ids into the flow, which should make those static methods unnecessary too! You can get the provider ids that are resolved, and fetch the provider instances from the context in the right spot.

One thing that does require testing is multiple instances of duo. I believe if you have more than one defined, then the resolved provider ids that are put into the conversation-scope there include all duo ids. If that is true, then we are golden here; refactor and remove static method calls and simply use the resolved providers.

@tsschmidt
Copy link
Contributor Author

Got a handle on this late in the day working with multiple Duo instances. I will try and finish up tomorrow morning and get an update committed.

@@ -60,11 +62,14 @@
LOGGER.debug("No multifactor authentication providers are configured");
return Pair.of(Boolean.FALSE, Optional.empty());
}
final Optional<MultifactorAuthenticationProvider> requestedProvider = locateRequestedProvider(providerMap.values(), requestedContext);
final Collection<MultifactorAuthenticationProvider> flattenedProviders = MultifactorAuthenticationUtils.flattenProviders(providerMap.values());
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to unwrap providers if Variegated Provider is found.

@@ -137,9 +143,9 @@
LOGGER.debug("No authentication context could be determined based on authentication attribute [{}]", this.authenticationContextAttribute);
return null;
}
contexts.forEach(context -> providers.removeIf(provider -> !provider.getId().equals(context)));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would fail when the context contained more than one provider.

private final String authenticationContextAttribute;

private final DuoAuthenticationHandler authenticationHandler;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the id of the specific provider as credential metadata so it can be matched correctly in ContextValidator.

@tsschmidt
Copy link
Contributor Author

Latest commits add supporting multiple instances of Duo. The final resolved provider is decided in multiple places. If SelectiveAuthenticationProviderWebflowEventEventResolver returns multiple events then the decision is passed on to the MultfactorFactorProviderSelector that is configured. For this reason I kept the addition of adding the resoloved provider into the webflow conversation scope.

Copy link
Member

@mmoayyed mmoayyed left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If SelectiveAuthenticationProviderWebflowEventEventResolver returns multiple events then the decision is passed on to the MultfactorFactorProviderSelector that is configured.

Could you explain this a bit more? I am not sure I follow here.

if (!requestedProvider.isPresent()) {
LOGGER.debug("Requested authentication provider cannot be recognized.");
return Pair.of(Boolean.FALSE, Optional.empty());
}
LOGGER.debug("RequestedContext is [{}] and Available Contexts are [{}]", requestedContext, contexts);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOGGER.debug("Requested authentication context is [{}] and available contexts are [{}]", requestedContext, contexts);

throw new IllegalArgumentException("No multifactor providers are found in the current request context");
}
final MultifactorAuthenticationProvider pr = providers.iterator().next();
final MultifactorAuthenticationProvider pr = WebUtils.getActiveMultifactorAuthenticationProvider(requestContext);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good starting point, but at some point, we should get this cleaned up. A handler component should not try to reach beyond its limits and deal with webflow scopes. We can clean up this bit later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I was able to clear this up by not using VariegatedMFA Provider and registering multiple authentication handlers into the application context with their specific provider passed to each in the constructor. Submitting it this way for now to limit the scope of changes for a single PR.

*
* @return - the provider id
*/
public String getProviderId() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you mean to make the method private of sorts, it would be better if you actually returned the provider instance, and renamed the method to reflect that bit too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Used currently in DuoAuthenticationMetadaPopulator. could be made protected and still work for the current solution.

}

@RefreshScope
@Bean
public AuthenticationHandler duoAuthenticationHandler() {
public DuoAuthenticationHandler duoAuthenticationHandler() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand why you had to do this, but this needs to be re-worked. We should try to stick to the most generic type possible, and introduce things that can be polymorphically recognized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, there are probably better ways to handle, and made easier if some other decisions are made like dropping VariegatedMFAProvider.

@tsschmidt
Copy link
Contributor Author

if SelectiveAuthenticationProviderWebflowEventEventResolver returns multiple events then the decision is passed on to the MultfactorFactorProviderSelector that is configured.

I think the scenario is mfa-duo-1 is resolved as the global MFA Provider, but the service is MFA policy resolves to mfa-duo-2. Then both are returned from Selective resolver and the decision is then made by the configured MultifactorProviderSelector for the MFA flow. There are currently two instances: RankedMFAProviderSelector and GroovyMFAProviderSelector.

@mmoayyed
Copy link
Member

mmoayyed commented Sep 4, 2018

Then both are returned from Selective resolver and the decision is then made by the configured MultifactorProviderSelector for the MFA flow.

Right. So how about we simply move the putActiveMultifactorAuthenticationProvider right after the selector picks a provider? That way, the behavior applies to all MFA providers, and isn't just a feature of duo.

@tsschmidt
Copy link
Contributor Author

tsschmidt commented Sep 4, 2018 via email

@mmoayyed
Copy link
Member

mmoayyed commented Sep 4, 2018

That makes sense. Thanks!

@tsschmidt
Copy link
Contributor Author

The latest commit removes the use of the Variegated provider from Duo in favor of registering a Bean for each provider created in to the ApplicationContext. This also shifts the resolving of handling and providers to a place where other MFA providers can use the same configuration to offer multiple instances. Key additions and changes are:

  • MfaCredential
    This is an interface that can be implemented to mark the Credential as a created from a MFA provider.
    Adds getter/setter requirements for the provider id that created the credential.
  • MfaAuthenticationHandler
    This is an interface used to mark the AuthenticationHandler that it used to authentic instances of MfaCredential. Adds getter for provider id and a default supports() method that checks if the passed Credential was created by the same provider the AuthenticationHandler uses
  • ByCredentialTypeAuthenticationHandlerResolver
    Added implementation for resolve() that filters the list of handlers by passing the credential to the handler supports() method.
  • MfaInitializeAction
    This action will pull the corresponding MultifactorAuthenticationProvider from the ApplicationContext for the active flow.
  • AbstractCasMultifactorWebflow
    Added an ActionState to call MfaInitializeAction in the MFA based on on STAT_ID_INIT_LOGIN_FORM

Remove activeMultifactorProvider form WebUtils
@mmoayyed
Copy link
Member

mmoayyed commented Sep 6, 2018

Nice. A few comments here:

The latest commit removes the use of the Variegated provider from Duo in favor of registering a Bean for each provider created in to the ApplicationContext.

Do we need to remove the Variegated provider API altogether then, if we have no use for it?

MfaCredential
MfaAuthenticationHandler

I am guessing that these two need to be implemented by all credentials and providers for MFA, and not just duo, right?

AbstractCasMultifactorWebflow
Added an ActionState to call MfaInitializeAction in the MFA based on on STAT_ID_INIT_LOGIN_FORM

How does this point in the webflow deal with this? Does it impact the REST API when MFA is attempted?

Also I have not looked too closely yet but we should be mindful of the scope here and how it might fit into a patch release. Are there any breaking changes, configuration or API-wise?

@@ -273,4 +274,9 @@ public Action serviceWarningAction() {
return new ServiceWarningAction(centralAuthenticationService, authenticationSystemSupport,
ticketRegistrySupport, warnCookieGenerator.getIfAvailable(), principalElectionStrategy);
}

@Bean
public Action mfaInitializeAction() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also not sure if this belongs here. Shouldn't the MFA-related actions, etc be housed in a module that is only required for MFA, and not by default?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do the same with cas-sever-core-webflow-api, that has MFA implementations? Create cas-server-core-webflow-mfa and cas-server-core-weblfow-mfa-api and refactor all MFA classes to these two?

* @author Travis Schmidt
* @since 5.3.4
*/
public class MfaInitializeAction extends AbstractAction {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, really. This probably needs to move into some sort of common MFA module.

@Override
protected Event doExecute(final RequestContext context) throws Exception {
final String activeFlow = context.getActiveFlow().getId();
final MultifactorAuthenticationProvider provider =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This does need to move into MFA Utils (I think there is a similar call in there already)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will look into this along with the entire MFA refactor, MFAUtils not currently in the build for this module.

*
* @return - the provider id
*/
String getProviderId();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be renamed to be more explicit because Credential in master presents a property for source and it would be confusing to have both source and providerId in the same class. Something slightly more verbose like MultifactorProviderId would do, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good

@@ -23,20 +24,33 @@
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(of = {"username"})
public class DuoCredential implements Credential {
public class DuoCredential implements Credential, MfaCredential {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted early, this needs to be implemented by every form of MFA credential and not just Duo.

|| credential instanceof DuoDirectCredential;
return (DuoCredential.class.isAssignableFrom(credential.getClass())
|| credential instanceof DuoDirectCredential)
&& supports((MfaCredential) credential);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is supports((MfaCredential) credential); a recursive call that would cause an overflow issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that I found in my testing. It calls the overloaded default method in MfaAuthenticationHandler interface.

public class DuoAuthenticationMetaDataPopulator extends BaseAuthenticationMetaDataPopulator {
private final String authenticationContextAttribute;

private final DuoAuthenticationHandler authenticationHandler;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be refactored now to use a more generic type? Since you're only adding a name here? And is this not a duplicate of AuthenticationContextAttributeMetaDataPopulator?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it has been refactored into a duplicate of AuthenticationContextAttributeMetaDataPopulator as I worked through the changes. I will remove it.

throw new IllegalArgumentException("At least one Duo instance must be defined");
}
return provider;
public DefaultDuoMultifactorAuthenticationProvider duoMultifactorAuthenticationProviders(final DuoSecurityMultifactorProperties duo) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Switch the type to DuoMultifactorAuthenticationProvider? Make the method private?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

duoP.setBypassEvaluator(MultifactorAuthenticationUtils.newMultifactorAuthenticationProviderBypass(duo.getBypass()));
duoP.setOrder(duo.getRank());
duoP.setId(duo.getId());
applicationContext.getBeanFactory().registerSingleton(duoP.getId()+"-provider", duoP);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use ApplicationContextProvider.registerBeanIntoApplicationContext?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find that method.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might have been looking at master. never mind this :)

}

@RefreshScope
@Bean
public AuthenticationHandler duoAuthenticationHandler() {
public Collection<DuoAuthenticationHandler> duoAuthenticationHandler() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Collection<AuthenticationHandler>. Always stick to the more generic type, where possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@tsschmidt
Copy link
Contributor Author

Do we need to remove the Variegated provider API altogether then, if we have no use for it?

Duo was the only authn method currently using it. If this approach is preferred then I believe it can be removed if we see no future use for it.

I am guessing that these two need to be implemented by all credentials and providers for MFA, and not just duo, right?

It would need to be implemented by all MFA providers that are able to be configure multiple instances of itself. So far that is only Duo. If it makes no sense to provide multiple instances of provider or the provider just can't support it then there is no need to implement these interfaces

How does this point in the webflow deal with this? Does it impact the REST API when MFA is attempted?

So non browser Duo use should work as it is currently coded. The REST API, I will need to look into further.

For breaking changes, if we only do this for Duo in the current releases then there are no breaking changes, that I foresee. Duo configuration already supports defining multiple instances and that is the only breaking change I see if we added to other MFA providers(i.e. authy[0] is not supported).

Scope wise, only applying this PR to Duo still fits within expected behavior of current releases, and only adds the functionality that rank order among Duo instances is now respected and guaranteed. Applying this pattern to other MFA providers should be handled in future releases, but this would give someone a template to use if they needed to implement this behavior locally.

@mmoayyed
Copy link
Member

mmoayyed commented Sep 6, 2018

All sounds good.

  • I would leave the variegated API for now, since technically it cannot be removed in a patch release. But for the master branch, we should try to do without it.
  • Given your description of MfaCredential (and like the related handler), I suggest these components need to then be renamed to explain why and how someone should implement them so the name explicitly indicates objective and intent. Right now, the name suggests that if a credential is related to mfa functionality, then it may be an implementor of this MfaCredential which isn't right by design per your description. This is not an issue really, as picking the right name to convey this sort of thing is difficult and if there are no good candidates, we should remember to revisit the naming scheme for this classes.

@tsschmidt
Copy link
Contributor Author

Latest commit address comments in the last review.

I was not able to find any affect on the REST API, as far as I can tell.

@mmoayyed
Copy link
Member

mmoayyed commented Sep 6, 2018

Looks like all things pass here. Thanks very much! Excellent work. Did you want to handle the master changeset as well? Leave that up to me? Both? :)

@tsschmidt
Copy link
Contributor Author

tsschmidt commented Sep 6, 2018

I am starting the master refactor now, waiting for IDE gradle refresh, Pushing one more commit to fix Codacy issues

tsschmidt added a commit that referenced this pull request Sep 9, 2018
@tsschmidt
Copy link
Contributor Author

1 similar comment
@tsschmidt
Copy link
Contributor Author

@mmoayyed
Copy link
Member

* What went wrong:
Execution failed for task ':support:cas-server-support-shibboleth:findbugsMain'.
> FindBugs rule violations were found. See the report at: file:///home/travis/build/apereo/cas/support/cas-server-support-shibboleth/build/findbugsReports/main.html
* Try:
Run with --info or --debug option to get more log output. Run with --scan to get full insights.
* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':support:cas-server-support-shibboleth:findbugsMain'.
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:110)
	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:77)
	at org.gradle.api.internal.tasks.execution.OutputDirectoryCreatingTaskExecuter.execute(OutputDirectoryCreatingTaskExecuter.java:51)
	at org.gradle.api.internal.tasks.execution.SkipCachedTaskExecuter.execute(SkipCachedTaskExecuter.java:105)
	at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:59)

Probably related to #3507

@mmoayyed mmoayyed merged commit 0becacd into 5.3.x Sep 13, 2018
@mmoayyed mmoayyed deleted the multi-duo branch September 13, 2018 16:05
mmoayyed pushed a commit that referenced this pull request Oct 12, 2018
* Refactor to separate MFA webflows from initial flows
Includes multi-duo refactor from PR #3498

* Typo fix
Refactor to prevent NoSuchElementException

* Ported MFA refactor changes from PR #3518 for 5.3.x

* Refactor dynamic providers to config and made sure refresh scope works

* revert webapp.gradle

* Refactored the MultifactorAuthenticationProvider  creation

* Changed mfa webflow configurer to key off of start state

* Added AbstractMultifactorAuthentication
Refactored AuthententicationContextAttributeMetaDataPopulator

* Added Mark to identify credentials added by provider
Set providerId into flowscope to look up provider

* Fix Tests

* Fix Tests

* api-mfa refactor
namoing refactor

* fix case of single provider

* fix webflow mfa test

* fix grouper mfa test

* fix openid test

* Refactored MultifactorAuthenticationCredential

* Refactored MultifactorAuthenticationCredential DirectCredential

* updated mfa documentation

* Removed Variegated provider
Ported forward changes from 5.3.4

* Fix DuoAuthenticationHandler supports

* Fix codacy issue

* Fix webflow mfa test

* Fix for validation tests

* try to fix ci build

* Fix surrogate mfa tests

* Fix checkstyle

* fix spot bugs

* Add mfa to tests

* Fix for multivalue auth atributes
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants