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:
RELEASE classes such as OAuth2RestTemplate , OAuth2ProtectedResourceDetails and ClientCredentialsAccessTokenProvider have all been marked as deprecated.
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.
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.
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.
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 RequestMatcher
s 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);
}
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With