Tuesday, March 20, 2012

CAS - authentication vs. principal resolver mismatch

Lately we found an annoying bug in our CAS login system. If you read already some of my older posts you might know that we had to login against multiple domains in our system which resulted in a complex configuration in our deployerConfigContext.xml.

A sample of that is shown here:

<!-- AUTHENTICATION MANAGERS -->
<bean id="authenticationManagerFirst" class="org.jasig.cas.authentication.AuthenticationManagerImpl">
<property name="credentialsToPrincipalResolvers">
<list>
<ref bean="credentialsToPrincipalResolverFirst" />
<ref bean="credentialsToPrincipalResolverSecond" />
<bean
class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver"
p:attributeRepository-ref="attributeRepositoryJdbc" />
<bean
class="org.jasig.cas.authentication.principal.HttpBasedServiceCredentialsToPrincipalResolver" />
</list>
</property>
<property name="authenticationHandlers">
<list>
<ref bean="authenticationHandlerFirst" />
<ref bean="authenticationHandlerSecond" />
<ref bean="authenticationHandlerJdbc" />
<bean
class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
p:httpClient-ref="httpClient" />
</list>
</property>
</bean>

In our opinion this would work like this, that it goes through the list of authentication handlers looking for the first one that could authenticate the users credentials. Lets say it was found in authenticationHandlerSecond it would go and look for the right principal resolver to get all the needed attributes to append to the response.

Unfortunately the later step does not know which authentication handler was used in first step, so it starts over to go through the list until it finds a resolver that knows about the username that wants to get access. In this step there is no more password involved so it could be that if the first principal resolver knows about the same username and returns that parameters (even though the password is not the same!). Furthermore if the principal belongs to another user you'll be logged in as that one. Quite bad!

Fortunately the solution is quite simple. Instead of using the standard AuthenticationManagerImpl we create our own manager class like the following

public final class DomainAuthenticationManagerImpl extends AbstractAuthenticationManager {
/** An array of authentication handlers. */
@NotNull
@Size(min = 1)
private List authenticationHandlers;
/** An array of CredentialsToPrincipalResolvers. */
@NotNull
@Size(min = 1)
private List credentialsToPrincipalResolvers;
@Override
protected Pair authenticateAndObtainPrincipal(final Credentials credentials) throws AuthenticationException {
 

This one is basically a copy of the original AuthenticationManagerImpl just having the following changes

// save the position in which the authenticationHandler was found
int authenticationHandlerPosition = 0;
for (final AuthenticationHandler authenticationHandler : this.authenticationHandlers) {
if (authenticationHandler.supports(credentials)) {
[..]
}
authenticationHandlerPosition++;
}
[..]
if (!authenticated) {
if (foundSupported) {
throw BadCredentialsAuthenticationException.ERROR;
}
throw UnsupportedCredentialsException.ERROR;
}
// Not check in every CredentialsToPrincipalResolver, but only in the one on the same position in the list where the authenticationHandlerhas been found above
// for (final CredentialsToPrincipalResolver credentialsToPrincipalResolver : this.credentialsToPrincipalResolvers) {
if (authenticationHandlerPosition >= 0
&& authenticationHandlerPosition < this.credentialsToPrincipalResolvers.size()) {
CredentialsToPrincipalResolver credentialsToPrincipalResolver = this.credentialsToPrincipalResolvers
.get(authenticationHandlerPosition);
[..]

With this small changes we can remember on which position the authentication handler was found and just test with the resolver on the same position in the list of resolvers.

1 comment:

Hannes Tribus said...

Has anyone encountered a situation where CAS authenticates user #1, but returns users#2 session information.