Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • query parameter
  • http header (e.g. RealmName)
like image 847
Konrad Avatar asked Jan 13 '15 07:01

Konrad


People also ask

Is OAuth2RestTemplate deprecated?

RELEASE classes such as OAuth2RestTemplate , OAuth2ProtectedResourceDetails and ClientCredentialsAccessTokenProvider have all been marked as deprecated.

How does OAuth2 2.0 work in spring boot?

Spring Security OAuth2 − Implements the OAUTH2 structure to enable the Authorization Server and Resource Server. Spring Security JWT − Generates the JWT Token for Web security. Spring Boot Starter JDBC − Accesses the database to ensure the user is available or not. Spring Boot Starter Web − Writes HTTP endpoints.

Does Spring Security support PKCE?

The latest version of Spring Security (5.2. 1 as of this writing) supports OAuth 2.0 and OpenID Connect natively. It supports PKCE for public clients.

Can I have multiple WebSecurityConfigurerAdapter?

When using Java configuration, the way to define multiple security realms is to have multiple @Configuration classes that extend the WebSecurityConfigurerAdapter base class – each with its own security configuration. These classes can be static and placed inside the main config.


2 Answers

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);
}
like image 87
Mithun Avatar answered Oct 19 '22 22:10

Mithun


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
    }
}
like image 32
Konrad Avatar answered Oct 19 '22 23:10

Konrad