Konrad
Konrad

Reputation: 1635

Spring Secuirty Oauth 2 - multiple user authentication services

My application provides oauth2 token service identical to this one provided in the following github project: https://github.com/iainporter/oauth2-provider

It is based on Spring Security OAuth2.

I provided my custom implementation of UserDetailsService:

<bean id="userService" class="org.example.core.service.DBUserServiceImpl" />

and the following user authentication manager:

<sec:authentication-manager alias="userAuthenticationManager">
    <sec:authentication-provider user-service-ref="userService">
        <sec:password-encoder ref="passwordEncoder" />
    </sec:authentication-provider>
</sec:authentication-manager>

Now I would like to provide other method of user authentication (other UserDetailsService), for example:

<bean id="otherUserService" class="org.example.core.service.LDAPUserServiceImpl" />

Unfortunately I didn't find a way how to do it in documentation. On the request level I would like to distinguish which method (which user service) to use by either:

Upvotes: 5

Views: 11196

Answers (2)

Konrad
Konrad

Reputation: 1635

I found different solution than solution provided by Mithun.

Application context contains user authentication manager initiated with different authentication providers:

<sec:authentication-manager alias="userAuthenticationManager">
    <sec:authentication-provider ref="customerAuthProvider" />
    <sec:authentication-provider ref="adminAuthProvider" />
</sec:authentication-manager>

where customerAuthProvider and adminAuthProvider are extensions of DaoAuthenticationProvider with different userDetails Service:

<bean id="customerAuthProvider" class="org.example.security.authentication.provider.CustomerAuthenticationProvider">
    <property name="userDetailsService" ref="userService" />
    <property name="passwordEncoder" ref="passwordEncoder" />
</bean>

<bean id="adminAuthProvider" class="org.example.security.authentication.provider.AdminAuthenticationProvider">
    <property name="userDetailsService" ref="otherUserService" />
</bean>

All you need to do is to override "supports" method that indicates whether current authentication provider is able to handle specific authentication:

public class CustomerAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    public boolean supports ( Class<?> authentication ) {
        return CustomerUsernamePasswordAuthenticationToken.isAssignableFrom(authentication);
    }
}

public class AdminAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    public boolean supports ( Class<?> authentication ) {
        return AdminUsernamePasswordAuthenticationToken.isAssignableFrom(authentication);
    }
}

At the end you need to extend token granter. In my case I extended ResourceOwnerPasswordTokenGranter which means that it supports "password" grant:

<oauth:authorization-server client-details-service-ref="client-details-service" token-services-ref="tokenServices">
    <oauth:refresh-token/>
    <oauth:custom-grant token-granter-ref="customPasswordTokenGranter"/>
</oauth:authorization-server>

You can use TokenRequest object to distinguish which Authentication class to instantiate (AdminUsernamePasswordAuthenticationToken or CustomerUsernamePasswordAuthenticationToken)

public class CustomResourceOwnerPasswordTokenGranter extends ResourceOwnerPasswordTokenGranter {

    protected OAuth2Authentication getOAuth2Authentication ( ClientDetails client, TokenRequest tokenRequest ) {
        Map parameters = tokenRequest.getRequestParameters();
        String username = (String) parameters.get("username");
        String password = (String) parameters.get("password");

        String realmName = (String) parameters.get("realm_name");

        Authentication userAuth = createAuthentication(username, password, realmName);
        try {
            userAuth = this.authenticationManager.authenticate(userAuth);
        } catch ( AccountStatusException ase ) {
            throw new InvalidGrantException(ase.getMessage());
        } catch ( BadCredentialsException e ) {
            throw new InvalidGrantException(e.getMessage());
        }
        if ( ( userAuth == null ) || ( ! ( userAuth.isAuthenticated() ) ) ) {
            throw new InvalidGrantException("Could not authenticate user: " + username);
        }

        OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }

    private Authentication createAuthentication ( String username, String password, String realmName ) throws InvalidGrantException {
       // TODO: decide basing on realm name
    }
}

Upvotes: 3

Mithun
Mithun

Reputation: 8067

You need to use DelegatingAuthenticationEntryPoint to configure multiple entry points. Which means you can have multiple ways of authenticating. Following is the sample code:

DBUser entry point:

public class DBUserAuthencticationEntryPoint extends BasicAuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
        super.commence(request, response, authException);
    }
}

LDAP entry point:

public class LDAPAuthencticationEntryPoint extends BasicAuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {
         super.commence(request, response, authException);
    }
}

Then you need to create RequestMatchers to pick the correct entry point (based on header/realm name):

DBUser request matcher:

RequestMatcher dbUserMatcher = new RequestMatcher() {       
    @Override
    public boolean matches(HttpServletRequest request) {
        // Logic to identify a DBUser kind of reqeust
    }
};

LDAP user requset matcher:

RequestMatcher ldapMatcher = new RequestMatcher() {     
    @Override
    public boolean matches(HttpServletRequest request) {
        // Logic to identify a LDAP kind of reqeust
    }
};

Now we need to add these matchers and entry points to DelegatingAuthenticationEntryPoint. In runtime DelegatingAuthenticationEntryPoint picks up the entry point and does the authentication based on the matcher which return true.

DBUserAuthencticationEntryPoint dbUserEntryPoint = new DBUserAuthencticationEntryPoint();
LDAPAuthencticationEntryPoint ldapEntryPoint  = new LDAPAuthencticationEntryPoint();

LinkedHashMap<RequestMatcher,AuthenticationEntryPoint> entryPoints = new LinkedHashMap<RequestMatcher,AuthenticationEntryPoint>();
entryPoints.put(ldapMatcher, ldapEntryPoint);
entryPoints.put(dbUserMatcher, dbUserEntryPoint);

DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint  = new DelegatingAuthenticationEntryPoint(entryPoints);

Now map the DelegatingAuthenticationEntryPoint to HttpSecurity in the configure() method:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.
            authorizeRequests().
                regexMatchers("/login.*").permitAll().
                regexMatchers("/api.*").fullyAuthenticated().        
        and().
            formLogin().loginPage("/login").
        and().
            exceptionHandling().authenticationEntryPoint(delegatingAuthenticationEntryPoint);
    }
}

Configure the provider manager:

@Bean
public AuthenticationManager authenticationManager() {
    return new ProviderManager(Arrays.asList(provider1, provider2);
}

Upvotes: 6

Related Questions